reze-engine 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ammo-loader.d.ts +0 -1
- package/dist/ammo-loader.d.ts.map +1 -1
- package/dist/ammo-loader.js +0 -3
- package/dist/engine.d.ts +2 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +44 -100
- package/dist/ik-solver.d.ts +7 -2
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +110 -137
- package/dist/math.d.ts +10 -6
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +36 -62
- package/dist/model.d.ts +1 -19
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +24 -93
- package/dist/player.d.ts +6 -20
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +89 -193
- package/package.json +1 -1
- package/src/ammo-loader.ts +0 -4
- package/src/engine.ts +54 -101
- package/src/ik-solver.ts +121 -178
- package/src/math.ts +43 -82
- package/src/model.ts +26 -101
- package/src/player.ts +116 -212
- package/src/bezier-interpolate.ts +0 -47
- package/src/ik.ts +0 -449
package/src/player.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { bezierInterpolate } from "./
|
|
2
|
-
import { Quat, Vec3 } from "./math"
|
|
1
|
+
import { Quat, Vec3, bezierInterpolate } from "./math"
|
|
3
2
|
import { BoneFrame, MorphFrame, VMDKeyFrame, VMDLoader } from "./vmd-loader"
|
|
4
3
|
|
|
5
4
|
export interface AnimationPose {
|
|
@@ -19,17 +18,15 @@ export class Player {
|
|
|
19
18
|
private frames: VMDKeyFrame[] = []
|
|
20
19
|
private boneTracks: Map<string, Array<{ boneFrame: BoneFrame; time: number }>> = new Map()
|
|
21
20
|
private morphTracks: Map<string, Array<{ morphFrame: MorphFrame; time: number }>> = new Map()
|
|
22
|
-
private
|
|
21
|
+
private _duration: number = 0
|
|
23
22
|
|
|
24
23
|
// Playback state
|
|
25
24
|
private isPlaying: boolean = false
|
|
26
25
|
private isPaused: boolean = false
|
|
27
|
-
private
|
|
26
|
+
private _currentTime: number = 0
|
|
28
27
|
|
|
29
28
|
// Timing
|
|
30
29
|
private startTime: number = 0 // Real-time when playback started
|
|
31
|
-
private pausedTime: number = 0 // Accumulated paused duration
|
|
32
|
-
private pauseStartTime: number = 0
|
|
33
30
|
|
|
34
31
|
/**
|
|
35
32
|
* Load VMD animation file
|
|
@@ -44,75 +41,57 @@ export class Player {
|
|
|
44
41
|
* Process frames into tracks
|
|
45
42
|
*/
|
|
46
43
|
private processFrames(): void {
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
44
|
+
// Helper to group frames by name and sort by time
|
|
45
|
+
const groupFrames = <T>(
|
|
46
|
+
items: Array<{ item: T; name: string; time: number }>
|
|
47
|
+
): Map<string, Array<{ item: T; time: number }>> => {
|
|
48
|
+
const tracks = new Map<string, Array<{ item: T; time: number }>>()
|
|
49
|
+
for (const { item, name, time } of items) {
|
|
50
|
+
if (!tracks.has(name)) tracks.set(name, [])
|
|
51
|
+
tracks.get(name)!.push({ item, time })
|
|
55
52
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const boneKeyFramesByBone = new Map<string, Array<{ boneFrame: BoneFrame; time: number }>>()
|
|
59
|
-
for (const { boneFrame, time } of allBoneKeyFrames) {
|
|
60
|
-
if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
|
|
61
|
-
boneKeyFramesByBone.set(boneFrame.boneName, [])
|
|
53
|
+
for (const keyFrames of tracks.values()) {
|
|
54
|
+
keyFrames.sort((a, b) => a.time - b.time)
|
|
62
55
|
}
|
|
63
|
-
|
|
56
|
+
return tracks
|
|
64
57
|
}
|
|
65
58
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
59
|
+
// Collect all bone and morph frames
|
|
60
|
+
const boneItems: Array<{ item: BoneFrame; name: string; time: number }> = []
|
|
61
|
+
const morphItems: Array<{ item: MorphFrame; name: string; time: number }> = []
|
|
69
62
|
|
|
70
|
-
// Process morph frames
|
|
71
|
-
const allMorphKeyFrames: Array<{ morphFrame: MorphFrame; time: number }> = []
|
|
72
63
|
for (const keyFrame of this.frames) {
|
|
64
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
65
|
+
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time })
|
|
66
|
+
}
|
|
73
67
|
for (const morphFrame of keyFrame.morphFrames) {
|
|
74
|
-
|
|
75
|
-
morphFrame,
|
|
76
|
-
time: keyFrame.time,
|
|
77
|
-
})
|
|
68
|
+
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time })
|
|
78
69
|
}
|
|
79
70
|
}
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
72
|
+
// Transform to expected format
|
|
73
|
+
this.boneTracks = new Map()
|
|
74
|
+
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
75
|
+
this.boneTracks.set(
|
|
76
|
+
name,
|
|
77
|
+
frames.map((f) => ({ boneFrame: f.item, time: f.time }))
|
|
78
|
+
)
|
|
87
79
|
}
|
|
88
80
|
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
this.morphTracks = new Map()
|
|
82
|
+
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
83
|
+
this.morphTracks.set(
|
|
84
|
+
name,
|
|
85
|
+
frames.map((f) => ({ morphFrame: f.item, time: f.time }))
|
|
86
|
+
)
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
//
|
|
94
|
-
this.boneTracks
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
for (const keyFrames of this.boneTracks.values()) {
|
|
100
|
-
if (keyFrames.length > 0) {
|
|
101
|
-
const lastTime = keyFrames[keyFrames.length - 1].time
|
|
102
|
-
if (lastTime > maxFrameTime) {
|
|
103
|
-
maxFrameTime = lastTime
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
for (const keyFrames of this.morphTracks.values()) {
|
|
108
|
-
if (keyFrames.length > 0) {
|
|
109
|
-
const lastTime = keyFrames[keyFrames.length - 1].time
|
|
110
|
-
if (lastTime > maxFrameTime) {
|
|
111
|
-
maxFrameTime = lastTime
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
this.duration = maxFrameTime > 0 ? maxFrameTime : 0
|
|
89
|
+
// Calculate duration from all tracks
|
|
90
|
+
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
|
|
91
|
+
this._duration = allTracks.reduce((max, keyFrames) => {
|
|
92
|
+
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0
|
|
93
|
+
return Math.max(max, lastTime)
|
|
94
|
+
}, 0)
|
|
116
95
|
}
|
|
117
96
|
|
|
118
97
|
/**
|
|
@@ -122,16 +101,8 @@ export class Player {
|
|
|
122
101
|
play(): void {
|
|
123
102
|
if (this.frames.length === 0) return
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.isPaused = false
|
|
128
|
-
// Adjust start time so current time calculation continues smoothly
|
|
129
|
-
this.startTime = performance.now() - this.currentTime * 1000
|
|
130
|
-
} else {
|
|
131
|
-
// Start from beginning or current seek position
|
|
132
|
-
this.startTime = performance.now() - this.currentTime * 1000
|
|
133
|
-
this.pausedTime = 0
|
|
134
|
-
}
|
|
104
|
+
this.isPaused = false
|
|
105
|
+
this.startTime = performance.now() - this._currentTime * 1000
|
|
135
106
|
|
|
136
107
|
this.isPlaying = true
|
|
137
108
|
}
|
|
@@ -141,9 +112,7 @@ export class Player {
|
|
|
141
112
|
*/
|
|
142
113
|
pause(): void {
|
|
143
114
|
if (!this.isPlaying || this.isPaused) return
|
|
144
|
-
|
|
145
115
|
this.isPaused = true
|
|
146
|
-
this.pauseStartTime = performance.now()
|
|
147
116
|
}
|
|
148
117
|
|
|
149
118
|
/**
|
|
@@ -152,22 +121,19 @@ export class Player {
|
|
|
152
121
|
stop(): void {
|
|
153
122
|
this.isPlaying = false
|
|
154
123
|
this.isPaused = false
|
|
155
|
-
this.
|
|
124
|
+
this._currentTime = 0
|
|
156
125
|
this.startTime = 0
|
|
157
|
-
this.pausedTime = 0
|
|
158
126
|
}
|
|
159
127
|
|
|
160
128
|
/**
|
|
161
129
|
* Seek to specific time
|
|
162
130
|
*/
|
|
163
131
|
seek(time: number): void {
|
|
164
|
-
const clampedTime = Math.max(0, Math.min(time, this.
|
|
165
|
-
this.
|
|
132
|
+
const clampedTime = Math.max(0, Math.min(time, this._duration))
|
|
133
|
+
this._currentTime = clampedTime
|
|
166
134
|
|
|
167
|
-
// Adjust start time if playing
|
|
168
135
|
if (this.isPlaying && !this.isPaused) {
|
|
169
136
|
this.startTime = performance.now() - clampedTime * 1000
|
|
170
|
-
this.pausedTime = 0
|
|
171
137
|
}
|
|
172
138
|
}
|
|
173
139
|
|
|
@@ -182,21 +148,21 @@ export class Player {
|
|
|
182
148
|
|
|
183
149
|
// If paused, return current pose at paused time (no time update)
|
|
184
150
|
if (this.isPaused) {
|
|
185
|
-
return this.getPoseAtTime(this.
|
|
151
|
+
return this.getPoseAtTime(this._currentTime)
|
|
186
152
|
}
|
|
187
153
|
|
|
188
154
|
// Calculate current animation time
|
|
189
155
|
const elapsedSeconds = (currentRealTime - this.startTime) / 1000
|
|
190
|
-
this.
|
|
156
|
+
this._currentTime = elapsedSeconds
|
|
191
157
|
|
|
192
158
|
// Check if animation ended
|
|
193
|
-
if (this.
|
|
194
|
-
this.
|
|
159
|
+
if (this._currentTime >= this._duration) {
|
|
160
|
+
this._currentTime = this._duration
|
|
195
161
|
this.pause() // Auto-pause at end
|
|
196
|
-
return this.getPoseAtTime(this.
|
|
162
|
+
return this.getPoseAtTime(this._currentTime)
|
|
197
163
|
}
|
|
198
164
|
|
|
199
|
-
return this.getPoseAtTime(this.
|
|
165
|
+
return this.getPoseAtTime(this._currentTime)
|
|
200
166
|
}
|
|
201
167
|
|
|
202
168
|
/**
|
|
@@ -209,136 +175,86 @@ export class Player {
|
|
|
209
175
|
morphWeights: new Map(),
|
|
210
176
|
}
|
|
211
177
|
|
|
212
|
-
//
|
|
213
|
-
const
|
|
214
|
-
let left = 0
|
|
215
|
-
|
|
178
|
+
// Generic binary search for upper bound
|
|
179
|
+
const upperBound = <T extends { time: number }>(time: number, keyFrames: T[]): number => {
|
|
180
|
+
let left = 0,
|
|
181
|
+
right = keyFrames.length
|
|
216
182
|
while (left < right) {
|
|
217
183
|
const mid = Math.floor((left + right) / 2)
|
|
218
|
-
if (keyFrames[mid].time <= time)
|
|
219
|
-
|
|
220
|
-
} else {
|
|
221
|
-
right = mid
|
|
222
|
-
}
|
|
184
|
+
if (keyFrames[mid].time <= time) left = mid + 1
|
|
185
|
+
else right = mid
|
|
223
186
|
}
|
|
224
187
|
return left
|
|
225
188
|
}
|
|
226
189
|
|
|
227
|
-
// Process
|
|
190
|
+
// Process bone tracks
|
|
228
191
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
229
192
|
if (keyFrames.length === 0) continue
|
|
230
193
|
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const clampedFrameTime = Math.max(startTime, Math.min(endTime, time))
|
|
235
|
-
|
|
236
|
-
const upperBoundIndex = upperBoundFrameIndex(clampedFrameTime, keyFrames)
|
|
237
|
-
const upperBoundIndexMinusOne = upperBoundIndex - 1
|
|
238
|
-
|
|
239
|
-
if (upperBoundIndexMinusOne < 0) continue
|
|
194
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
195
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
196
|
+
if (idx < 0) continue
|
|
240
197
|
|
|
241
|
-
const
|
|
242
|
-
const
|
|
198
|
+
const frameA = keyFrames[idx].boneFrame
|
|
199
|
+
const frameB = keyFrames[idx + 1]?.boneFrame
|
|
243
200
|
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
pose.
|
|
247
|
-
pose.boneTranslations.set(boneName, boneFrameA.translation)
|
|
201
|
+
if (!frameB) {
|
|
202
|
+
pose.boneRotations.set(boneName, frameA.rotation)
|
|
203
|
+
pose.boneTranslations.set(boneName, frameA.translation)
|
|
248
204
|
} else {
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
gradient
|
|
262
|
-
)
|
|
263
|
-
const interpolatedRotation = Quat.slerp(boneFrameA.rotation, boneFrameB.rotation, rotWeight)
|
|
264
|
-
|
|
265
|
-
// Interpolate translation using Bezier (separate curves for X, Y, Z)
|
|
266
|
-
const xWeight = bezierInterpolate(
|
|
267
|
-
interp[0] / 127, // X_x1
|
|
268
|
-
interp[8] / 127, // X_x2
|
|
269
|
-
interp[4] / 127, // X_y1
|
|
270
|
-
interp[12] / 127, // X_y2
|
|
271
|
-
gradient
|
|
272
|
-
)
|
|
273
|
-
const yWeight = bezierInterpolate(
|
|
274
|
-
interp[16] / 127, // Y_x1
|
|
275
|
-
interp[24] / 127, // Y_x2
|
|
276
|
-
interp[20] / 127, // Y_y1
|
|
277
|
-
interp[28] / 127, // Y_y2
|
|
278
|
-
gradient
|
|
279
|
-
)
|
|
280
|
-
const zWeight = bezierInterpolate(
|
|
281
|
-
interp[32] / 127, // Z_x1
|
|
282
|
-
interp[40] / 127, // Z_x2
|
|
283
|
-
interp[36] / 127, // Z_y1
|
|
284
|
-
interp[44] / 127, // Z_y2
|
|
285
|
-
gradient
|
|
205
|
+
const timeA = keyFrames[idx].time
|
|
206
|
+
const timeB = keyFrames[idx + 1].time
|
|
207
|
+
const gradient = (clampedTime - timeA) / (timeB - timeA)
|
|
208
|
+
const interp = frameB.interpolation
|
|
209
|
+
|
|
210
|
+
pose.boneRotations.set(
|
|
211
|
+
boneName,
|
|
212
|
+
Quat.slerp(
|
|
213
|
+
frameA.rotation,
|
|
214
|
+
frameB.rotation,
|
|
215
|
+
bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient)
|
|
216
|
+
)
|
|
286
217
|
)
|
|
287
218
|
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
219
|
+
const lerp = (a: number, b: number, w: number) => a + (b - a) * w
|
|
220
|
+
const getWeight = (offset: number) =>
|
|
221
|
+
bezierInterpolate(
|
|
222
|
+
interp[offset] / 127,
|
|
223
|
+
interp[offset + 8] / 127,
|
|
224
|
+
interp[offset + 4] / 127,
|
|
225
|
+
interp[offset + 12] / 127,
|
|
226
|
+
gradient
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
pose.boneTranslations.set(
|
|
230
|
+
boneName,
|
|
231
|
+
new Vec3(
|
|
232
|
+
lerp(frameA.translation.x, frameB.translation.x, getWeight(0)),
|
|
233
|
+
lerp(frameA.translation.y, frameB.translation.y, getWeight(16)),
|
|
234
|
+
lerp(frameA.translation.z, frameB.translation.z, getWeight(32))
|
|
235
|
+
)
|
|
292
236
|
)
|
|
293
|
-
|
|
294
|
-
pose.boneRotations.set(boneName, interpolatedRotation)
|
|
295
|
-
pose.boneTranslations.set(boneName, interpolatedTranslation)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Helper to find upper bound index for morph frames
|
|
300
|
-
const upperBoundMorphIndex = (time: number, keyFrames: Array<{ morphFrame: MorphFrame; time: number }>): number => {
|
|
301
|
-
let left = 0
|
|
302
|
-
let right = keyFrames.length
|
|
303
|
-
while (left < right) {
|
|
304
|
-
const mid = Math.floor((left + right) / 2)
|
|
305
|
-
if (keyFrames[mid].time <= time) {
|
|
306
|
-
left = mid + 1
|
|
307
|
-
} else {
|
|
308
|
-
right = mid
|
|
309
|
-
}
|
|
310
237
|
}
|
|
311
|
-
return left
|
|
312
238
|
}
|
|
313
239
|
|
|
314
|
-
// Process
|
|
240
|
+
// Process morph tracks
|
|
315
241
|
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
316
242
|
if (keyFrames.length === 0) continue
|
|
317
243
|
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const clampedFrameTime = Math.max(startTime, Math.min(endTime, time))
|
|
322
|
-
|
|
323
|
-
const upperBoundIndex = upperBoundMorphIndex(clampedFrameTime, keyFrames)
|
|
324
|
-
const upperBoundIndexMinusOne = upperBoundIndex - 1
|
|
325
|
-
|
|
326
|
-
if (upperBoundIndexMinusOne < 0) continue
|
|
244
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
245
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
246
|
+
if (idx < 0) continue
|
|
327
247
|
|
|
328
|
-
const
|
|
329
|
-
const
|
|
248
|
+
const frameA = keyFrames[idx].morphFrame
|
|
249
|
+
const frameB = keyFrames[idx + 1]?.morphFrame
|
|
330
250
|
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
pose.morphWeights.set(morphName, morphFrameA.weight)
|
|
251
|
+
if (!frameB) {
|
|
252
|
+
pose.morphWeights.set(morphName, frameA.weight)
|
|
334
253
|
} else {
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
const interpolatedWeight = morphFrameA.weight + (morphFrameB.weight - morphFrameA.weight) * gradient
|
|
340
|
-
|
|
341
|
-
pose.morphWeights.set(morphName, interpolatedWeight)
|
|
254
|
+
const timeA = keyFrames[idx].time
|
|
255
|
+
const timeB = keyFrames[idx + 1].time
|
|
256
|
+
const gradient = (clampedTime - timeA) / (timeB - timeA)
|
|
257
|
+
pose.morphWeights.set(morphName, frameA.weight + (frameB.weight - frameA.weight) * gradient)
|
|
342
258
|
}
|
|
343
259
|
}
|
|
344
260
|
|
|
@@ -350,37 +266,25 @@ export class Player {
|
|
|
350
266
|
*/
|
|
351
267
|
getProgress(): AnimationProgress {
|
|
352
268
|
return {
|
|
353
|
-
current: this.
|
|
354
|
-
duration: this.
|
|
355
|
-
percentage: this.
|
|
269
|
+
current: this._currentTime,
|
|
270
|
+
duration: this._duration,
|
|
271
|
+
percentage: this._duration > 0 ? (this._currentTime / this._duration) * 100 : 0,
|
|
356
272
|
}
|
|
357
273
|
}
|
|
358
274
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
*/
|
|
362
|
-
getCurrentTime(): number {
|
|
363
|
-
return this.currentTime
|
|
275
|
+
get currentTime(): number {
|
|
276
|
+
return this._currentTime
|
|
364
277
|
}
|
|
365
278
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
*/
|
|
369
|
-
getDuration(): number {
|
|
370
|
-
return this.duration
|
|
279
|
+
get duration(): number {
|
|
280
|
+
return this._duration
|
|
371
281
|
}
|
|
372
282
|
|
|
373
|
-
|
|
374
|
-
* Check if playing
|
|
375
|
-
*/
|
|
376
|
-
isPlayingState(): boolean {
|
|
283
|
+
get isPlayingState(): boolean {
|
|
377
284
|
return this.isPlaying && !this.isPaused
|
|
378
285
|
}
|
|
379
286
|
|
|
380
|
-
|
|
381
|
-
* Check if paused
|
|
382
|
-
*/
|
|
383
|
-
isPausedState(): boolean {
|
|
287
|
+
get isPausedState(): boolean {
|
|
384
288
|
return this.isPaused
|
|
385
289
|
}
|
|
386
290
|
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bezier interpolation for VMD animations
|
|
3
|
-
* Based on the reference implementation from babylon-mmd
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Bezier interpolation function
|
|
8
|
-
* @param x1 First control point X (0-127, normalized to 0-1)
|
|
9
|
-
* @param x2 Second control point X (0-127, normalized to 0-1)
|
|
10
|
-
* @param y1 First control point Y (0-127, normalized to 0-1)
|
|
11
|
-
* @param y2 Second control point Y (0-127, normalized to 0-1)
|
|
12
|
-
* @param t Interpolation parameter (0-1)
|
|
13
|
-
* @returns Interpolated value (0-1)
|
|
14
|
-
*/
|
|
15
|
-
export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
|
|
16
|
-
// Clamp t to [0, 1]
|
|
17
|
-
t = Math.max(0, Math.min(1, t))
|
|
18
|
-
|
|
19
|
-
// Binary search for the t value that gives us the desired x
|
|
20
|
-
// We're solving for t in the Bezier curve: x(t) = 3*(1-t)^2*t*x1 + 3*(1-t)*t^2*x2 + t^3
|
|
21
|
-
let start = 0
|
|
22
|
-
let end = 1
|
|
23
|
-
let mid = 0.5
|
|
24
|
-
|
|
25
|
-
// Iterate until we find the t value that gives us the desired x
|
|
26
|
-
for (let i = 0; i < 15; i++) {
|
|
27
|
-
// Evaluate Bezier curve at mid point
|
|
28
|
-
const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
|
|
29
|
-
|
|
30
|
-
if (Math.abs(x - t) < 0.0001) {
|
|
31
|
-
break
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (x < t) {
|
|
35
|
-
start = mid
|
|
36
|
-
} else {
|
|
37
|
-
end = mid
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
mid = (start + end) / 2
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Now evaluate the y value at this t
|
|
44
|
-
const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
|
|
45
|
-
|
|
46
|
-
return y
|
|
47
|
-
}
|