Немногие компании могут заявить, что изобрели глагол или хотя бы переделали его. Глаголы, которые были, одинаковы во всех языках мира. Самый известный, пожалуй, «Google». Вы что-то гуглите, то есть ищете.

Хотя есть еще более ранняя, более коварная, настолько, что почти оторвалась от первоначального владельца, а именно «фотошоп». Фотографии описываются как обработанные в фотошопе, даже если приложение Adobe «Photoshop» не использовалось для их изменения, но я думаю, что Adobe не возражает, поскольку все это бесплатная реклама.

Конечно, сегодня у Adobe самое обширное в мире портфолио творческих приложений, и она незаметно завоевала рынок с другими приложениями, такими как Adobe Illustrator и Adobe After Effects. Последний пакет отлично подходит для создания спецэффектов в неземных видео. Присоединяйтесь ко мне, чтобы узнать, как мы можем использовать SwiftUI для воссоздания некоторых звуковых эффектов, используя строки с атрибутами, анимацию и немного магии Swift.

Исследовать

Я начал этот проект на этой странице, которая принадлежит Adobe — или, конечно, близкому родственнику, если не принадлежит. На нем бесчисленные шаблоны демонстрируют предварительно созданные шаблоны SFX After Effects. Многие шаблоны пишут слова буква за буквой, почти все буквы и слова перемещаются по странице. Они часто меняют непрозрачность и цвета и изменяют углы.

Начало

Я начал проект с еще одного взгляда на кернинг. Я думал, что мне нужно будет разработать какой-нибудь хитрый код для его захвата, но, к счастью, в этом не было необходимости, потому что я обнаружил, что NSString сделает это за меня.

for w in 0..<words.count {
    let word = words[w]
    if let font = UIFont(name: "Neuton-Regular", size: 48) {
        let fontAttributes = [NSAttributedString.Key.font: font]
        let text = "\(word)"
        let boxValue = (text as NSString).size(withAttributes: fontAttributes)
        let boundingBox = (text as NSString).boundingRect(with: boxValue, context: nil)
        boxes.append(boxValue)
        bounding.append(boundingBox)
    }
}
if boxes.count == words.count {
    isReady = true
}

Значение ограничительной рамки имеет стандартную ширину независимо от отображаемого символа шрифта. Затем я создал представление, используя HStack для отображения строки, с которой я начал. Вот код:

struct WordView: View {
    @EnvironmentObject var paras:Paras
    @Binding var boxes:[CGSize]
    @Binding var isReady:Bool
    var body: some View {
        HStack(spacing: paras.spaces) {
                if isReady {
                    ForEach(0..<words.count, id: \.self) { dix in
                        LetterView(boxes: $boxes, letter2D: words[dix],index2D: dix)
                    }
                }
        }
    }
}

Представление, которое само содержит другое, а именно это.

struct LetterView: View {
    @EnvironmentObject var paras:Paras
    @Binding var boxes:[CGSize]
    @State var letter2D:String
    @State var index2D:Int
    
    var body: some View {
        HStack {
            Text(letter2D)
                .font(Fonts.neutonRegular(size: 256))
                .foregroundColor(Color.white)
                .offset(paras.offsets[index2D])
                .animation(.easeOut(duration: 1), value: paras.offsets)
                .rotation3DEffect(.degrees(paras.angles[index2D]), axis: (x:paras.axis[index2D].x.cg, y: paras.axis[index2D].y.cg,z:paras.axis[index2D].z.cg), anchor: .center, anchorZ: 0, perspective: 2)
                .animation(.easeOut(duration: 1), value: paras.angles)
                .scaleEffect(paras.zoomer[index2D])
                .animation(.easeOut(duration: 1), value: paras.zoomer)
                .opacity(paras.alphas[index2D])
                .animation(.easeIn(duration: 1), value: paras.alphas)
                .padding(.trailing,0)
                .zIndex(10)
            
        }
    }
}

Обратите внимание, что объект окружения, на который ссылается этот код, представляет собой набор параметров. Значения, которые я могу изменить, влияют на некоторые аспекты отдельных символов в отображаемой строке.

