学习Canvas动画时间线技术,掌握复杂动画序列的管理和编排方法。动画时间线用于管理复杂的动画序列,协调多个动画的播放顺序和时序。
动画时间线是一个管理多个动画的工具,可以控制动画的播放顺序、同步和组合。
| 功能 | 说明 |
|---|---|
| 序列控制 | 按顺序播放动画 |
| 并行控制 | 同时播放多个动画 |
| 时间控制 | 暂停、继续、跳转 |
| 事件触发 | 在特定时间触发回调 |
class Timeline {
constructor() {
this.tracks = []
this.currentTime = 0
this.duration = 0
this.playing = false
this.speed = 1
}
add(track) {
this.tracks.push(track)
this.duration = Math.max(this.duration, track.endTime)
return this
}
play() {
this.playing = true
this.lastTime = performance.now()
this.tick()
return this
}
pause() {
this.playing = false
return this
}
seek(time) {
this.currentTime = Math.max(0, Math.min(time, this.duration))
this.updateTracks()
return this
}
tick() {
if (!this.playing) return
const now = performance.now()
const delta = (now - this.lastTime) * this.speed
this.lastTime = now
this.currentTime += delta
if (this.currentTime >= this.duration) {
this.currentTime = 0
}
this.updateTracks()
requestAnimationFrame(() => this.tick())
}
updateTracks() {
this.tracks.forEach(track => {
track.update(this.currentTime)
})
}
}
class AnimationTrack {
constructor(options = {}) {
this.target = options.target
this.property = options.property
this.keyframes = []
this.startTime = options.startTime || 0
this.endTime = 0
}
addKeyframe(time, value, easing = 'linear') {
this.keyframes.push({ time, value, easing })
this.keyframes.sort((a, b) => a.time - b.time)
this.endTime = Math.max(this.endTime, time + this.startTime)
return this
}
update(currentTime) {
const localTime = currentTime - this.startTime
if (localTime < 0 || localTime > this.endTime - this.startTime) return
let prev = this.keyframes[0]
let next = this.keyframes[0]
for (let i = 0; i < this.keyframes.length - 1; i++) {
if (localTime >= this.keyframes[i].time && localTime <= this.keyframes[i + 1].time) {
prev = this.keyframes[i]
next = this.keyframes[i + 1]
break
}
}
if (prev === next) {
this.target[this.property] = prev.value
return
}
const progress = (localTime - prev.time) / (next.time - prev.time)
const easedProgress = this.applyEasing(progress, prev.easing)
this.target[this.property] = this.lerp(prev.value, next.value, easedProgress)
}
lerp(a, b, t) {
return a + (b - a) * t
}
applyEasing(t, easing) {
switch (easing) {
case 'easeIn': return t * t
case 'easeOut': return t * (2 - t)
case 'easeInOut': return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
default: return t
}
}
}
class KeyframeAnimation {
constructor(options = {}) {
this.target = options.target
this.keyframes = options.keyframes || []
this.duration = options.duration || 1000
this.loop = options.loop || false
this.easing = options.easing || (t => t)
this.onUpdate = options.onUpdate
this.onComplete = options.onComplete
this.currentTime = 0
this.playing = false
}
play() {
this.playing = true
this.startTime = performance.now()
this.tick()
return this
}
pause() {
this.playing = false
return this
}
tick() {
if (!this.playing) return
const elapsed = performance.now() - this.startTime
this.currentTime = elapsed % this.duration
const progress = this.currentTime / this.duration
const easedProgress = this.easing(progress)
this.applyKeyframes(easedProgress)
if (this.onUpdate) {
this.onUpdate(this.target, progress)
}
if (!this.loop && elapsed >= this.duration) {
this.playing = false
if (this.onComplete) {
this.onComplete()
}
return
}
requestAnimationFrame(() => this.tick())
}
applyKeyframes(progress) {
if (this.keyframes.length === 0) return
let prev = this.keyframes[0]
let next = this.keyframes[this.keyframes.length - 1]
for (let i = 0; i < this.keyframes.length - 1; i++) {
if (progress >= this.keyframes[i].time && progress <= this.keyframes[i + 1].time) {
prev = this.keyframes[i]
next = this.keyframes[i + 1]
break
}
}
const localProgress = (progress - prev.time) / (next.time - prev.time)
Object.keys(prev.properties).forEach(prop => {
const from = prev.properties[prop]
const to = next.properties[prop]
this.target[prop] = from + (to - from) * localProgress
})
}
}
const ball = { x: 50, y: 100, radius: 20 }
new KeyframeAnimation({
target: ball,
keyframes: [
{ time: 0, properties: { x: 50, radius: 20 } },
{ time: 0.5, properties: { x: 350, radius: 30 } },
{ time: 1, properties: { x: 50, radius: 20 } }
],
duration: 2000,
loop: true
}).play()
class AnimationSequence {
constructor() {
this.animations = []
this.currentIndex = 0
this.playing = false
}
add(animation) {
this.animations.push(animation)
return this
}
play() {
this.playing = true
this.currentIndex = 0
this.playNext()
return this
}
playNext() {
if (!this.playing || this.currentIndex >= this.animations.length) {
this.playing = false
return
}
const animation = this.animations[this.currentIndex]
animation.onComplete = () => {
this.currentIndex++
this.playNext()
}
animation.play()
}
pause() {
this.playing = false
if (this.animations[this.currentIndex]) {
this.animations[this.currentIndex].pause()
}
return this
}
stop() {
this.playing = false
this.currentIndex = 0
return this
}
}
class ParallelAnimation {
constructor() {
this.animations = []
this.playing = false
this.completed = 0
}
add(animation) {
this.animations.push(animation)
return this
}
play() {
this.playing = true
this.completed = 0
this.animations.forEach(animation => {
const originalOnComplete = animation.onComplete
animation.onComplete = () => {
this.completed++
if (originalOnComplete) originalOnComplete()
if (this.completed === this.animations.length) {
this.playing = false
}
}
animation.play()
})
return this
}
pause() {
this.playing = false
this.animations.forEach(a => a.pause())
return this
}
}
class AnimationGroup {
constructor(options = {}) {
this.animations = []
this.startTime = options.startTime || 0
this.duration = options.duration || 0
this.loop = options.loop || false
}
add(animation, delay = 0) {
animation.delay = delay
this.animations.push(animation)
this.duration = Math.max(this.duration, delay + animation.duration)
return this
}
update(currentTime) {
const localTime = currentTime - this.startTime
this.animations.forEach(animation => {
const animTime = localTime - animation.delay
if (animTime >= 0 && animTime < animation.duration) {
animation.update(animTime)
}
})
}
}
class TimelineController {
constructor() {
this.timeline = new Timeline()
this.ui = null
}
createUI(container) {
const wrapper = document.createElement('div')
wrapper.className = 'timeline-ui'
const controls = document.createElement('div')
controls.className = 'timeline-controls'
const playBtn = document.createElement('button')
playBtn.textContent = '播放'
playBtn.onclick = () => this.togglePlay()
const progress = document.createElement('input')
progress.type = 'range'
progress.min = 0
progress.max = 100
progress.value = 0
progress.oninput = (e) => {
const time = (e.target.value / 100) * this.timeline.duration
this.timeline.seek(time)
}
controls.appendChild(playBtn)
controls.appendChild(progress)
wrapper.appendChild(controls)
container.appendChild(wrapper)
this.ui = { playBtn, progress }
return wrapper
}
togglePlay() {
if (this.timeline.playing) {
this.timeline.pause()
this.ui.playBtn.textContent = '播放'
} else {
this.timeline.play()
this.ui.playBtn.textContent = '暂停'
}
}
updateUI() {
if (this.ui && this.timeline.duration > 0) {
const progress = (this.timeline.currentTime / this.timeline.duration) * 100
this.ui.progress.value = progress
}
}
}
class EasingTimeline {
constructor() {
this.segments = []
this.currentTime = 0
this.duration = 0
}
addSegment(duration, easing, update) {
const startTime = this.duration
this.segments.push({
startTime,
duration,
endTime: startTime + duration,
easing,
update
})
this.duration += duration
return this
}
update(dt) {
this.currentTime += dt
if (this.currentTime >= this.duration) {
this.currentTime = this.currentTime % this.duration
}
this.segments.forEach(segment => {
if (this.currentTime >= segment.startTime && this.currentTime < segment.endTime) {
const localTime = this.currentTime - segment.startTime
const progress = localTime / segment.duration
const easedProgress = segment.easing(progress)
segment.update(easedProgress, localTime)
}
})
}
}
const timeline = new EasingTimeline()
.addSegment(1000, t => t * t, (p) => {
console.log('第一阶段:', p)
})
.addSegment(500, t => t * (2 - t), (p) => {
console.log('第二阶段:', p)
})
.addSegment(800, t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, (p) => {
console.log('第三阶段:', p)
})
class EventTimeline extends Timeline {
constructor() {
super()
this.events = []
this.triggeredEvents = new Set()
}
addEvent(time, callback) {
this.events.push({ time, callback, id: Math.random() })
this.events.sort((a, b) => a.time - b.time)
return this
}
updateTracks() {
super.updateTracks()
this.checkEvents()
}
checkEvents() {
this.events.forEach(event => {
if (this.currentTime >= event.time && !this.triggeredEvents.has(event.id)) {
event.callback()
this.triggeredEvents.add(event.id)
}
})
}
seek(time) {
super.seek(time)
this.triggeredEvents.clear()
}
}
class AnimationStateMachine {
constructor() {
this.states = new Map()
this.currentState = null
this.transitions = []
}
addState(name, animation) {
this.states.set(name, animation)
return this
}
addTransition(from, to, condition) {
this.transitions.push({ from, to, condition })
return this
}
setState(name) {
if (this.currentState) {
const currentAnim = this.states.get(this.currentState)
if (currentAnim && currentAnim.pause) {
currentAnim.pause()
}
}
this.currentState = name
const animation = this.states.get(name)
if (animation && animation.play) {
animation.play()
}
return this
}
update() {
if (!this.currentState) return
this.transitions.forEach(transition => {
if (transition.from === this.currentState && transition.condition()) {
this.setState(transition.to)
}
})
}
}
const fsm = new AnimationStateMachine()
.addState('idle', idleAnimation)
.addState('walk', walkAnimation)
.addState('run', runAnimation)
.addTransition('idle', 'walk', () => isMoving)
.addTransition('walk', 'run', () => isRunning)
.addTransition('run', 'walk', () => !isRunning)
.addTransition('walk', 'idle', () => !isMoving)
fsm.setState('idle')