machio Development Diary

思いつきで技術的なことをつらつらと

イルミネーションもいいけど僕の『正弦関数 in Swift』で作ったハート群もめっちゃ綺麗だから見て見て

この記事は Hakusan mafia Advent Calendar 2017 の24日目の記事です。どの辺がマフィアなのかはよくわかってません。。。

はじめに

クリスマスイブですね(記事出した時はそうなんです)。街には笑顔が溢れて、活気に満ちていることと思います。カップルで幸せな時間を共にする人、家族で温かい食卓を囲む人、孤独を噛みしめる人など、色々な過ごし方の人がいると思います。

中には クリスマスなのにSwiftでハートがセクシーに動くAnimationが作れなくて困ってる人 もいるでしょう。今回はそんな人たちのための記事です。

Swiftでは基本的なAnimationは数行で実装できるようになっている(本当に素敵)のですが、今回は標準では積まれていないゆるやかな往復運動正弦関数を用いて実装したいと思います。

https://gyazo.com/fe84349e823b9a2ab50167daac931885

これから触れるAnimationは実際に僕の会社のサービス PinQul でも使われているものの一部で結構我ながら気に入ってます。

今回はこのハートの動きの部分が純Swiftでどうやって動かされているのかを見ていきたいと思います。

方針

このハートのAnimationは3つの小さなAnimationに分解できます。

  1. だんだんopacityが小さくなるAnimation
  2. y方向に一定速度で上昇していくAnimation
  3. x方向に緩やかに振動するAnimation

です。1と2に関してはCABasicAnimationというswift組み込みのAnimationで簡単に実装できるので、今回は3をメインに触れていきたいと思いますが、一応Animationの基礎についても触れていきます。

CAAnimationの基礎

僕の記事なんて読まなくても、素晴らしい記事がQiitaにたくさん落ちています。

qiita.com

その中でも今回必要なものだけを抜粋しました。

CABasicAnimation

SwiftでUIViewの属性の1つをAnimationで変化させたいと思った時、大概の場合は

  • 何を変化させるか(keyPath
  • 初期値(fromValue
  • 終了値(toValue
  • 期間(duration

を指定するだけで簡単に実装できます。これを可能にしているのが CABasicAnimation です。

例)

//
// 透明からだんだんViewが現れるAnimation
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300
verticalAnimation.duration = 3.0
heartView.layer.add(verticalAnimation, forKey: nil)

https://gyazo.com/8200a6b47e063a18dd059bb725e07d97

※CABasicAnimationを用いてAnimationで変化させられる要素(keyPath)は以下を参照してください。

developer.apple.com

これでも十分綺麗ですが、まだちょっと物足りないですね。

CAAnimationGroup

実はCAAnimationはいくつでも組み合わせることができます。CAAnimationGroupというクラスを使っていきます。

CAAnimationGroupのインスタンスがもつ animations プロパティにCAAnimationの配列を渡すことで、それらのAnimationが1つに集められた1つのAnimationを作成することができます。

例)

let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.0
        
let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300

let animationGroup = CAAnimationGroup()
animationGroup.duration = 3.0
animationGroup.animations = [opacityAnimation, verticalAnimation]
heartView.layer.add(animationGroup, forKey: nil)

https://gyazo.com/a4dbe9ca175efcc04483ddd394397b67

透明度を追加したことで儚げな感じがプラスされましたね。人の心は移ろいやすいものです

もちろん配列に追加していけば3つでも4つでもAnimationを組み合わせることができます。

本題の正弦関数Animationについて

Swiftでの正弦関数

Swiftの標準ライブラリである Foundation は豊富に数学関数を含んでいます。

三角関数関連だけでも以下のラインナップです。完璧に揃ってます。

let x = CGFloat(1.0) //単位はラジアン

// 通常のサイン・コサイン・タンジェント
sin(x)
cos(x)
tan(x)

// アークサイン・コサイン・タンジェント
asin(x)
acos(x)
atan(x)

// ハイパボリックサイン・コサイン・タンジェント
sinh(x)
cosh(x)
tanh(x)

// アークハイパボリックサイン・コサイン・タンジェント
asinh(x)
acosh(x)
atanh(x)

今回はその中でも正弦関数(sin)をつかって滑らかなAnimationを実装していきます。

CAKeyframeAnimation

CAKeyframeAnimation は経過時間の配列とそれに対応する値の配列を与えることで、その途中経過をよしなに補完したAnimationを作成してくれます。

例) 公式Documentのサンプル

let colorAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorAnimation.values = [UIColor.red.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor]
colorAnimation.keyTimes = [0, 0.5, 1]
colorAnimation.duration = 2

値や経過時間の差分に変化をもたせることで、CABasicAnimationより自由度の高いAnimationを作成することができます。

今回は正弦関数を使って position.x の値を変則的に動かすことでゆるやかなカーブを描かせていきます。

ゆるやかな往復運動の実装

あらかじめAnimationの duration(時間の長さ) と frameCount(コマ数) を指定しておきます。

そして horizontalAnimation という名前で宣言しておきます。

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.duration = duration

keyTimes(値を指定する先の経過時間)を設定する

今回 keyTimes に関しては均等に与えたいので duration をframeCountで均等に割った経過時間を指定します。

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

// let duration = 3.0
// let frameCount = 30
// let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
// horizontalAnimation.duration = duration

horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

values(keyTimesに対応する値の組み合わせ)を設定する

ここで正弦関数を用いて動きに遊びをもたせます

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

// let duration = 3.0
// let frameCount = 30
// let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
// horizontalAnimation.duration = duration

// horizontalAnimation.keyTimes = (0...frameCount).map({
//    let dividedTime = Double($0) * duration / Double(frameCount)
//    return NSNumber(value: Double(dividedTime))
// })

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

完成形

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.duration = duration

horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

heartView.layer.add(horizontalAnimation, forKey: nil)

https://gyazo.com/d3f70babd6d6e27fad5bf7049a34b47b

いい感じでゆらゆらしてます!

完成!

今までの2つの Animation と組み合わせて再生してみましょう。

let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.0
        
let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

let animationGroup = CAAnimationGroup()
animationGroup.duration = duration
animationGroup.animations = [opacityAnimation, verticalAnimation, horizontalAnimation]
heartView.layer.add(animationGroup, forKey: nil)

https://gyazo.com/07a1abac976314c59d1092b4ab451003

素敵なAnimationが完成しました!!

まとめ

記事書いてて思ったけど "hyperbolic" ってカタカナで ハイパボリック って書いた瞬間急にダサいよね