reze-engine 0.3.3 → 0.3.4

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/src/ik.ts ADDED
@@ -0,0 +1,449 @@
1
+ import { Quat, Vec3, Mat4 } from "./math"
2
+ import type { Bone, Skeleton } from "./model"
3
+
4
+ export interface IKLink {
5
+ boneIndex: number
6
+ hasLimit: boolean
7
+ minAngle?: Vec3 // Euler angles in radians
8
+ maxAngle?: Vec3 // Euler angles in radians
9
+ }
10
+
11
+ export interface IKChain {
12
+ targetBoneIndex: number // Bone that should reach the target position
13
+ effectorBoneIndex: number // End effector bone (usually the last bone in chain)
14
+ iterationCount: number
15
+ rotationConstraint: number // Angle limit in radians (typically 0.1-0.5)
16
+ links: IKLink[] // Chain links from effector toward root
17
+ enabled: boolean
18
+ }
19
+
20
+ export class IK {
21
+ private chains: IKChain[]
22
+ private computeWorldMatricesCallback?: () => void
23
+ // Track last target positions to detect movement
24
+ private lastTargetPositions: Map<number, Vec3> = new Map()
25
+ // Track convergence state for each chain (true = converged, skip solving)
26
+ private convergedChains: Set<number> = new Set()
27
+ // Accumulated IK rotations for each bone (boneIndex -> quaternion)
28
+ private ikRotations: Map<number, Quat> = new Map()
29
+
30
+ constructor(chains: IKChain[] = []) {
31
+ this.chains = chains
32
+ }
33
+
34
+ // Set the callback to use Model's computeWorldMatrices
35
+ // The callback doesn't need parameters since Model uses its own runtime state
36
+ setComputeWorldMatricesCallback(callback: () => void): void {
37
+ this.computeWorldMatricesCallback = callback
38
+ }
39
+
40
+ getChains(): IKChain[] {
41
+ return this.chains
42
+ }
43
+
44
+ // Enable/disable IK chain by target bone name
45
+ enableChain(targetBoneName: string, enabled: boolean): void {
46
+ // Chains are identified by target bone index, but we need to find by name
47
+ // This will be called from Model which has bone name lookup
48
+ for (const chain of this.chains) {
49
+ // We'll need to pass bone names or use indices - for now, this is a placeholder
50
+ // The actual implementation will be in Model class
51
+ }
52
+ }
53
+
54
+ // Main IK solve method - modifies bone local rotations in-place
55
+ solve(
56
+ skeleton: Skeleton,
57
+ localRotations: Float32Array,
58
+ localTranslations: Float32Array,
59
+ worldMatrices: Float32Array
60
+ ): void {
61
+ if (this.chains.length === 0) return
62
+
63
+ const boneCount = skeleton.bones.length
64
+
65
+ // Reset accumulated IK rotations for all chain bones (as per reference)
66
+ for (const chain of this.chains) {
67
+ for (const link of chain.links) {
68
+ const boneIdx = link.boneIndex
69
+ if (boneIdx >= 0 && boneIdx < boneCount) {
70
+ this.ikRotations.set(boneIdx, new Quat(0, 0, 0, 1))
71
+ }
72
+ }
73
+ }
74
+
75
+ // Use Model's computeWorldMatrices if available (it uses the same arrays)
76
+ // Otherwise fall back to simplified version
77
+ if (this.computeWorldMatricesCallback) {
78
+ this.computeWorldMatricesCallback()
79
+ } else {
80
+ // Fallback to simplified version (shouldn't happen in normal usage)
81
+ this.computeWorldMatrices(skeleton, localRotations, localTranslations, worldMatrices)
82
+ }
83
+
84
+ // Solve each IK chain
85
+ for (const chain of this.chains) {
86
+ if (!chain.enabled) continue
87
+
88
+ const targetBoneIdx = chain.targetBoneIndex
89
+ const effectorBoneIdx = chain.effectorBoneIndex
90
+
91
+ if (targetBoneIdx < 0 || targetBoneIdx >= boneCount || effectorBoneIdx < 0 || effectorBoneIdx >= boneCount) {
92
+ continue
93
+ }
94
+
95
+ // Get target position (world position of target bone)
96
+ // In MMD, the target bone is the bone that should reach a specific position
97
+ // The target bone's current world position is where we want the effector to reach
98
+ const targetWorldMatIdx = targetBoneIdx * 16
99
+ const targetWorldMat = new Mat4(worldMatrices.subarray(targetWorldMatIdx, targetWorldMatIdx + 16))
100
+ const targetPos = targetWorldMat.getPosition()
101
+
102
+ // Check if target has moved (detect any movement, even small)
103
+ const lastTargetPos = this.lastTargetPositions.get(targetBoneIdx)
104
+ let targetMoved = false
105
+ if (!lastTargetPos) {
106
+ // First time seeing this target, initialize position and always solve
107
+ this.lastTargetPositions.set(targetBoneIdx, new Vec3(targetPos.x, targetPos.y, targetPos.z))
108
+ targetMoved = true
109
+ } else {
110
+ const targetMoveDistance = targetPos.subtract(lastTargetPos).length()
111
+ targetMoved = targetMoveDistance > 0.001 // Detect any movement > 0.001 units (0.1mm)
112
+ }
113
+
114
+ // Get current effector position
115
+ const effectorWorldMatIdx = effectorBoneIdx * 16
116
+ const effectorWorldMat = new Mat4(worldMatrices.subarray(effectorWorldMatIdx, effectorWorldMatIdx + 16))
117
+ const effectorPos = effectorWorldMat.getPosition()
118
+
119
+ // Check distance to target
120
+ const distanceToTarget = effectorPos.subtract(targetPos).length()
121
+
122
+ // If target moved, always clear convergence and solve
123
+ if (targetMoved) {
124
+ this.convergedChains.delete(targetBoneIdx)
125
+ this.lastTargetPositions.set(targetBoneIdx, new Vec3(targetPos.x, targetPos.y, targetPos.z))
126
+ // Always solve when target moves, regardless of distance
127
+ } else if (distanceToTarget < 0.1) {
128
+ // Target hasn't moved and we're already close, skip solving
129
+ if (!this.convergedChains.has(targetBoneIdx)) {
130
+ this.convergedChains.add(targetBoneIdx)
131
+ }
132
+ continue
133
+ }
134
+ // Otherwise, solve (target hasn't moved but effector is far from target)
135
+
136
+ // Solve using CCD
137
+ // Note: In PMX, links are stored from effector toward root
138
+ // So links[0] is the effector, links[links.length-1] is closest to root
139
+ this.solveCCD(chain, skeleton, localRotations, localTranslations, worldMatrices, targetPos)
140
+
141
+ // Recompute world matrices after IK adjustments
142
+ if (this.computeWorldMatricesCallback) {
143
+ this.computeWorldMatricesCallback()
144
+ } else {
145
+ this.computeWorldMatrices(skeleton, localRotations, localTranslations, worldMatrices)
146
+ }
147
+ }
148
+ }
149
+
150
+ // Cyclic Coordinate Descent IK solver (based on saba MMD implementation)
151
+ private solveCCD(
152
+ chain: IKChain,
153
+ skeleton: Skeleton,
154
+ localRotations: Float32Array,
155
+ localTranslations: Float32Array,
156
+ worldMatrices: Float32Array,
157
+ targetPos: Vec3
158
+ ): void {
159
+ const bones = skeleton.bones
160
+ const iterationCount = chain.iterationCount
161
+ const rotationConstraint = chain.rotationConstraint
162
+ const links = chain.links
163
+
164
+ if (links.length === 0) return
165
+
166
+ const effectorBoneIdx = chain.effectorBoneIndex
167
+
168
+ // Get effector position
169
+ const effectorWorldMatIdx = effectorBoneIdx * 16
170
+ const effectorWorldMat = new Mat4(worldMatrices.subarray(effectorWorldMatIdx, effectorWorldMatIdx + 16))
171
+ let effectorPos = effectorWorldMat.getPosition()
172
+
173
+ // Check initial distance - only skip if extremely close (numerical precision threshold)
174
+ const initialDistanceSq = effectorPos.subtract(targetPos).lengthSquared()
175
+ if (initialDistanceSq < 1.0e-10) {
176
+ this.convergedChains.add(chain.targetBoneIndex)
177
+ return
178
+ }
179
+
180
+ const halfIteration = iterationCount >> 1
181
+
182
+ for (let iter = 0; iter < iterationCount; iter++) {
183
+ const useAxis = iter < halfIteration
184
+
185
+ for (let linkIdx = 0; linkIdx < links.length; linkIdx++) {
186
+ const link = links[linkIdx]
187
+ const jointBoneIdx = link.boneIndex
188
+
189
+ if (jointBoneIdx < 0 || jointBoneIdx >= bones.length) continue
190
+
191
+ const bone = bones[jointBoneIdx]
192
+
193
+ // Get joint world position
194
+ const jointWorldMatIdx = jointBoneIdx * 16
195
+ const jointWorldMat = new Mat4(worldMatrices.subarray(jointWorldMatIdx, jointWorldMatIdx + 16))
196
+ const jointPos = jointWorldMat.getPosition()
197
+
198
+ // Vectors: from joint to target and effector (REVERSED from typical CCD!)
199
+ // This matches the reference implementation
200
+ const chainTargetVector = jointPos.subtract(targetPos).normalize()
201
+ const chainIkVector = jointPos.subtract(effectorPos).normalize()
202
+
203
+ // Rotation axis: cross product
204
+ const chainRotationAxis = chainTargetVector.cross(chainIkVector)
205
+ const axisLenSq = chainRotationAxis.lengthSquared()
206
+
207
+ // Skip if axis is too small (vectors are parallel)
208
+ if (axisLenSq < 1.0e-8) continue
209
+
210
+ const chainRotationAxisNorm = chainRotationAxis.normalize()
211
+
212
+ // Get parent's world rotation matrix (rotation part only)
213
+ let parentWorldRot: Quat
214
+ if (bone.parentIndex >= 0 && bone.parentIndex < bones.length) {
215
+ const parentWorldMatIdx = bone.parentIndex * 16
216
+ const parentWorldMat = new Mat4(worldMatrices.subarray(parentWorldMatIdx, parentWorldMatIdx + 16))
217
+ parentWorldRot = parentWorldMat.toQuat()
218
+ } else {
219
+ parentWorldRot = new Quat(0, 0, 0, 1)
220
+ }
221
+
222
+ // Transform rotation axis to parent's local space
223
+ // Invert parent rotation: parentWorldRot^-1
224
+ const parentWorldRotInv = parentWorldRot.conjugate()
225
+ // Transform axis: parentWorldRotInv * axis (as vector)
226
+ const localAxis = parentWorldRotInv.rotateVec(chainRotationAxisNorm).normalize()
227
+
228
+ // Calculate angle between vectors
229
+ const dot = Math.max(-1.0, Math.min(1.0, chainTargetVector.dot(chainIkVector)))
230
+ const angle = Math.min(rotationConstraint * (linkIdx + 1), Math.acos(dot))
231
+
232
+ // Create rotation quaternion from axis and angle
233
+ // q = (sin(angle/2) * axis, cos(angle/2))
234
+ const halfAngle = angle * 0.5
235
+ const sinHalf = Math.sin(halfAngle)
236
+ const cosHalf = Math.cos(halfAngle)
237
+ const rotationFromAxis = new Quat(
238
+ localAxis.x * sinHalf,
239
+ localAxis.y * sinHalf,
240
+ localAxis.z * sinHalf,
241
+ cosHalf
242
+ ).normalize()
243
+
244
+ // Get accumulated ikRotation for this bone (or identity if first time)
245
+ let accumulatedIkRot = this.ikRotations.get(jointBoneIdx) || new Quat(0, 0, 0, 1)
246
+
247
+ // Accumulate rotation: ikRotation = rotationFromAxis * ikRotation
248
+ // Reference: ikRotation.multiplyToRef(chainBone.ikChainInfo!.ikRotation, chainBone.ikChainInfo!.ikRotation)
249
+ // This means: ikRotation = rotationFromAxis * accumulatedIkRot
250
+ accumulatedIkRot = rotationFromAxis.multiply(accumulatedIkRot)
251
+ this.ikRotations.set(jointBoneIdx, accumulatedIkRot)
252
+
253
+ // Get current local rotation
254
+ const qi = jointBoneIdx * 4
255
+ const currentLocalRot = new Quat(
256
+ localRotations[qi],
257
+ localRotations[qi + 1],
258
+ localRotations[qi + 2],
259
+ localRotations[qi + 3]
260
+ )
261
+
262
+ // Reference: ikRotation.multiplyToRef(chainBone.ikChainInfo!.localRotation, ikRotation)
263
+ // This means: tempRot = accumulatedIkRot * currentLocalRot
264
+ let tempRot = accumulatedIkRot.multiply(currentLocalRot)
265
+
266
+ // Apply angle constraints if specified (on the combined rotation)
267
+ if (link.hasLimit && link.minAngle && link.maxAngle) {
268
+ tempRot = this.applyAngleConstraints(tempRot, link.minAngle, link.maxAngle)
269
+ }
270
+
271
+ // Reference: ikRotation.multiplyToRef(invertedLocalRotation, ikRotation)
272
+ // This means: accumulatedIkRot = tempRot * currentLocalRot^-1
273
+ // But we need the new local rotation, not the accumulated IK rotation
274
+ // The new local rotation should be: newLocalRot such that accumulatedIkRot * newLocalRot = tempRot
275
+ // So: newLocalRot = accumulatedIkRot^-1 * tempRot
276
+ // But wait, the reference updates ikRotation, not localRotation directly...
277
+ // Actually, looking at the reference, it seems like ikRotation is used to compute the final rotation
278
+ // Let me try a different approach: the reference applies constraints to (ikRotation * localRotation)
279
+ // then extracts the new ikRotation, but we need the new localRotation
280
+
281
+ // Actually, I think the issue is that we should apply: newLocalRot = tempRot (the constrained result)
282
+ // But we need to extract what the new local rotation should be
283
+ // If tempRot = accumulatedIkRot * currentLocalRot (after constraints)
284
+ // Then: newLocalRot = accumulatedIkRot^-1 * tempRot
285
+ const accumulatedIkRotInv = accumulatedIkRot.conjugate()
286
+ let newLocalRot = accumulatedIkRotInv.multiply(tempRot)
287
+
288
+ // Update local rotation
289
+ const normalized = newLocalRot.normalize()
290
+ localRotations[qi] = normalized.x
291
+ localRotations[qi + 1] = normalized.y
292
+ localRotations[qi + 2] = normalized.z
293
+ localRotations[qi + 3] = normalized.w
294
+
295
+ // Update accumulated IK rotation as per reference
296
+ const localRotInv = currentLocalRot.conjugate()
297
+ accumulatedIkRot = tempRot.multiply(localRotInv)
298
+ this.ikRotations.set(jointBoneIdx, accumulatedIkRot)
299
+
300
+ // Update world matrices after this link adjustment (only once per link, not per bone)
301
+ if (this.computeWorldMatricesCallback) {
302
+ this.computeWorldMatricesCallback()
303
+ } else {
304
+ this.computeWorldMatrices(skeleton, localRotations, localTranslations, worldMatrices)
305
+ }
306
+
307
+ // Update effector position for next link
308
+ const updatedEffectorMat2 = new Mat4(worldMatrices.subarray(effectorWorldMatIdx, effectorWorldMatIdx + 16))
309
+ effectorPos = updatedEffectorMat2.getPosition()
310
+
311
+ // Early exit if converged (check against original target position)
312
+ const currentDistanceSq = effectorPos.subtract(targetPos).lengthSquared()
313
+ if (currentDistanceSq < 1.0e-10) {
314
+ this.convergedChains.add(chain.targetBoneIndex)
315
+ return
316
+ }
317
+ }
318
+
319
+ // Check convergence at end of iteration
320
+ const finalEffectorMat = new Mat4(worldMatrices.subarray(effectorWorldMatIdx, effectorWorldMatIdx + 16))
321
+ const finalEffectorPos = finalEffectorMat.getPosition()
322
+ const finalDistanceSq = finalEffectorPos.subtract(targetPos).lengthSquared()
323
+
324
+ if (finalDistanceSq < 1.0e-10) {
325
+ this.convergedChains.add(chain.targetBoneIndex)
326
+ break
327
+ }
328
+ }
329
+ }
330
+
331
+ // Apply angle constraints to local rotation (Euler angle limits)
332
+ private applyAngleConstraints(localRot: Quat, minAngle: Vec3, maxAngle: Vec3): Quat {
333
+ // Convert quaternion to Euler angles
334
+ const euler = localRot.toEuler()
335
+
336
+ // Clamp each Euler angle component
337
+ let clampedX = Math.max(minAngle.x, Math.min(maxAngle.x, euler.x))
338
+ let clampedY = Math.max(minAngle.y, Math.min(maxAngle.y, euler.y))
339
+ let clampedZ = Math.max(minAngle.z, Math.min(maxAngle.z, euler.z))
340
+
341
+ // Convert back to quaternion (ZXY order, left-handed)
342
+ return Quat.fromEuler(clampedX, clampedY, clampedZ)
343
+ }
344
+
345
+ // Compute world matrices from local rotations and translations
346
+ // This matches Model.computeWorldMatrices logic (including append transforms)
347
+ private computeWorldMatrices(
348
+ skeleton: Skeleton,
349
+ localRotations: Float32Array,
350
+ localTranslations: Float32Array,
351
+ worldMatrices: Float32Array
352
+ ): void {
353
+ const bones = skeleton.bones
354
+ const boneCount = bones.length
355
+ const computed = new Array(boneCount).fill(false)
356
+
357
+ const computeWorld = (i: number): void => {
358
+ if (computed[i]) return
359
+
360
+ const bone = bones[i]
361
+ if (bone.parentIndex >= boneCount) {
362
+ console.warn(`[IK] bone ${i} parent out of range: ${bone.parentIndex}`)
363
+ }
364
+
365
+ const qi = i * 4
366
+ const ti = i * 3
367
+
368
+ // Get local rotation
369
+ let rotateM = Mat4.fromQuat(
370
+ localRotations[qi],
371
+ localRotations[qi + 1],
372
+ localRotations[qi + 2],
373
+ localRotations[qi + 3]
374
+ )
375
+ let addLocalTx = 0,
376
+ addLocalTy = 0,
377
+ addLocalTz = 0
378
+
379
+ // Handle append transforms (same as Model.computeWorldMatrices)
380
+ const appendParentIdx = bone.appendParentIndex
381
+ const hasAppend =
382
+ bone.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
383
+
384
+ if (hasAppend) {
385
+ const ratio = bone.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, bone.appendRatio))
386
+ const hasRatio = Math.abs(ratio) > 1e-6
387
+
388
+ if (hasRatio) {
389
+ const apQi = appendParentIdx * 4
390
+ const apTi = appendParentIdx * 3
391
+
392
+ if (bone.appendRotate) {
393
+ let ax = localRotations[apQi]
394
+ let ay = localRotations[apQi + 1]
395
+ let az = localRotations[apQi + 2]
396
+ const aw = localRotations[apQi + 3]
397
+ const absRatio = ratio < 0 ? -ratio : ratio
398
+ if (ratio < 0) {
399
+ ax = -ax
400
+ ay = -ay
401
+ az = -az
402
+ }
403
+ const identityQuat = new Quat(0, 0, 0, 1)
404
+ const appendQuat = new Quat(ax, ay, az, aw)
405
+ const result = Quat.slerp(identityQuat, appendQuat, absRatio)
406
+ rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
407
+ }
408
+
409
+ if (bone.appendMove) {
410
+ const appendRatio = bone.appendRatio ?? 1
411
+ addLocalTx = localTranslations[apTi] * appendRatio
412
+ addLocalTy = localTranslations[apTi + 1] * appendRatio
413
+ addLocalTz = localTranslations[apTi + 2] * appendRatio
414
+ }
415
+ }
416
+ }
417
+
418
+ // Get bone's own translation
419
+ const boneTx = localTranslations[ti]
420
+ const boneTy = localTranslations[ti + 1]
421
+ const boneTz = localTranslations[ti + 2]
422
+
423
+ // Build local matrix: bindTranslation + rotation + (bone translation + append translation)
424
+ const localM = Mat4.identity().translateInPlace(
425
+ bone.bindTranslation[0],
426
+ bone.bindTranslation[1],
427
+ bone.bindTranslation[2]
428
+ )
429
+ const transM = Mat4.identity().translateInPlace(boneTx + addLocalTx, boneTy + addLocalTy, boneTz + addLocalTz)
430
+ const localMatrix = localM.multiply(rotateM).multiply(transM)
431
+
432
+ const worldOffset = i * 16
433
+ if (bone.parentIndex >= 0) {
434
+ const p = bone.parentIndex
435
+ if (!computed[p]) computeWorld(p)
436
+ const parentOffset = p * 16
437
+ const parentWorldMat = new Mat4(worldMatrices.subarray(parentOffset, parentOffset + 16))
438
+ const worldMat = parentWorldMat.multiply(localMatrix)
439
+ worldMatrices.set(worldMat.values, worldOffset)
440
+ } else {
441
+ worldMatrices.set(localMatrix.values, worldOffset)
442
+ }
443
+ computed[i] = true
444
+ }
445
+
446
+ // Process all bones
447
+ for (let i = 0; i < boneCount; i++) computeWorld(i)
448
+ }
449
+ }
package/src/math.ts CHANGED
@@ -26,6 +26,10 @@ export class Vec3 {
26
26
  return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
27
27
  }