class Paras: ObservableObject {
    static var counts = words.count
    @Published var spaces = -4.0
    @Published var zoomer:[Double] = Array(repeating: 1, count: counts)
    @Published var angles:[Double] = Array(repeating: 0, count: counts)
    @Published var offsets:[CGSize] = Array(repeating: CGSize.zero, count: counts)
    @Published var axis:[SIMD3<Int>] = Array(repeating: SIMD3<Int>(x: 0, y: 1, z: 0), count: counts)
    @Published var alphas:[Double] = Array(repeating: 1, count: counts)
    @Published var colours:[Color] = Array(repeating: Color.white, count: counts)
}

let screenSize: CGRect = UIScreen.main.bounds
let screenWidth: CGFloat = UIScreen.main.bounds.width
let screenHeight: CGFloat = UIScreen.main.bounds.height

Используя кнопки меню, я могу изменить непрозрачность, угол, положение и масштаб символов в строке, отображаемой по отдельности, и даже применить к ним 3D-преобразование.

Более

Но более того, я хотел обеспечить максимальную гибкость в отношении окраски указанной строки. Поэтому я решил реализовать его как маску. За ним я воспроизвел файл фонового фильма, который скачал с этого сайта. Я совершил этот подвиг с помощью этого кода:

struct AVPlayerControllerRepresented : UIViewControllerRepresentable {
    var player : AVPlayer
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let controller = AVPlayerViewController()
        controller.player = player
        controller.showsPlaybackControls = false
        NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: .main) { [self] _ in
            self.player.seek(to: CMTime.zero)
            self.player.play()
        }
        return controller
    }
    
    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
        
    }
}

Я нашел этот полезный совет по зацикливанию видео на Stack Overflow. За вас это сделает версия плеера — но не та, которую я реализовал здесь.



Наконец, собрав все вместе, вот основное ContentView.

struct ContentView: View {
    @StateObject var paras = Paras()
    
    @State var boxes:[CGSize] = []
    @State var bounding:[CGRect] = []
    @State var isReady = false
    @State var id = 0
    @State var bitPattern = "0"
    
        let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "fire", ofType: "mp4")!))
        var body: some View {
            ZStack {
              // initalization of boxes mask goes here
              VStack {
                    AVPlayerControllerRepresented(player: player)
                        .onAppear {
                            player.play()
                            player.actionAtItemEnd = .none
                        }
                        .scaleEffect(4.0)
                        .offset(y: -312)
                        .mask(WordView(boxes: $boxes, isReady: $isReady))
                        .font(Fonts.neutonRegular(size: 312))
                    Actions(bitPattern: $bitPattern)
                }
            }.environmentObject(paras)
        }
}

Интерфейс с полдюжиной кнопок дал мне возможность запускать различные звуковые эффекты. Все действия выглядят как этот шаблон:

Text("hide")
    .font(Fonts.neutonRegular(size: 24))
    .foregroundColor(Color.red)
    .onTapGesture(count: 2) {
        for i in stride(from: words.count - 1, through: 0, by: -1) {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
                paras.angles[i] = 90.0
            }
        }
    }
    .onTapGesture(count: 1) {
        for i in stride(from: words.count - 1, through: 0, by: -1) {
            paras.angles[i] = 0
        }
    }.padding()

В общем, вот второй монтаж SFX, который вы можете получить с этой установкой. Я использовал тот же код с другим видео, чтобы начать статью.

Чистый SwiftUI

Если вы следите, вы правильно спрашиваете, что случилось с кодом размеров кернинга. Я имею в виду, в чем был смысл? Я, в конце концов, еще не использовал его. Закомментируйте код плеера и добавьте в микс эту структуру:

struct BackView: View {
    @EnvironmentObject var paras:Paras
    @Binding var boxes:[CGSize]
    @Binding var isReady:Bool
    var body: some View {
        HStack(spacing: paras.spaces) {
                if isReady {
                    let max = Double(words.count)
                    ForEach(0..<words.count, id: \.self) { dix in
                        let min = Double(dix + 1)
                        let hue = min / max
                        let color = Color(hue: hue, saturation: 1.0, brightness: 1.0)
                        let mode = LinearGradient(gradient: Gradient(colors: [.black,color]), startPoint: .top, endPoint: .bottom)
                        RecView(index2D: dix, letter2D: words[dix], color2D: mode, boxes: $boxes)
                    }
                }
        }
    }
}

