-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathAnimationRunner.ts
More file actions
237 lines (201 loc) · 5.98 KB
/
AnimationRunner.ts
File metadata and controls
237 lines (201 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
/**
* 动画状态的泛型。
*/
type TState = any
/**
* 调度器需要调用的回调函数类型。
*/
type TickCallback = () => void
/**
* 调度器接口。
* 它的职责是根据自己的策略(RAF, setTimeout, 手动等)
* 来重复调用一个 tick 回调函数。
*/
interface IScheduler {
/**
* 启动调度器。
* @param tick - 每一帧需要执行的回调函数。
*/
start(tick: TickCallback): void
/**
* 停止调度器。
*/
stop(): void
}
class RequestAnimationFrameScheduler implements IScheduler {
private animationFrameId: number | null = null
public start(tick: TickCallback): void {
const loop = () => {
tick()
this.animationFrameId = requestAnimationFrame(loop)
}
// 立即开始第一次循环
this.animationFrameId = requestAnimationFrame(loop)
}
public stop(): void {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
}
}
/** 固定帧率. */
class SetTimeoutScheduler implements IScheduler {
private timeoutId: number | null = null
private readonly fps: number
constructor(fps: number = 60) {
this.fps = fps
}
public start(tick: TickCallback): void {
const interval = 1000 / this.fps
const loop = () => {
tick()
this.timeoutId = window.setTimeout(loop, interval)
}
// 立即开始第一次循环
this.timeoutId = window.setTimeout(loop, interval)
}
public stop(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
this.timeoutId = null
}
}
}
/**
* 代表一个可动画化对象的接口。
* 它封装了状态以及更新和渲染该状态的逻辑。
*/
interface IAnimatable<S extends TState> {
state: S
update(deltaTime: number): void
render(): void
}
/**
* 定义了单帧执行策略的接口。
*/
interface IFrameRunner<S extends TState> {
/**
* 执行一帧的逻辑。
* @param animatable - 需要执行动画的对象。
* @param deltaTime - 时间增量。
*/
runFrame(animatable: IAnimatable<S>, deltaTime: number): void
}
class SimpleFrameRunner<S extends TState> implements IFrameRunner<S> {
public runFrame(animatable: IAnimatable<S>, deltaTime: number): void {
animatable.update(deltaTime)
animatable.render()
}
}
class FixedTimestepRunner<S extends TState> implements IFrameRunner<S> {
private accumulator: number = 0
private readonly fixedDeltaTime: number
constructor(updatesPerSecond: number = 60) {
this.fixedDeltaTime = 1000 / updatesPerSecond
}
public runFrame(animatable: IAnimatable<S>, deltaTime: number): void {
// 防止因浏览器标签页切换等原因导致 deltaTime 过大
if (deltaTime > 250) {
deltaTime = 250
}
this.accumulator += deltaTime
while (this.accumulator >= this.fixedDeltaTime) {
animatable.update(this.fixedDeltaTime)
this.accumulator -= this.fixedDeltaTime
}
animatable.render()
}
}
/**
* `AnimationRunner` 彻底变成了一个通用的“协调器”,它对动画的具体内容、执行时机、执行策略一无所知.
*/
export class AnimationRunner<S extends TState> {
private animatable: IAnimatable<S>
private scheduler: IScheduler
private frameRunner: IFrameRunner<S>
private lastTime: number = 0
private isRunning: boolean = false
constructor(
animatable: IAnimatable<S>,
scheduler: IScheduler = new RequestAnimationFrameScheduler(),
frameRunner: IFrameRunner<S> = new SimpleFrameRunner<S>()
) {
this.animatable = animatable
this.scheduler = scheduler
this.frameRunner = frameRunner
}
public start(): void {
if (this.isRunning) return
this.isRunning = true
this.lastTime = performance.now()
this.scheduler.start(this.tick)
console.log('Animation started.')
}
public stop(): void {
if (!this.isRunning) return
this.isRunning = false
this.scheduler.stop()
console.log('Animation stopped.')
}
private tick = (): void => {
if (!this.isRunning) return
const currentTime = performance.now()
const deltaTime = currentTime - this.lastTime
this.lastTime = currentTime
// 将所有工作委托给 FrameRunner
this.frameRunner.runFrame(this.animatable, deltaTime)
}
}
if (require.main === module) {
interface CircleState {
x: number
y: number
radius: number
vx: number
vy: number
}
class BouncingCircle implements IAnimatable<CircleState> {
public state: CircleState
private ctx: CanvasRenderingContext2D
constructor(initialState: CircleState, context: CanvasRenderingContext2D) {
this.state = initialState
this.ctx = context
}
update(deltaTime: number): void {
this.state.x += this.state.vx * deltaTime
this.state.y += this.state.vy * deltaTime
const canvas = this.ctx.canvas
if (this.state.x < this.state.radius || this.state.x > canvas.width - this.state.radius) {
this.state.vx *= -1
}
if (this.state.y < this.state.radius || this.state.y > canvas.height - this.state.radius) {
this.state.vy *= -1
}
}
render(): void {
const { width, height } = this.ctx.canvas
this.ctx.clearRect(0, 0, width, height)
this.ctx.beginPath()
this.ctx.arc(this.state.x, this.state.y, this.state.radius, 0, Math.PI * 2)
this.ctx.fillStyle = '#28b'
this.ctx.fill()
}
}
const canvas = document.createElement('canvas')
document.body.appendChild(canvas)
const ctx = canvas.getContext('2d')!
const initialState: CircleState = { x: 50, y: 50, radius: 20, vx: 0.1, vy: 0.1 }
// 创建 Animatable 实例
const myCircle = new BouncingCircle(initialState, ctx)
// 组合1: 默认行为 (RAF + 简单策略)
const runner1 = new AnimationRunner(myCircle)
runner1.start()
// 组合2: 固定 30fps + 稳定逻辑更新策略
const runner2 = new AnimationRunner(
myCircle,
new SetTimeoutScheduler(30),
new FixedTimestepRunner(60) // 逻辑更新频率为 60/s
)
// runner2.start();
}