120 lines
3 KiB
Swift
120 lines
3 KiB
Swift
|
|
// AnimationPlayer.swift
|
||
|
|
// CADisplayLink-driven playback engine matching Blender's transport controls.
|
||
|
|
|
||
|
|
import UIKit
|
||
|
|
import Observation
|
||
|
|
|
||
|
|
@Observable
|
||
|
|
final class AnimationPlayer {
|
||
|
|
var animationSystem: AnimationSystem
|
||
|
|
|
||
|
|
private var displayLink: CADisplayLink?
|
||
|
|
private var lastTimestamp: CFTimeInterval = 0
|
||
|
|
|
||
|
|
init(system: AnimationSystem) {
|
||
|
|
self.animationSystem = system
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Transport controls (mirror Blender's timeline buttons)
|
||
|
|
|
||
|
|
func play() {
|
||
|
|
animationSystem.isPlaying = true
|
||
|
|
startDisplayLink()
|
||
|
|
}
|
||
|
|
|
||
|
|
func pause() {
|
||
|
|
animationSystem.isPlaying = false
|
||
|
|
stopDisplayLink()
|
||
|
|
}
|
||
|
|
|
||
|
|
func stop() {
|
||
|
|
animationSystem.isPlaying = false
|
||
|
|
animationSystem.currentFrame = animationSystem.frameStart
|
||
|
|
stopDisplayLink()
|
||
|
|
}
|
||
|
|
|
||
|
|
func togglePlayback() {
|
||
|
|
if animationSystem.isPlaying {
|
||
|
|
pause()
|
||
|
|
} else {
|
||
|
|
play()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func jumpToStart() {
|
||
|
|
animationSystem.currentFrame = animationSystem.frameStart
|
||
|
|
}
|
||
|
|
|
||
|
|
func jumpToEnd() {
|
||
|
|
animationSystem.currentFrame = animationSystem.frameEnd
|
||
|
|
}
|
||
|
|
|
||
|
|
func stepForward() {
|
||
|
|
animationSystem.currentFrame = min(
|
||
|
|
animationSystem.currentFrame + 1,
|
||
|
|
animationSystem.frameEnd
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func stepBackward() {
|
||
|
|
animationSystem.currentFrame = max(
|
||
|
|
animationSystem.currentFrame - 1,
|
||
|
|
animationSystem.frameStart
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func nextKeyframe() {
|
||
|
|
guard let action = animationSystem.activeAction else { return }
|
||
|
|
let allFrames = action.tracks
|
||
|
|
.flatMap { $0.sortedKeyframes }
|
||
|
|
.map(\.frame)
|
||
|
|
.sorted()
|
||
|
|
if let next = allFrames.first(where: { $0 > animationSystem.currentFrame + 0.5 }) {
|
||
|
|
animationSystem.currentFrame = next
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func previousKeyframe() {
|
||
|
|
guard let action = animationSystem.activeAction else { return }
|
||
|
|
let allFrames = action.tracks
|
||
|
|
.flatMap { $0.sortedKeyframes }
|
||
|
|
.map(\.frame)
|
||
|
|
.sorted()
|
||
|
|
if let prev = allFrames.last(where: { $0 < animationSystem.currentFrame - 0.5 }) {
|
||
|
|
animationSystem.currentFrame = prev
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Display link
|
||
|
|
|
||
|
|
private func startDisplayLink() {
|
||
|
|
guard displayLink == nil else { return }
|
||
|
|
lastTimestamp = 0
|
||
|
|
let link = CADisplayLink(target: self, selector: #selector(tick))
|
||
|
|
link.preferredFrameRateRange = CAFrameRateRange(
|
||
|
|
minimum: 30, maximum: 120, preferred: 60
|
||
|
|
)
|
||
|
|
link.add(to: .main, forMode: .common)
|
||
|
|
displayLink = link
|
||
|
|
}
|
||
|
|
|
||
|
|
private func stopDisplayLink() {
|
||
|
|
displayLink?.invalidate()
|
||
|
|
displayLink = nil
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func tick(_ link: CADisplayLink) {
|
||
|
|
if lastTimestamp == 0 {
|
||
|
|
lastTimestamp = link.timestamp
|
||
|
|
return
|
||
|
|
}
|
||
|
|
let dt = Float(link.timestamp - lastTimestamp)
|
||
|
|
lastTimestamp = link.timestamp
|
||
|
|
animationSystem.tick(dt: dt)
|
||
|
|
}
|
||
|
|
|
||
|
|
deinit {
|
||
|
|
stopDisplayLink()
|
||
|
|
}
|
||
|
|
}
|