Вот код, который ему нужен:

struct RecView: View {
    @EnvironmentObject var paras:Paras
    @State var index2D:Int
    @State var letter2D:String
    @State var color2D:LinearGradient
    @Binding var boxes:[CGSize]
   
    var body: some View {
        HStack(spacing: paras.spaces) {
                Rectangle()
                    .fill(color2D)
                    .offset(paras.offsets[index2D])
                    .frame(width: boxes[index2D].width, height: boxes[index2D].height * 3)
                    .padding(.trailing,0)
                    .rotation3DEffect(.degrees(paras.angles[index2D]), axis: (x:paras.axis[index2D].x.cg, y: paras.axis[index2D].y.cg,z:paras.axis[index2D].z.cg), anchor: .center, anchorZ: 0, perspective: 2)
   
                    .animation(.easeOut(duration: 4), value: paras.offsets)
                    .scaleEffect(paras.zoomer[index2D])
        }
    }
}

И, наконец, добавьте код BackView в закомментированный раздел ContentView.

Теперь запустите его. Предполагая, что вы изменили слово, вы должны увидеть что-то вроде этого:

Я использовал LinearGradient, чтобы раскрасить буквы. Я изменил этот градиент на черно-белый с помощью кнопки Fire. Я создал этот эффект, используя «комбинированный издатель», который я добавил в файл RecView().

.onReceive(colorPublisher, perform: { message in
    let (color,index) = message
    if index == index2D {
        let color2R = color2D
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
            color2D = color
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
                color2D = color2R
            }
        }
    }
})

Конечно, все остальные звуковые эффекты, которые я использовал в предыдущем примере, тоже присутствуют, поэтому вы можете заставить текст исчезать по одной букве за раз, вращать его, переворачивать в любом направлении и т. д.

Вот еще один вариант использования TimeLineView для создания строки сканирования, которая проходит за маской с помощью этого кода.

TimelineView(.periodic(from: .now, by: 0.010)) { timeline in
    Rectangle()
        .fill(LinearGradient(colors: [.black,.red], startPoint: .leading, endPoint: .trailing))
        .frame(width: 16, height: 256)
        .position(x: CGFloat(xPos), y: 200)
        .onChange(of: timeline.date, perform: { newValue in
            let max = Double(screenWidth)
            let min = Double(xPos + 1)
            let hue = min / max
            paras.colours[0] = Color(hue: hue, saturation: 1.0, brightness: 1.0)
            xPos = xPos < screenWidth ? xPos:0
            xPos += 4
        })
        .mask(WordView(boxes: $boxes, isReady: $isReady))
        .scaleEffect(y:2)
}

Вот код, который производит это. Вы можете прочитать слово?

Набор сцен

Наличие более реактивной среды за маской имеет больше смысла. Замените фон этим кодом. Это настроит простой вид сцены, а затем подписчика для создания ящиков, которые упадут на землю.

let wedgePublisher = PassthroughSubject<Color,Never>()
var wedgeSubscriber:AnyCancellable!

class GameScene: SKScene {
    override func didMove(to view: SKView) {
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
        
        wedgeSubscriber = wedgePublisher
            .sink(receiveValue: { [self] color in
                let newCord = CGPoint(x:Double.random(in: 0..<screenWidth),y:screenHeight)
                let box = SKSpriteNode(color: UIColor(color), size: CGSize(width: 64, height: 64))
                box.position = newCord
                box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 64, height: 64))
                addChild(box)
            })
    }

}
struct GameView: View {
    var scene: SKScene {
        let scene = GameScene()
        scene.size = CGSize(width: screenWidth, height: screenWidth)
        scene.scaleMode = .fill
        return scene
    }

