Swiftで漸近アニメーション


概要

「漸近って、数学のどっかで習ったなー」って感じのイメージだと思います。
ただそれは多分漸近線で、今回の内容とは似て非なるものだったりするので一旦全て忘れてください。

漸近は、だんだんと近づく的な意味です。
読み方は「ぜんきん」です。

これだけ聞いても、なんとなくアニメーションしてそうじゃないですか?

これが漸近アニメーションかー、ってなってもらえるように、コードと成果物で説明していきます。

成果物

今回は作り込んだアニメーションではなく、あくまでこういう風に書くとこういうことが実現できるよっていうことを伝えたいので、めちゃめちゃシンプルな成果物になっています。

ちょっとわかりにくいけど、どういうものかというと

  • 上へ向かう速度は一定
  • 目的地を決めて、円のx座標を目的地へだんだん近づける
  • 目的地に着いたら、新たな目的地を決める

って感じ。
細かい話はこれから説明していきます。

実装

まずは、目的地用のstruct

struct AsymptoticDestination {
    var x     : CGFloat = 0.0 // 目的地のx座標
    var time  : Int = 0 // 目的地まで何フレームで移動するか 
    var count : Int = 0 // 移動しだして何フレーム目か

    // 次の目的地を決める
    mutating func calculateDestination() {
        self.x = CGFloat(arc4random_uniform(UInt32(UIScreen.main.bounds.width)))
        self.time = Int(arc4random_uniform(90)) + 10
        self.count = 0
    }

    // countを毎フレーム増やしていき、timeに到達したら次の目的地へ
    mutating func countUp() {
        self.count += 1
        if self.count == self.time {
            self.calculateDestination()
        }
    }
}

ループを回して状態管理するので、ループ部分
と、円レイヤー周り

final class AsymptoticView: BaseView {

    private var displayLink: CADisplayLink?
    private var destination: AsymptoticDestination = .init()
    private var currentPoint: CGPoint = .zero
    private var circle = CALayer()
}

// MARK: - loop
extension AsymptoticView {

    // ループ設定
    func setupLoop() {
        self.displayLink = CADisplayLink(target: self, selector: #selector(self.update))
        if #available(iOS 10.0, *) {
            self.displayLink?.preferredFramesPerSecond = 60
        } else {
            self.displayLink?.frameInterval = 1
        }
        self.displayLink?.add(to: .current, forMode: .common)
        self.displayLink?.isPaused = true // 設定時は止めておく
    }

    // ループ開始
    func startTick() {
        if self.displayLink == nil {
            self.setupLoop()
        }
        self.displayLink?.isPaused = false
    }

    // ループ停止
    func stopTick() {
        self.displayLink?.isPaused = true
        self.displayLink?.invalidate()
    }

    // 毎フレーム呼び出される
    @objc
    private func update(displayLink: CADisplayLink) {
        self.updateCircle()
    }
}

// MARK: - circle
extension AsymptoticView {

    func setupCircle() {
        // 初期位置決め
        self.setupCurrentPoint()

        // 円を描く
        self.circle = CAShapeLayer()
        self.circle.bounds = CGRect(x: 0, y: 0, width: 44.0, height: 44.0)
        self.circle.position = self.currentPoint
        self.circle.backgroundColor = UIColor.red.cgColor
        self.circle.cornerRadius = 22.0
        self.layer.addSublayer(self.circle)
    }

    private func setupCurrentPoint() {
        self.currentPoint = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height)
        self.destination.calculateDestination()
    }

    private func updateCircle() {
        self.currentPoint.x += (self.destination.x - self.currentPoint.x) / CGFloat(self.destination.time) // *1
        self.currentPoint.y -= 1.0 // y座標方向のスピード
        self.circle.position = self.currentPoint

        self.destination.countUp()
    }
}

*1と書いた部分が、今回のポイントです。
漸近の「だんだん近づける」という部分を実装したところですね!

今の位置と目的地のx座標の差分を算出し、
その差分を何フレームで埋めるかっていう値で割ると、
1フレーム当たりの移動量が計算できます。
それが*1です。

フレームごとに、目的地へだんだん近づけることで、こういうアニメーションが完成するわけですね。

まとめ

アニメーションってどうやって動いてるんやろう、ってところが紐解いていけると、自分が実装できる幅も広がるので、漸近の知識は様々な表現に役立ちます!
何回漸近って言うねんって思われたはずなので、この辺でおさらば!