はじめに
RunCatのキーフレームアニメーションのコアな部分の実装について公開.
速度の変わるキーフレームアニメーション(いわゆるパラパラ漫画とかコマ撮りアニメ)の実装方法の文献がなく,非常に厄介だった.
デモ
このデモのやつは普通にNSView
のlayer
上でアニメーションしているので簡単だが,メニューバー(NSStatusItem
)上で再生するとなると実際にはもっと手の込んだことが必要になる.
ソース
duration
を固定して,親レイヤーのspeed
を調整するというのがキモ
あとはtimeOffset
とbeginTime
の役割把握が重要
AnimationLayer.swift
import Cocoa
class AnimationLayer: CALayer {
//キーフレームアニメーションをするやつ
private var keyFrameAnimation: CAKeyframeAnimation!
//何かしらの初期設定(実装によってはいらない)
public func initialize() {
self.masksToBounds = true
self.contentsScale = 2.0
}
//キーフレームアニメーションの用意
public func setSequence(_ sequence: [NSImage]) {
keyFrameAnimation = CAKeyframeAnimation(keyPath: "contents")
keyFrameAnimation.calculationMode = .discrete //パラパラ漫画形式にするために必須な設定
keyFrameAnimation.fillMode = .forwards
keyFrameAnimation.repeatCount = Float.infinity
keyFrameAnimation.autoreverses = false
keyFrameAnimation.isRemovedOnCompletion = false
keyFrameAnimation.beginTime = 0.0
keyFrameAnimation.values = sequence
keyFrameAnimation.duration = Double(sequence.count)
}
//アニメーション開始
public func startAnimate() {
if keyFrameAnimation == nil { return }
CATransaction.begin()
CATransaction.setDisableActions(true)
self.add(keyFrameAnimation, forKey: "running")
CATransaction.commit()
}
//アニメーションのスピード変更
public func updateSpeed(_ speed: Float) {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.timeOffset = self.convertTime(CACurrentMediaTime(), from: nil)
self.beginTime = CACurrentMediaTime()
self.speed = speed
CATransaction.commit()
}
}
ViewController.swift
import Cocoa
class ViewController: NSViewController {
let animationLayer = AnimationLayer()
var timer: Timer? = nil
override func viewDidLoad() {
super.viewDidLoad()
//アニメーションレイヤーを初期化して追加
self.view.wantsLayer = true
animationLayer.initialize()
self.view.layer!.addSublayer(animationLayer)
//アニメーションするためのコマ画像を用意する
var icons = [NSImage]()
for i in (1 ... 4) {
let icon = NSImage(imageLiteralResourceName: "page" + String(i))
icons.append(icon)
}
//フレームサイズと画像を設定してアニメーション開始
animationLayer.frame = NSRect(x: 40, y: 40, width: self.view.bounds.width - 80, height: self.view.bounds.height - 80)
animationLayer.setSequence(icons)
animationLayer.startAnimate()
//例として定期的にアニメーションをランダムなスピードに変更
timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { (t) in
let speed = Float.random(in: 1 ... 5)
self.animationLayer.updateSpeed(speed)
}
}
override func viewWillDisappear() {
timer?.invalidate()
}
override var representedObject: Any? {
didSet {
}
}
}
備考
一応この方法をベースとしてキーフレームアニメーションを行うことが可能なのだが,Core Animation
は勝手にGPUを使える場合は使ってしまうらしく,発熱問題をユーザーから指摘された.GPUを使わずにCPUの消耗が小さい実装方法が知りたい.