    var body: some View {
        SpriteView(scene: scene)
            .frame(width: screenWidth, height: screenHeight)
            .ignoresSafeArea()
    }
    
}

Когда вы закончите с этим, создайте кнопку действия, чтобы добавить ее в представление, которое вызовет wedgePublisher сто двадцать восемь раз.

struct Actions3:View {
    var body: some View {
        Text("fire")
            .font(Fonts.neutonRegular(size: 24))
            .foregroundColor(Color.red)
            .onTapGesture(count: 2) {
                let max = Double(128)
                for i in 0..<128 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.1) {
                        let min = Double(i + 1)
                        let hue = min / max
                        let color = Color(hue: hue, saturation: 1.0, brightness: 1.0)
                        wedgePublisher.send(color)
                    }
                }
            }
    }
}

Наконец, добавьте новые представления в основной ContentView с помощью этого кода:

Добавив его, попробуйте запустить. Это будет выглядеть примерно так. Я использовал SKShapeObject в этом видео и projectionEffect в этом заголовке, чтобы превратить мой обычный шрифт в курсив. Вы можете найти отличную статью о Stack Overflow здесь.

.projectionEffect(ProjectionTransform(CATransform3DMakeRotation(-10 * (.pi / 180), 0.0, 0.0, 1.0)))

Наконец, еще один тег, который я не затронул, — это shadow. Но это не обычная тень, у которой есть общий эффект разбиения тени на три основных цвета — красный, зеленый и синий.

struct NewView: View {
    
    @State private var timeRemaining = 0.0
    
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    let shadowRange:CGFloat = 4
    
    @State var redShadow = Color.clear
    @State var greenShadow = Color.clear
    @State var blueShadow = Color.clear
    
    @State var redSX:CGFloat = 8
    @State var redSY:CGFloat = 8
    @State var blueSX:CGFloat = 8
    @State var blueSY:CGFloat = -8
    @State var greenSX:CGFloat = -8
    @State var greenSY:CGFloat = -8
    
    @State var offsets = CGSize.zero
    @State private var offset = CGSize.zero
    
    var body: some View {
        
        Text("shadows")
            .font(Fonts.neutonRegular(size: 128))
            .foregroundColor(Color.white)
            .offset(offsets)
            .onTapGesture {
                timeRemaining = 1.0
            }
            .shadow(color: greenShadow, radius: 0.5, x:blueSX, y:blueSY)
            .shadow(color: redShadow, radius: 0.5, x:redSX, y:redSY)
            .shadow(color: blueShadow, radius: 0.5, x:greenSX, y:greenSY)
            .onReceive(timer) { time in
                if timeRemaining > 0 {
                    greenSX = offset.width > 0 ? 4.0 : -4.0
                    greenSY = offset.height > 0 ? 4.0 : -4.0
                    redSX = offset.width > 0 ? -4.0 : 4.0
                    redSY = offset.height > 0 ? 4.0 : -4.0
                    blueSX = offset.width > 0 ? -4.0 : 4.0
                    blueSY = offset.height > 0 ? -4.0 : 4.0
                    withAnimation(.linear(duration: 0.5)) {
                        redShadow = Color.clear
                        greenShadow = Color.clear
                        blueShadow = Color.clear
                    }
                }
            }
            .gesture(
                DragGesture()
                    .onChanged { gesture in
                        offset = gesture.translation
                        withAnimation(.linear(duration: 0.25)) {
                            redShadow = Color.red
                            greenShadow = Color.green
                            blueShadow = Color.blue
                        }
                    }
                    .onEnded { _ in
                        if abs(offset.height) > 10 || abs(offset.width) > 10 {
                            offsets = offset
                        } else {
                            offsets = .zero
                        }
                        timeRemaining = 1
                    }
            )
    }
}

Фрагмент кода, который создает слегка преувеличенную версию эффекта RGB для отображения движения. Я использую жест перетаскивания, чтобы перемещать текст целиком по экрану. Жест запускает анимацию.

Попробуйте немного пошалить с ним; изменить время и смещения для теней.

Все это подводит меня к концу этой части — я надеюсь, что чтение дало вам некоторые идеи для ваших названий.