アニメーションとお近づき ~すのうふれいくを君と 第ゼロ章~

どうも、はぐっです・ω・♪

## はじめに
フロントエンドに興味が特化しているゆえに、そっち方面とお近づきになりたいわけです。
ってことで、今回はアニメーションとの身体的・精神的距離をどうにか(

## テーマ
初雪にざわめく街で
雪が綺麗と笑うのは君がいい
・・・ってことで、雪を降らせよう！っていう感じ。

## 完成物
今回作ったものはこんな感じ。

![advent.gif](https://qiita-image-store.s3.amazonaws.com/0/81434/b05e1a83-ef45-3a6a-e35c-b32bcc7979b6.gif)


雪、降らせた。
ループはさせてないので、永遠に降るわけではない。

まぁ、実際結晶の画像を使ってるので、雪ってかでっかい結晶が落ちてくる感じやけど。
この辺りは、まぁご愛嬌ってことで。

## 作り方
で、この動きをどうやって作ったかっていうのを、今回は2パターンやってみた。
ちょっと違うけど、だいたい似たようなことを異なる形でお届けしてみる。
正解は一つじゃないかもしれない！っていう、あれね。

どちらも、ボタンを押したら

```swift

final class SnowViewController: UIViewController {
  @IBAction private func didTapStartButton(_ sender: UIButton) {}
}
```

に入ってきて、アニメーションが始まるっていう前提で進めまーす。

じゃあ、二通り実装を見ていく、と。
なお、どちらの手法もCoreAnimationを利用している。

### 途中の計算をCoreAnimationに任せるパターン
キーワードは **「CABasicAnimation」**
CABasicAnimationは、簡単に言うと
**始まり、終わり、秒数、動き方を指定したら、それに従ってよしなに動かせてくれるやーつ**
かな。

この値からこの値まで、この秒数で、こういう変化の仕方で動いてほしい
途中はお前に任せる。

全貌はこんな感じ。

```swift

final class SnowViewController: UIViewController {

  @IBAction private func didTapStartButton(_ sender: UIButton) {
    self.createSnows()
  }

  // 雪いっぱい作って、動かす
  private func createSnows() {
    for sequence in 0..<80 {
      let imageView = self.createSnow()
      self.view.addSubview(imageView)
      self.animateSnow(imageView: imageView, sequence: sequence)
    }
  }

  // 雪いっこ作る
  private func createSnow() -> UIImageView {
    let image               = UIImage(named: "snow")
    let imageView           = UIImageView(image: image)
    let scale               = CGFloat(Double(arc4random_uniform(10)) / 50)
    imageView.center        = self.calculatePoint()
    imageView.transform     = imageView.transform.scaledBy(x: scale, y: scale)
    imageView.layer.opacity = 0.0
    return imageView
  }

  // 画面のどっかいい感じの位置取得
  private func calculatePoint() -> CGPoint {
    let x = CGFloat(arc4random_uniform(UInt32(self.view.bounds.width)))
    let y = CGFloat(arc4random_uniform(UInt32(self.view.bounds.height)))
    return CGPoint(x: x, y: y)
  }

  // 雪のアニメーションを諸々作成し、付与
  private func animateSnow(imageView: UIImageView, sequence: Int) {
    let duration = CFTimeInterval((arc4random_uniform(5) + 3) * 2)

    let translateAnimation = self.createTranslateAnimation(duration: duration)
    let rotateAnimation    = self.createRotateAnimation(duration: duration, sequence: sequence)
    let opacityAnimation   = self.createOpacityAnimation(duration: duration)

    imageView.layer.add(translateAnimation , forKey: "transform.translation.y")
    imageView.layer.add(rotateAnimation    , forKey: "transform.rotation.z")
    imageView.layer.add(opacityAnimation   , forKey: "opacity")
  }

  // 移動アニメーション作成
  private func createTranslateAnimation(duration: CFTimeInterval) -> CABasicAnimation {
    let animation            = CABasicAnimation(keyPath: "transform.translation.y")
    animation.fromValue      = 0.0
    animation.toValue        = CGFloat(arc4random_uniform(200))
    animation.duration       = duration
    animation.timingFunction = Easing.easeIn.sine.function
    return animation
  }

  // 回転アニメーション作成
  private func createRotateAnimation(duration: CFTimeInterval, sequence: Int) -> CABasicAnimation {
    let animation            = CABasicAnimation(keyPath: "transform.rotation.z")
    animation.fromValue      = 0.0
    animation.toValue        = CGFloat(arc4random_uniform(4) + 1) * (sequence % 2 == 0 ? CGFloat.pi : -CGFloat.pi)
    animation.duration       = duration
    animation.timingFunction = Easing.easeIn.sine.function
    return animation
  }

  // 透過度アニメーション作成
  private func createOpacityAnimation(duration: CFTimeInterval) -> CAKeyframeAnimation {
    let startRatio = arc4random_uniform(4)
    let endRatio   = CGFloat(arc4random_uniform(5 - startRatio) + startRatio)

    let animation            = CAKeyframeAnimation(keyPath: "opacity")
    animation.values         = [0.0, 1.0, 1.0, 0.0]
    animation.keyTimes       = [0.0, Double(startRatio + 2) / 10, Double(endRatio + 3) / 10, 1.0] as [NSNumber]
    animation.duration       = duration
    animation.timingFunction = Easing.easeInOut.sine.function
    return animation
  }
}
```

コード的にはこんな感じなんですよ、ええ。

で、まぁ、注目すべきは下の三つかな。

まずこいつ。

```swift
// 移動アニメーション作成
private func createTranslateAnimation(duration: CFTimeInterval) -> CABasicAnimation {
  let animation            = CABasicAnimation(keyPath: "transform.translation.y")
  animation.fromValue      = 0.0
  animation.toValue        = CGFloat(arc4random_uniform(200))
  animation.duration       = duration
  animation.timingFunction = Easing.easeIn.sine.function
  return animation
}
```

CABasicAnimationっていうのが、CoreAnimationの基本のやーつ。
初期化時に何を動かすかっていうkeyPathを指定するんやけど、これは文字列で指定するガバガバな感じかと思いきや、何が指定できるか決まっている。
この辺りを参考に。
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AnimatableProperties/AnimatableProperties.html#//apple_ref/doc/uid/TP40004514-CH11-SW2
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Key-ValueCodingExtensions/Key-ValueCodingExtensions.html#//apple_ref/doc/uid/TP40004514-CH12-SW8

ここの場合は「transform.translation.y」
つまり、y軸方向の移動に関するアニメーションを作りまっせと。

で、こっからいろんな値を指定して動かすのだが、、、
もはや以前書いたのではしょる。
https://qiita.com/haguhoms/items/abc5635e8fa95719cb12

んで、こいつ。

```swift
// 回転アニメーション作成
private func createRotateAnimation(duration: CFTimeInterval, sequence: Int) -> CABasicAnimation {
  let animation            = CABasicAnimation(keyPath: "transform.rotation.z")
  animation.fromValue      = 0.0
  animation.toValue        = CGFloat(arc4random_uniform(4) + 1) * (sequence % 2 == 0 ? CGFloat.pi : -CGFloat.pi)
  animation.duration       = duration
  animation.timingFunction = Easing.easeIn.sine.function
  return animation
}
```
こいつはもうほぼほぼ言うことない。
「transform.rotation.z」
z軸方向の回転に関するアニメーションを作る。いじょっ。

最後はちょっと新キャラが出てくる。

```swift
// 透過度アニメーション作成
private func createOpacityAnimation(duration: CFTimeInterval) -> CAKeyframeAnimation {
  let startRatio = arc4random_uniform(4)
  let endRatio   = CGFloat(arc4random_uniform(5 - startRatio) + startRatio)

  let animation            = CAKeyframeAnimation(keyPath: "opacity")
  animation.values         = [0.0, 1.0, 1.0, 0.0]
  animation.keyTimes       = [0.0, Double(startRatio + 2) / 10, Double(endRatio + 3) / 10, 1.0] as [NSNumber]
  animation.duration       = duration
  animation.timingFunction = Easing.easeInOut.sine.function
  return animation
}
```
CAKeyframeAnimationというやつを使いだした。
キーフレームアニメーションに関して詳しくは調べてもらうとして、
要は、タイミングと、そのタイミングでの値を指定して、それを一連の流れとして動かす手法。
今回でいうと、4つのタイミングを指定していて、
最初と最後で0.0、つまり不可視状態に
指定タイミングで1.0、つまり可視状態に
途中は、CoreAnimationくん、**わかるよね？**
っていう感じ。
こういう0.0から1.0にいって、一定期間保って、また0.0に、みたいなちょっと変わったことしたいときはCAKeyframeAnimationが便利。
(今回でいうと他にもやりようはあるけど、使ってみる)

で、この三つのアニメーションを作って、対象のCALayerにaddするとアニメーションが付与される。
きたこれー！

### 途中の計算を自力でやるパターン
キーワードは **「CADisplayLink」**
CADisplayLinkは、簡単に言うと
**描画タイミングごとに呼ばれるループ**
かな。

描画タイミングごとに俺を呼んでくれ
そのとき対象物をどうするかは俺が計算する。

まず、計算を自力でやるということで、
いろいろと情報を持たせたstructを作ることにする。
中身の詳細はあとで。

```swift
struct Snow {
  let imageView          : UIImageView
  let x                  : CGFloat
  let startY             : CGFloat
  let endY               : CGFloat
  let angle              : CGFloat
  let scale              : CGFloat
  let duration           : TimeInterval
  let opacityFirstPoint  : TimeInterval
  let opacitySecondPoint : TimeInterval

  init(parentViewSize: CGSize = UIScreen.main.bounds.size) {
    self.imageView          = UIImageView(image: UIImage(named: "snow"))
    self.imageView.alpha    = 0.0
    self.x                  = CGFloat(arc4random_uniform(UInt32(parentViewSize.width + self.imageView.bounds.width * 1.5))) - self.imageView.bounds.width * 0.75
    self.startY             = CGFloat(arc4random_uniform(UInt32(parentViewSize.height + self.imageView.bounds.height * 1.5))) - self.imageView.bounds.height * 0.75
    self.endY               = self.startY + CGFloat(arc4random_uniform(200))
    self.angle              = CGFloat(arc4random_uniform(4) + 1) * (arc4random_uniform(1) == 0 ? CGFloat.pi : -CGFloat.pi)
    self.scale              = CGFloat(Double(arc4random_uniform(10)) / 10)
    self.duration           = TimeInterval((arc4random_uniform(5) + 3) * 2) // 6 - 16
    self.opacityFirstPoint  = TimeInterval(arc4random_uniform(UInt32(self.duration - 3)) + 1) // 2 - 12
    self.opacitySecondPoint = TimeInterval(arc4random_uniform(UInt32(self.duration - self.opacityFirstPoint - 2))) + self.opacityFirstPoint + 1 // 4 - 14
  }

  // Y軸方向の移動に関する進捗を取得
  func getProgressY(elapsed: TimeInterval) -> CGFloat {
    return Easing.easeIn.sine.getProgress(elapsed: elapsed, duration: self.duration, startValue: self.startY, endValue: self.endY)
  }

  // 回転に関する進捗を取得
  func getProgressAngle(elapsed: TimeInterval) -> CGFloat {
    return Easing.easeIn.sine.getProgress(elapsed: elapsed, duration: self.duration, startValue: 0.0, endValue: self.angle)
  }

  // 透明度に関する進捗を取得
  func getProgressOpacity(elapsed: TimeInterval) -> CGFloat {
    if elapsed < self.opacityFirstPoint {
      return Easing.easeInOut.sine.getProgress(elapsed: elapsed, duration: self.opacityFirstPoint, startValue: 0.0, endValue: 1.0)
    }
    else if self.opacitySecondPoint <= elapsed && elapsed <= self.opacitySecondPoint {
      return 1.0
    }
    else {
      return Easing.easeInOut.sine.getProgress(elapsed: elapsed - self.opacitySecondPoint, duration: self.duration - self.opacitySecondPoint, startValue: 1.0, endValue: 0.0)
    }
  }
}
```

ほんで、ViewControllerの全貌。

```swift
final class SnowViewController: UIViewController {

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.createLoop()
    self.createSnows()
  }

  @IBAction private func didTapStartButton(_ sender: UIButton) {
    self.startLoop()
  }

  // ループ作成
  private func createLoop() {
    self.loop = CADisplayLink(target: self, selector: #selector(self.update))
    self.loop?.preferredFramesPerSecond = 60
    self.loop?.add(to: .current, forMode: .common)
    self.loop?.isPaused = true
  }

  // ループ稼働
  private func startLoop() {
    self.startTime      = Date.timeIntervalSinceReferenceDate
    self.loop?.isPaused = false
  }

  // 雪いっぱい作る
  private func createSnows() {
    for _ in 0..<80 {
      let snow = Snow()
      self.snows.append(snow)
      self.view.addSubview(snow.imageView)
    }
  }

  // 描画タイミングごとに呼ばれる
  @objc
  private func update() {
    let currentTime = Date.timeIntervalSinceReferenceDate
    self.snows.forEach { self.updateSnow($0, currentTime: currentTime) }
  }

  // 雪の進捗を取得し、反映
  private func updateSnow(_ snow: Snow, currentTime: TimeInterval) {
    let elapsed = currentTime - self.startTime
    let y       = snow.getProgressY(elapsed: elapsed)
    let angle   = snow.getProgressAngle(elapsed: elapsed)
    let opacity = snow.getProgressOpacity(elapsed: elapsed)

    snow.imageView.transform = CGAffineTransform.identity.translatedBy(x: snow.x, y: y).rotated(by: angle).scaledBy(x: snow.scale, y: snow.scale)
    snow.imageView.alpha = opacity
  }
}
```

コード的にはこんな感じなんですよ、ええ。

描画タイミングごとに呼び出すためにはこうする・・・的な話は、
またしても以前に書いたので、そちらで。
https://qiita.com/haguhoms/items/c87d335756042bc867c4

で、ここでは、実際にそのタイミングでの進捗を計算して反映する部分の説明。

まずはここ。

```swift
// ループ稼働
private func startLoop() {
  self.startTime      = Date.timeIntervalSinceReferenceDate
  self.loop?.isPaused = false
}
```

ここで、アニメーション開始時の時間を取得する。
開始時間から○秒経ったらここにいるはずだよな、っていう計算をするわけなので、これは必須。
で、isPausedをfalseにすることで、止めてたループを開始させる。

すると、

```swift
// 描画タイミングごとに呼ばれる
@objc
private func update() {
  let currentTime = Date.timeIntervalSinceReferenceDate
  self.snows.forEach { self.updateSnow($0, currentTime: currentTime) }
}
```
こいつが、ぽんぽん呼ばれるわけですね、ええ。
で、呼ばれた時の時間を取得して、
**この時間を元に進捗を計算して反映させちゃってー**
って、updateSnowに全てを託す。

ほんで、

```swift
// 雪の進捗を取得し、反映
private func updateSnow(_ snow: Snow, currentTime: TimeInterval) {
  let elapsed = currentTime - self.startTime
  let y       = snow.getProgressY(elapsed: elapsed)
  let angle   = snow.getProgressAngle(elapsed: elapsed)
  let opacity = snow.getProgressOpacity(elapsed: elapsed)

  snow.imageView.transform = CGAffineTransform.identity.translatedBy(x: snow.x, y: y).rotated(by: angle).scaledBy(x: snow.scale, y: snow.scale)
  snow.imageView.alpha     = opacity
}
```
こいつがとうとう呼ばれる。
現在時間を元に進捗を計算する役割はstructに持たせたので、ここではそれぞれ取得した値をアニメーション対象物に反映させるだけ。
もっというと、

```swift
// Y軸方向の移動に関する進捗を取得
func getProgressY(elapsed: TimeInterval) -> CGFloat {
  return Easing.easeIn.sine.getProgress(elapsed: elapsed, duration: self.duration, startValue: self.startY, endValue: self.endY)
}
```
これも、Easingのメソッドを呼んでいるだけ。
中身は、以下のような。

```swift
func getProgress(elapsed: TimeInterval, duration: TimeInterval, startValue: CGFloat, endValue: CGFloat) -> CGFloat {
  if elapsed < 0 {
    return startValue
  }
  if elapsed > duration {
    return endValue
  }

  var progress: CGFloat
  switch self {
  case .sine:
    let position: TimeInterval = elapsed / duration
    progress = CGFloat( -cos(position * .pi / 2) + 1.0 )
  }

  if startValue > endValue {
    return startValue - abs(endValue - startValue) * progress
  }
  else {
    return startValue + abs(endValue - startValue) * progress
  }
}
```
一つ目の手法で、
**始まり、終わり、秒数、動き方を指定したら、それに従ってよしなに動かせてくれるやーつ**
ってなってた、そのよしなにの部分を自力で計算している。
さらに、今回はやっていないけど、こっちの手法やと毎タイミングで少しランダムに横にずらす、とかで、より雪に近づけることができたり。
かなり自由度が高く実装することができるので、いろいろやりたいときはこちらの手法がいいかなと。

## おわりに
二つの手法で雪を降らせて、かなり **ゆきやこんこ** な気分になったかと思います。

用法用量を守って適材適所に、みたいな話を近日noteとかで書こうかなーと思うので、
twitterやnoteもチェックしてくれればと思います(九分九厘エンジニアっぽくないつぶやきなのでご了承を)
基本 **「haguhoms」** で検索いただければ。

これからもアニメーションとの距離を近づけて、ゆくゆくはゼロ距離になれたらなと思います！
snow flakeな冬の贈り物をお届けに参ったはぐっでした・ω・♪