SwiftUIで花火エフェクトを作成（高度なアニメーション）

SwiftUIで花火エフェクトが簡単に作成できます。
この記事では、そうしたエフェクトの作り方を順を追って説明していきます。
また、SwiftUIで高度なアニメーションを設計する方法を学びましょう!

![ezgif-3-8f467ca52f.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/eec1c5d9-5ade-64c9-14d1-ad0979ef2d8a.gif)

## 花火粒子の動きの分析

効果をデザインするには、時間の経過とともにオブジェクトがどのように動くかを理解する必要があります。

花火の場合は、個別の粒子が多くあります。
粒子ごとに動きはランダムになります。
全体的な形状は以下の図のようになります：

![firework-overall-diagram.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/509d2c70-a296-b002-7718-18598726f07e.jpeg)

そこで、1つの粒子（丸で囲んだ粒子）だけの動きを重点的に解析することにした：

![IMG_58038EED76FC-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/3b631ff2-6c23-e961-6ee1-466143d60332.jpeg)

粒子は右上の位置に移動している。この動きは、数学（cosとsin）を使って、x方向とy方向の移動の方程式に当てはめることが出来る。

今は花火の中にはたくさんの小さな粒子が入っていることがわかり、それぞれの粒子がどのように動くかも分かっている。
あくまでアニメーション効果なので、各粒子の正確な動きをシミュレートする必要はなく、各粒子の動きにランダムな値を与えて、花火のように見せれば良かった。

例えばこの場合、x軸方向における個々の粒子の位置は`cos(angle 角度) * speed 速度 * time 時間`に等しくなる。角度は0度から360度までの間でランダムに選択されるため、花火の粒子はそれぞれ様々な方向に移動する。速度はそれぞれの粒子に対してランダムに生成される定数であるため、粒子の移動速度は様々である。時間変数は、アニメーション開始から経過した時間を指している。

角度は各パーティクルのランダムな値です。パーティクルの数が多いと、花火のように見えます。

## 単一の粒子の移動効果の設計

まずは、上の図の青い円で囲まれた粒子のように、単一の粒子の移動効果を設計していきます。

各粒子が円の方向にランダムに移動する場合（0度から360度、または-PiからPi度）、粒子が移動する方向はランダムになります。

```swift
struct ParticlesFireworkEffect : GeometryEffect {
    
    var time: Double
    var speed: Double = .random(in: 10...100)
    var direction: Double = .random(in: -Double.pi ... Double.pi)
    
    var animatableData: Double {
        get { return time }
        set { time = newValue }
    }
    
    func effectValue(size: CGSize) -> ProjectionTransform {
        let translationX = cos(direction) * speed * time
        let translationY = sin(direction) * speed * time
        let translation = CGAffineTransform(translationX: translationX, y: translationY)
        return ProjectionTransform(translation)
    }
    
}
```

`effectValue`関数では、上の図のsinとcosの計算に基づいて、x軸とy軸での粒子の動きを計算しています。

この関数の戻り値は、粒子の`ProjectionTransform`座標位置を表しています。

## アニメーション効果全体の設計

これで1つのパーティクルに対するアニメーションの設定が完了したので、
このようなパーティクル100個に対してアニメーションを適用していきます。

```swift
struct ParticlesModifier: ViewModifier {
    
    @State var time = 0.0
    @State var scale = 0.1
    var duration = 5.0
    var particlesCount: Int = 100
    
    func body(content: Content) -> some View {
        
        ZStack {
            ForEach(0..<particlesCount, id: \.self) { _ in
                content
                    .scaleEffect(scale)
                    .opacity((duration - time) / duration)
                    .modifier(ParticlesFireworkEffect(time: time))
            }
        }
        .onAppear {
            withAnimation (.easeOut(duration: duration)) {
                self.time = duration
                self.scale = 1.0
            }
        }
        
    }
    
}
```

ご覧のとおり、上記のコードには多数のパーティクルに対してパーティクル効果を適用するためのループが用意されています。
コードのリセットでは、パーティクルの不透明度とスケールを変更します。

## 花火エフェクトのあるビューを作成する

ここでは、上記のビュー・モディファイアのみを使用して、花火エフェクトを作成します。

```swift
struct ParticlesModifier_Preview: PreviewProvider {
    static var previews: some View {
        ZStack {
            
            Circle()
                .fill(Color.blue)
                .frame(width: 12, height: 12)
                .modifier(ParticlesModifier())
                .offset(x: -100, y : -50)
            
            Circle()
                .fill(Color.red)
                .frame(width: 12, height: 12)
                .modifier(ParticlesModifier())
                .offset(x: 60, y : 70)
        }
    }
}
```

---

Happy ゴールデンウィーク！！

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます：
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">