28
28
 
29
+ lengthSquared(): number {
30
+ return this.x * this.x + this.y * this.y + this.z * this.z
31
+ }
32
+
29
33
  normalize(): Vec3 {
30
34
  const len = this.length()
31
35
  if (len === 0) return new Vec3(0, 0, 0)
package/src/player.ts CHANGED
@@ -31,72 +31,13 @@ export class Player {
31
31
  private pausedTime: number = 0 // Accumulated paused duration
32
32
  private pauseStartTime: number = 0
33
33
 
34
- // Audio
35
- private audioElement?: HTMLAudioElement
36
- private audioUrl?: string
37
- private audioLoaded: boolean = false
38
-
39
34
  /**
40
- * Load VMD animation file and optionally audio
35
+ * Load VMD animation file
41
36
  */
42
- async loadVmd(vmdUrl: string, audioUrl?: string): Promise<void> {
37
+ async loadVmd(vmdUrl: string): Promise<void> {
43
38
  // Load animation
44
39
  this.frames = await VMDLoader.load(vmdUrl)
45
40
  this.processFrames()
46
-
47
- // Load audio if provided
48
- if (audioUrl) {
49
- await this.loadAudio(audioUrl)
50
- }
51
- }
52
-
53
- /**
54
- * Load audio file
55
- */
56
- async loadAudio(url: string): Promise<void> {
57
- this.audioUrl = url
58
- this.audioLoaded = false
59
-
60
- return new Promise((resolve, reject) => {
61
- const audio = new Audio(url)
62
- audio.preload = "auto"
63
-
64
- // iOS Safari requires playsinline attribute for inline playback
65
- // This must be set before loading
66
- audio.setAttribute("playsinline", "true")
67
- audio.setAttribute("webkit-playsinline", "true")
68
-
69
- // Set volume to ensure audio is ready
70
- audio.volume = 1.0
71
-
72
- // iOS sometimes requires audio element to be in DOM
73
- // Add it hidden to the document body
74
- audio.style.display = "none"
75
- audio.style.position = "absolute"
76
- audio.style.visibility = "hidden"
77
- audio.style.width = "0"
78
- audio.style.height = "0"
79
- document.body.appendChild(audio)
80
-
81
- audio.addEventListener("loadeddata", () => {
82
- this.audioElement = audio
83
- this.audioLoaded = true
84
- resolve()
85
- })
86
-
87
- audio.addEventListener("error", (e) => {
88
- console.warn("Failed to load audio:", url, e)
89
- this.audioLoaded = false
90
- // Remove from DOM on error
91
- if (audio.parentNode) {
92
- audio.parentNode.removeChild(audio)
93
- }
94
- // Don't reject - animation should still work without audio
95
- resolve()
96
- })
97
-
98
- audio.load()
99
- })
100
41
  }
101
42
 
102
43
  /**
@@ -176,6 +117,7 @@ export class Player {
176
117
 
177
118
  /**
178
119
  * Start or resume playback
120
+ * Note: For iOS, this should be called synchronously from a user interaction event
179
121
  */
180
122
  play(): void {
181
123
  if (this.frames.length === 0) return
@@ -192,25 +134,6 @@ export class Player {
192
134
  }
193
135
 
194
136
  this.isPlaying = true
195
-
196
- // Play audio if available
197
- if (this.audioElement && this.audioLoaded) {
198
- // Ensure audio is ready for iOS
199
- this.audioElement.currentTime = this.currentTime
200
- this.audioElement.muted = false
201
- this.audioElement.volume = 1.0
202
-
203
- // iOS requires play() to be called synchronously during user interaction
204
- // This must happen directly from the user's click/touch event
205
- const playPromise = this.audioElement.play()
206
-
207
- if (playPromise !== undefined) {
208
- playPromise.catch((error) => {
209
- // Log error but don't block animation playback
210
- console.warn("Audio play failed:", error, error.name)
211
- })
212
- }
213
- }
214
137
  }
215
138
 
216
139
  /**
@@ -221,11 +144,6 @@ export class Player {
221
144
 
222
145
  this.isPaused = true
223
146
  this.pauseStartTime = performance.now()
224
-
225
- // Pause audio if available
226
- if (this.audioElement) {
227
- this.audioElement.pause()
228
- }
229
147
  }
230
148
 
231
149
  /**
@@ -237,12 +155,6 @@ export class Player {
237
155
  this.currentTime = 0
238
156
  this.startTime = 0
239
157
  this.pausedTime = 0
240
-
241
- // Stop audio if available
242
- if (this.audioElement) {
243
- this.audioElement.pause()
244
- this.audioElement.currentTime = 0
245
- }
246
158
  }
247
159
 
248
160
  /**
@@ -257,11 +169,6 @@ export class Player {
257
169
  this.startTime = performance.now() - clampedTime * 1000
258
170
  this.pausedTime = 0
259
171
  }
260
-
261
- // Seek audio if available
262
- if (this.audioElement && this.audioLoaded) {
263
- this.audioElement.currentTime = clampedTime
264
- }
265
172
  }
266
173
 
267
174
  /**
@@ -289,15 +196,6 @@ export class Player {
289
196
  return this.getPoseAtTime(this.currentTime)
290
197
  }
291
198
 
292
- // Sync audio if present (with tolerance)
293
- if (this.audioElement && this.audioLoaded) {
294
- const audioTime = this.audioElement.currentTime
295
- const syncTolerance = 0.1 // 100ms tolerance
296
- if (Math.abs(audioTime - this.currentTime) > syncTolerance) {
297
- this.audioElement.currentTime = this.currentTime
298
- }
299
- }
300
-
301
199
  return this.getPoseAtTime(this.currentTime)
302
200
  }
303
201
 
@@ -485,38 +383,4 @@ export class Player {
485
383
  isPausedState(): boolean {
486
384
  return this.isPaused
487
385
  }
488
-
489
- /**
490
- * Check if has audio
491
- */
492
- hasAudio(): boolean {
493
- return this.audioElement !== undefined && this.audioLoaded
494
- }
495
-
496
- /**
497
- * Set audio volume (0.0 to 1.0)
498
- */
499
- setVolume(volume: number): void {
500
- if (this.audioElement) {
501
- this.audioElement.volume = Math.max(0, Math.min(1, volume))
502
- }
503
- }
504
-
505
- /**
506
- * Mute audio
507
- */
508
- mute(): void {
509
- if (this.audioElement) {
510
- this.audioElement.muted = true
511
- }
512
- }
513
-
514
- /**
515
- * Unmute audio
516
- */
517
- unmute(): void {
518
- if (this.audioElement) {
519
- this.audioElement.muted = false
520
- }
521
- }
522
386
  }