@haiyue/extensions 0.1.0
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/animation/Animation2DComponent.d.ts +46 -0
- package/dist/animation/Animation2DExtensionRegistry.d.ts +27 -0
- package/dist/animation/Animation2DRenderSystem.d.ts +89 -0
- package/dist/animation/Animation2DRuntime.d.ts +99 -0
- package/dist/animation/Animation2DStateMachine.d.ts +13 -0
- package/dist/animation/Animation2DStateMachineComponent.d.ts +54 -0
- package/dist/animation/Animation2DStateMachineRuntime.d.ts +30 -0
- package/dist/animation/Animation2DStateMachineSystem.d.ts +14 -0
- package/dist/animation/Animation2DStateMachineVisualRuntime.d.ts +27 -0
- package/dist/animation/Animation2DSystem.d.ts +14 -0
- package/dist/animation/AnimationAssetLoader.d.ts +9 -0
- package/dist/animation/AnimationAudioClip.d.ts +26 -0
- package/dist/animation/AnimationPathTessellator.d.ts +52 -0
- package/dist/animation/AnimationTextRasterizer.d.ts +21 -0
- package/dist/animation/AnimationVisual2D.d.ts +68 -0
- package/dist/animation/HyaAnimation2DClipAdapter.d.ts +16 -0
- package/dist/animation/Particle2DDescriptorAdapter.d.ts +5 -0
- package/dist/animation/runtime/mixer/Action.d.ts +150 -0
- package/dist/animation/runtime/mixer/Mixer.d.ts +54 -0
- package/dist/animation/runtime/mixer/PoseBuffer.d.ts +23 -0
- package/dist/animation/runtime/mixer/Sampler.d.ts +17 -0
- package/dist/animation/runtime/mixer/Types.d.ts +101 -0
- package/dist/animation/runtime/mixer/index.d.ts +6 -0
- package/dist/animation-state-machine/AnimationStateMachine.d.ts +111 -0
- package/dist/animation-state-machine/AnimationStateMachineChannels.d.ts +23 -0
- package/dist/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.d.ts +51 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.d.ts +14 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineCompiler.d.ts +109 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineController.d.ts +103 -0
- package/dist/animation-state-machine/runtime/AnimationStateMachineMixerPort.d.ts +35 -0
- package/dist/animation-state-machine/runtime/index.d.ts +7 -0
- package/dist/animation.d.ts +15 -0
- package/dist/animation.js +1171 -0
- package/dist/animation3d/Animation3DAction.d.ts +46 -0
- package/dist/animation3d/Animation3DBinding.d.ts +85 -0
- package/dist/animation3d/Animation3DClip.d.ts +20 -0
- package/dist/animation3d/Animation3DError.d.ts +13 -0
- package/dist/animation3d/Animation3DMixer.d.ts +27 -0
- package/dist/animation3d/Animation3DPose.d.ts +40 -0
- package/dist/animation3d/Animation3DPoseBuffer.d.ts +24 -0
- package/dist/animation3d/Animation3DResource.d.ts +28 -0
- package/dist/animation3d/Animation3DStateMachine.d.ts +95 -0
- package/dist/animation3d/Animation3DStateMachineRuntime.d.ts +70 -0
- package/dist/animation3d/Animation3DTrack.d.ts +19 -0
- package/dist/animation3d/hya/HyaAnimation3DRuntime.d.ts +73 -0
- package/dist/animation3d/hya/HyaAnimation3DStateMachineRuntime.d.ts +65 -0
- package/dist/animation3d/hya/HyaAnimation3DTypes.d.ts +170 -0
- package/dist/animation3d/hya/index.d.ts +4 -0
- package/dist/animation3d/index.d.ts +14 -0
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.d.ts +81 -0
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.d.ts +39 -0
- package/dist/animation3d/runtime/integration/index.d.ts +2 -0
- package/dist/animation3d/runtime/mixer/Action.d.ts +150 -0
- package/dist/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.d.ts +6 -0
- package/dist/animation3d/runtime/mixer/Mixer.d.ts +59 -0
- package/dist/animation3d/runtime/mixer/PoseBuffer.d.ts +1 -0
- package/dist/animation3d/runtime/mixer/TrackSampler.d.ts +24 -0
- package/dist/animation3d/runtime/mixer/index.d.ts +4 -0
- package/dist/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineController.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.d.ts +1 -0
- package/dist/animation3d/runtime/state-machine/index.d.ts +6 -0
- package/dist/animation3d.d.ts +5 -0
- package/dist/animation3d.js +1708 -0
- package/dist/benchmark.d.ts +30 -0
- package/dist/benchmark.js +119 -0
- package/dist/canvas-text/CanvasText2DRenderSystem.d.ts +24 -0
- package/dist/canvas-text/CanvasTextComponent.d.ts +18 -0
- package/dist/canvas-text.d.ts +4 -0
- package/dist/canvas-text.js +276 -0
- package/dist/chunks/2d-ui-animation-2d.generated-D4aHzFU2.js +4 -0
- package/dist/chunks/2d-ui-canvas-text-2d.generated-C0Eaej2m.js +4 -0
- package/dist/chunks/2d-ui-spine2d.generated-CUUi8xjl.js +4 -0
- package/dist/chunks/2d-ui-tilemap2d.generated-gebw4mX7.js +4 -0
- package/dist/chunks/Animation2DStateMachine-9IonguNa.js +3245 -0
- package/dist/chunks/AnimationStateMachineController-BIcAh-sT.js +1438 -0
- package/dist/chunks/GltfAnimation3DAdapter-Cu3rQQsW.js +2240 -0
- package/dist/chunks/GltfAnimationRuntime-CRHsLycN.js +631 -0
- package/dist/chunks/RenderSystem2DBase-DDT61dAu.js +227 -0
- package/dist/chunks/Spine2DRuntime-D9M9LAQR.js +3056 -0
- package/dist/chunks/SpineAssetParser-BSDmPge1.js +378 -0
- package/dist/chunks/gltfLoader-Dm22jLYB.js +2085 -0
- package/dist/chunks/pluginUtils-CvwroRMd.js +33 -0
- package/dist/experimental-gltf-worker.d.ts +4 -0
- package/dist/experimental-gltf-worker.js +138 -0
- package/dist/experimental-spine-worker.d.ts +4 -0
- package/dist/experimental-spine-worker.js +110 -0
- package/dist/gltf/GltfAccessorReader.d.ts +7 -0
- package/dist/gltf/GltfAnimation3DAdapter.d.ts +47 -0
- package/dist/gltf/GltfAnimationRuntime.d.ts +10 -0
- package/dist/gltf/GltfAssetWorkerClient.d.ts +20 -0
- package/dist/gltf/GltfCompatibilityReport.d.ts +70 -0
- package/dist/gltf/GltfConservativeBounds.d.ts +18 -0
- package/dist/gltf/GltfDracoDecoder.d.ts +4 -0
- package/dist/gltf/GltfExtensionAdapter.d.ts +54 -0
- package/dist/gltf/GltfLoaderContract.d.ts +249 -0
- package/dist/gltf/GltfLoaderErrors.d.ts +4 -0
- package/dist/gltf/GltfMaterialDescriptor.d.ts +23 -0
- package/dist/gltf/GltfMaterialEncoder.d.ts +20 -0
- package/dist/gltf/GltfMaterialLoader.d.ts +6 -0
- package/dist/gltf/GltfModelComponent.d.ts +72 -0
- package/dist/gltf/GltfModelSystem.d.ts +37 -0
- package/dist/gltf/GltfPlugin.d.ts +6 -0
- package/dist/gltf/GltfSchema.d.ts +225 -0
- package/dist/gltf/GltfUvSemanticPlanner.d.ts +33 -0
- package/dist/gltf/gltfLoader.d.ts +9 -0
- package/dist/gltf-animation3d.d.ts +3 -0
- package/dist/gltf-animation3d.js +8 -0
- package/dist/gltf-worker-runtime.d.ts +7 -0
- package/dist/gltf-worker-runtime.js +6981 -0
- package/dist/gltf.d.ts +12 -0
- package/dist/gltf.js +517 -0
- package/dist/grid/Grid2DComponent.d.ts +25 -0
- package/dist/grid.d.ts +2 -0
- package/dist/grid.js +56 -0
- package/dist/hya-state-machine.d.ts +7 -0
- package/dist/hya-state-machine.js +2934 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/internal/2d-ui-shader-artifact.js +870 -0
- package/dist/plugins/pluginUtils.d.ts +20 -0
- package/dist/plugins.d.ts +6 -0
- package/dist/shaders/generated/2d-ui-artifact.generated.d.ts +658 -0
- package/dist/spine/Spine2DComponent.d.ts +116 -0
- package/dist/spine/Spine2DGpuRenderer.d.ts +65 -0
- package/dist/spine/Spine2DRenderSystem.d.ts +27 -0
- package/dist/spine/Spine2DRuntime.d.ts +103 -0
- package/dist/spine/SpineAnimationBoneTimelines.d.ts +8 -0
- package/dist/spine/SpineAssetParser.d.ts +18 -0
- package/dist/spine/SpineAssetWorkerClient.d.ts +19 -0
- package/dist/spine/SpineAssetWorkerContract.d.ts +6 -0
- package/dist/spine/SpineAtlasParser.d.ts +21 -0
- package/dist/spine/SpineFloatBuilder.d.ts +15 -0
- package/dist/spine/SpinePathConstraintSolver.d.ts +32 -0
- package/dist/spine/SpinePlugin.d.ts +8 -0
- package/dist/spine/SpineSkeletonRuntime.d.ts +185 -0
- package/dist/spine/SpineSlotAnimation.d.ts +17 -0
- package/dist/spine/SpineTimelineRuntime.d.ts +25 -0
- package/dist/spine/SpineVertexBuilder.d.ts +61 -0
- package/dist/spine.d.ts +7 -0
- package/dist/spine.js +578 -0
- package/dist/tilemap/Tilemap2DComponent.d.ts +110 -0
- package/dist/tilemap/Tilemap2DRenderSystem.d.ts +11 -0
- package/dist/tilemap/Tilemap2DRenderer.d.ts +24 -0
- package/dist/tilemap/TilemapPlugin.d.ts +8 -0
- package/dist/tilemap.d.ts +6 -0
- package/dist/tilemap.js +535 -0
- package/dist/tween/Tween2DComponent.d.ts +70 -0
- package/dist/tween/Tween2DSystem.d.ts +12 -0
- package/dist/tween.d.ts +4 -0
- package/dist/tween.js +138 -0
- package/dist/utils/RenderSystem2DBase.d.ts +42 -0
- package/dist/utils/arrayAccess.d.ts +4 -0
- package/dist/utils/camera2d.d.ts +12 -0
- package/dist/utils/render2dGpu.d.ts +32 -0
- package/package.json +124 -0
- package/src/animation/Animation2DComponent.ts +135 -0
- package/src/animation/Animation2DExtensionRegistry.ts +52 -0
- package/src/animation/Animation2DRenderSystem.ts +1026 -0
- package/src/animation/Animation2DRuntime.ts +1326 -0
- package/src/animation/Animation2DStateMachine.ts +39 -0
- package/src/animation/Animation2DStateMachineComponent.ts +228 -0
- package/src/animation/Animation2DStateMachineRuntime.ts +141 -0
- package/src/animation/Animation2DStateMachineSystem.ts +60 -0
- package/src/animation/Animation2DStateMachineVisualRuntime.ts +282 -0
- package/src/animation/Animation2DSystem.ts +72 -0
- package/src/animation/AnimationAssetLoader.ts +84 -0
- package/src/animation/AnimationAudioClip.ts +140 -0
- package/src/animation/AnimationPathTessellator.ts +691 -0
- package/src/animation/AnimationTextRasterizer.ts +340 -0
- package/src/animation/AnimationVisual2D.ts +153 -0
- package/src/animation/HyaAnimation2DClipAdapter.ts +845 -0
- package/src/animation/Particle2DDescriptorAdapter.ts +35 -0
- package/src/animation/runtime/mixer/Action.ts +876 -0
- package/src/animation/runtime/mixer/Mixer.ts +505 -0
- package/src/animation/runtime/mixer/PoseBuffer.ts +105 -0
- package/src/animation/runtime/mixer/Sampler.ts +252 -0
- package/src/animation/runtime/mixer/Types.ts +138 -0
- package/src/animation/runtime/mixer/index.ts +31 -0
- package/src/animation-state-machine/AnimationStateMachine.ts +146 -0
- package/src/animation-state-machine/AnimationStateMachineChannels.ts +116 -0
- package/src/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.ts +197 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.ts +503 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineCompiler.ts +763 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineController.ts +987 -0
- package/src/animation-state-machine/runtime/AnimationStateMachineMixerPort.ts +54 -0
- package/src/animation-state-machine/runtime/index.ts +47 -0
- package/src/animation.ts +33 -0
- package/src/animation3d/Animation3DAction.ts +73 -0
- package/src/animation3d/Animation3DBinding.ts +116 -0
- package/src/animation3d/Animation3DClip.ts +22 -0
- package/src/animation3d/Animation3DError.ts +34 -0
- package/src/animation3d/Animation3DMixer.ts +82 -0
- package/src/animation3d/Animation3DPose.ts +100 -0
- package/src/animation3d/Animation3DPoseBuffer.ts +85 -0
- package/src/animation3d/Animation3DResource.ts +49 -0
- package/src/animation3d/Animation3DStateMachine.ts +126 -0
- package/src/animation3d/Animation3DStateMachineRuntime.ts +191 -0
- package/src/animation3d/Animation3DTrack.ts +29 -0
- package/src/animation3d/hya/HyaAnimation3DRuntime.ts +756 -0
- package/src/animation3d/hya/HyaAnimation3DStateMachineRuntime.ts +618 -0
- package/src/animation3d/hya/HyaAnimation3DTypes.ts +168 -0
- package/src/animation3d/hya/index.ts +10 -0
- package/src/animation3d/index.ts +76 -0
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.ts +325 -0
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.ts +158 -0
- package/src/animation3d/runtime/integration/index.ts +9 -0
- package/src/animation3d/runtime/mixer/Action.ts +833 -0
- package/src/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.ts +26 -0
- package/src/animation3d/runtime/mixer/Mixer.ts +562 -0
- package/src/animation3d/runtime/mixer/PoseBuffer.ts +1 -0
- package/src/animation3d/runtime/mixer/TrackSampler.ts +399 -0
- package/src/animation3d/runtime/mixer/index.ts +8 -0
- package/src/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.ts +4 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.ts +1 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineController.ts +1 -0
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.ts +1 -0
- package/src/animation3d/runtime/state-machine/index.ts +45 -0
- package/src/animation3d.ts +5 -0
- package/src/benchmark.ts +168 -0
- package/src/canvas-text/CanvasText2DRenderSystem.ts +292 -0
- package/src/canvas-text/CanvasTextComponent.ts +47 -0
- package/src/canvas-text.ts +4 -0
- package/src/experimental-gltf-worker.ts +18 -0
- package/src/experimental-spine-worker.ts +9 -0
- package/src/gltf/GltfAccessorReader.ts +271 -0
- package/src/gltf/GltfAnimation3DAdapter.ts +680 -0
- package/src/gltf/GltfAnimationRuntime.ts +392 -0
- package/src/gltf/GltfAssetWorkerClient.ts +136 -0
- package/src/gltf/GltfCompatibilityReport.ts +214 -0
- package/src/gltf/GltfConservativeBounds.ts +219 -0
- package/src/gltf/GltfDracoDecoder.ts +251 -0
- package/src/gltf/GltfExtensionAdapter.ts +340 -0
- package/src/gltf/GltfLoaderContract.ts +333 -0
- package/src/gltf/GltfLoaderErrors.ts +59 -0
- package/src/gltf/GltfMaterialDescriptor.ts +134 -0
- package/src/gltf/GltfMaterialEncoder.ts +173 -0
- package/src/gltf/GltfMaterialLoader.ts +293 -0
- package/src/gltf/GltfModelComponent.ts +115 -0
- package/src/gltf/GltfModelSystem.ts +208 -0
- package/src/gltf/GltfPlugin.ts +84 -0
- package/src/gltf/GltfSchema.ts +229 -0
- package/src/gltf/GltfUvSemanticPlanner.ts +166 -0
- package/src/gltf/gltfLoader.ts +910 -0
- package/src/gltf-animation3d.ts +10 -0
- package/src/gltf-worker-runtime.ts +10 -0
- package/src/gltf.ts +56 -0
- package/src/grid/Grid2DComponent.ts +69 -0
- package/src/grid.ts +2 -0
- package/src/hya-state-machine.ts +19 -0
- package/src/index.ts +2 -0
- package/src/plugins/pluginUtils.ts +50 -0
- package/src/plugins.ts +6 -0
- package/src/shaders/generated/2d-ui-animation-2d.generated.wgsl +242 -0
- package/src/shaders/generated/2d-ui-artifact.generated.ts +867 -0
- package/src/shaders/generated/2d-ui-canvas-text-2d.generated.wgsl +38 -0
- package/src/shaders/generated/2d-ui-spine2d.generated.wgsl +41 -0
- package/src/shaders/generated/2d-ui-tilemap2d.generated.wgsl +36 -0
- package/src/spine/Spine2DComponent.ts +168 -0
- package/src/spine/Spine2DGpuRenderer.ts +449 -0
- package/src/spine/Spine2DRenderSystem.ts +199 -0
- package/src/spine/Spine2DRuntime.ts +1529 -0
- package/src/spine/SpineAnimationBoneTimelines.ts +30 -0
- package/src/spine/SpineAssetParser.ts +20 -0
- package/src/spine/SpineAssetWorkerClient.ts +112 -0
- package/src/spine/SpineAssetWorkerContract.ts +4 -0
- package/src/spine/SpineAtlasParser.ts +127 -0
- package/src/spine/SpineFloatBuilder.ts +66 -0
- package/src/spine/SpinePathConstraintSolver.ts +582 -0
- package/src/spine/SpinePlugin.ts +76 -0
- package/src/spine/SpineSkeletonRuntime.ts +521 -0
- package/src/spine/SpineSlotAnimation.ts +195 -0
- package/src/spine/SpineTimelineRuntime.ts +453 -0
- package/src/spine/SpineVertexBuilder.ts +686 -0
- package/src/spine.ts +7 -0
- package/src/tilemap/Tilemap2DComponent.ts +232 -0
- package/src/tilemap/Tilemap2DRenderSystem.ts +58 -0
- package/src/tilemap/Tilemap2DRenderer.ts +289 -0
- package/src/tilemap/TilemapPlugin.ts +75 -0
- package/src/tilemap.ts +6 -0
- package/src/tween/Tween2DComponent.ts +83 -0
- package/src/tween/Tween2DSystem.ts +78 -0
- package/src/tween.ts +4 -0
- package/src/types/earcut.d.ts +7 -0
- package/src/types/wgsl.d.ts +4 -0
- package/src/utils/RenderSystem2DBase.ts +164 -0
- package/src/utils/arrayAccess.ts +17 -0
- package/src/utils/camera2d.ts +68 -0
- package/src/utils/render2dGpu.ts +132 -0
- package/test/fixtures/gltf/animation-characterization.gltf +344 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { Entity } from '@haiyue/engine';
|
|
2
|
+
import type { AssetManager } from '@haiyue/engine/assets';
|
|
3
|
+
import type { ParsedAnimation } from '@haiyue/animation-spec';
|
|
4
|
+
import type { Animation2DExtensionRegistry } from './Animation2DExtensionRegistry.js';
|
|
5
|
+
import type { HyaAnimation2DEffectPayload } from './HyaAnimation2DClipAdapter.js';
|
|
6
|
+
import {
|
|
7
|
+
Animation2DRuntime,
|
|
8
|
+
type Animation2DRuntimeNode,
|
|
9
|
+
} from './Animation2DRuntime.js';
|
|
10
|
+
import type { Animation2DEffectEvent, Animation2DPose } from './runtime/mixer/index.js';
|
|
11
|
+
import {
|
|
12
|
+
audioAutoplayRejectedDiagnostic,
|
|
13
|
+
type AnimationStateMachineChannelDiagnostic,
|
|
14
|
+
} from '../animation-state-machine/AnimationStateMachineChannels.js';
|
|
15
|
+
|
|
16
|
+
/** State-machine-only pose writer kept out of the ordinary Animation2D bundle. */
|
|
17
|
+
export class Animation2DStateMachineVisualRuntime extends Animation2DRuntime {
|
|
18
|
+
private readonly _particleOwners = new Map<string, string[]>();
|
|
19
|
+
private readonly _audioOwners = new Map<string, string[]>();
|
|
20
|
+
private readonly _effectPayloads = new Map<string, HyaAnimation2DEffectPayload>();
|
|
21
|
+
private readonly _diagnostics: AnimationStateMachineChannelDiagnostic[] = [];
|
|
22
|
+
private readonly _reportedAudioRejections = new Set<string>();
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
owner: Entity,
|
|
26
|
+
animation: ParsedAnimation,
|
|
27
|
+
runtimeExtensions?: Animation2DExtensionRegistry,
|
|
28
|
+
assetManager?: AssetManager,
|
|
29
|
+
) {
|
|
30
|
+
super(owner, animation, runtimeExtensions, assetManager);
|
|
31
|
+
for (const node of this._nodes) {
|
|
32
|
+
for (const particle of node.particles) {
|
|
33
|
+
particle.playing = false;
|
|
34
|
+
particle.emitting = false;
|
|
35
|
+
particle.clear();
|
|
36
|
+
}
|
|
37
|
+
for (const audio of node.audio) audio.exitStateMachine();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get diagnostics(): readonly AnimationStateMachineChannelDiagnostic[] {
|
|
42
|
+
return this._diagnostics;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get sideEffectOwnerCount(): number {
|
|
46
|
+
return ownerCount(this._particleOwners) + ownerCount(this._audioOwners);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resetSideEffects(): void {
|
|
50
|
+
this._particleOwners.clear();
|
|
51
|
+
this._audioOwners.clear();
|
|
52
|
+
this._effectPayloads.clear();
|
|
53
|
+
for (const node of this._nodes) {
|
|
54
|
+
for (const particle of node.particles) {
|
|
55
|
+
particle.playing = false;
|
|
56
|
+
particle.emitting = false;
|
|
57
|
+
particle.clear();
|
|
58
|
+
}
|
|
59
|
+
for (const audio of node.audio) audio.exitStateMachine();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
applyPose(pose: Animation2DPose, playing = this._playing, speed = this._speed): void {
|
|
64
|
+
this._playing = playing;
|
|
65
|
+
this._speed = speed;
|
|
66
|
+
this._opacityMemo.clear();
|
|
67
|
+
for (const node of this._nodes) {
|
|
68
|
+
node.entity.disabled = false;
|
|
69
|
+
node.transform.setPosition(node.initialX, node.initialY);
|
|
70
|
+
node.transform.rotation = node.initialRotation;
|
|
71
|
+
node.transform.setScale(node.initialScaleX, node.initialScaleY);
|
|
72
|
+
node.opacity = node.initialOpacity;
|
|
73
|
+
}
|
|
74
|
+
for (const channel of pose.channels) {
|
|
75
|
+
const node = this._nodesById.get(channel.binding.targetId);
|
|
76
|
+
if (!node) {
|
|
77
|
+
throw new ReferenceError(`Animation2D pose references unknown node "${channel.binding.targetId}".`);
|
|
78
|
+
}
|
|
79
|
+
switch (channel.binding.path) {
|
|
80
|
+
case 'transform.position': {
|
|
81
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 2);
|
|
82
|
+
node.transform.setPosition(value[0]!, -value[1]!);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'transform.rotation': {
|
|
86
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 1);
|
|
87
|
+
node.transform.rotation = -value[0]!;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'transform.scale': {
|
|
91
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 2);
|
|
92
|
+
node.transform.setScale(value[0]!, value[1]!);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'opacity': {
|
|
96
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 1);
|
|
97
|
+
node.opacity = clamp(value[0]!, 0, 1);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'visibility':
|
|
101
|
+
if (typeof channel.value !== 'boolean') {
|
|
102
|
+
throw new TypeError(`Animation2D visibility binding "${channel.binding.id}" requires a boolean.`);
|
|
103
|
+
}
|
|
104
|
+
node.entity.disabled = !channel.value;
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
if (!this._applyStateMachineVisualChannel(node, channel.binding.path, channel.value)) {
|
|
108
|
+
throw new RangeError(`Animation2D state-machine runtime does not support pose binding path "${channel.binding.path}".`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const node of this._nodes) this._applyVisualOpacity(node);
|
|
113
|
+
for (const [key, owners] of this._audioOwners) {
|
|
114
|
+
if (owners.length === 0) continue;
|
|
115
|
+
const target = parseEffectTargetKey(key);
|
|
116
|
+
const node = this._nodesById.get(target.targetId);
|
|
117
|
+
this._audioTarget(key)?.updateStateMachineProperties(
|
|
118
|
+
speed,
|
|
119
|
+
node ? this._resolveOpacity(node) : 1,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
for (const effect of pose.effects) this._applyEffect(effect, playing, speed);
|
|
123
|
+
this._lastAppliedTime = Number.NaN;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override setPlaying(playing: boolean): void {
|
|
127
|
+
this._playing = playing;
|
|
128
|
+
for (const [key, owners] of this._particleOwners) {
|
|
129
|
+
if (owners.length === 0) continue;
|
|
130
|
+
const target = this._particleTarget(key);
|
|
131
|
+
if (target) target.playing = playing;
|
|
132
|
+
}
|
|
133
|
+
for (const [key, owners] of this._audioOwners) {
|
|
134
|
+
if (owners.length === 0) continue;
|
|
135
|
+
const target = this._audioTarget(key);
|
|
136
|
+
target?.setStateMachinePlaying(playing, this._speed, reason => this._reportAudioRejection(key, reason));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override destroy(): void {
|
|
141
|
+
this.resetSideEffects();
|
|
142
|
+
super.destroy();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private _applyVisualOpacity(node: Animation2DRuntimeNode): void {
|
|
146
|
+
const opacity = this._resolveOpacity(node);
|
|
147
|
+
if (node.deferredVisuals.length > 0 && this._isNodeActive(node)) {
|
|
148
|
+
for (const deferred of node.deferredVisuals.splice(0)) this._materializeCoreVisual(node, deferred);
|
|
149
|
+
}
|
|
150
|
+
for (const visual of node.visuals) {
|
|
151
|
+
const alpha = visual.baseAlpha * opacity;
|
|
152
|
+
if (Math.abs(alpha - visual.lastAlpha) < 1e-6) continue;
|
|
153
|
+
visual.color[3] = alpha;
|
|
154
|
+
visual.component.revision++;
|
|
155
|
+
visual.lastAlpha = alpha;
|
|
156
|
+
}
|
|
157
|
+
for (const extension of node.extensions) extension.setOpacity?.(opacity);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private _applyEffect(effect: Animation2DEffectEvent, playing: boolean, speed: number): void {
|
|
161
|
+
const payload = effect.cue.payload as HyaAnimation2DEffectPayload | undefined;
|
|
162
|
+
if (!payload || typeof payload.targetId !== 'string' || !Number.isSafeInteger(payload.slot)) {
|
|
163
|
+
throw new TypeError(`Animation2D effect cue "${effect.cue.id}" has an invalid HYA target payload.`);
|
|
164
|
+
}
|
|
165
|
+
const key = effectTargetKey(payload);
|
|
166
|
+
this._effectPayloads.set(key, payload);
|
|
167
|
+
const owners = effect.cue.kind === 'particle'
|
|
168
|
+
? this._particleOwners
|
|
169
|
+
: this._audioOwners;
|
|
170
|
+
const stack = owners.get(key) ?? [];
|
|
171
|
+
if (!owners.has(key)) owners.set(key, stack);
|
|
172
|
+
|
|
173
|
+
if (effect.lifecycle === 'enter') {
|
|
174
|
+
const existing = stack.indexOf(effect.actionId);
|
|
175
|
+
if (existing >= 0) stack.splice(existing, 1);
|
|
176
|
+
stack.push(effect.actionId);
|
|
177
|
+
if (effect.cue.kind === 'particle') {
|
|
178
|
+
const particle = this._particleTarget(key);
|
|
179
|
+
if (particle) {
|
|
180
|
+
particle.emitting = true;
|
|
181
|
+
particle.restart(true);
|
|
182
|
+
particle.playing = playing;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
const audio = this._audioTarget(key);
|
|
186
|
+
const node = this._nodesById.get(payload.targetId);
|
|
187
|
+
const opacity = node ? this._resolveOpacity(node) : 1;
|
|
188
|
+
audio?.enterStateMachine(playing, speed, opacity, reason => this._reportAudioRejection(key, reason));
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (effect.lifecycle === 'exit') {
|
|
194
|
+
const index = stack.indexOf(effect.actionId);
|
|
195
|
+
const wasDominant = index === stack.length - 1;
|
|
196
|
+
if (index >= 0) stack.splice(index, 1);
|
|
197
|
+
if (stack.length === 0) {
|
|
198
|
+
owners.delete(key);
|
|
199
|
+
if (effect.cue.kind === 'particle') {
|
|
200
|
+
const particle = this._particleTarget(key);
|
|
201
|
+
if (particle) {
|
|
202
|
+
particle.playing = false;
|
|
203
|
+
particle.emitting = false;
|
|
204
|
+
particle.clear();
|
|
205
|
+
}
|
|
206
|
+
} else this._audioTarget(key)?.exitStateMachine();
|
|
207
|
+
} else if (wasDominant && effect.cue.kind === 'particle') {
|
|
208
|
+
const particle = this._particleTarget(key);
|
|
209
|
+
if (particle) {
|
|
210
|
+
particle.emitting = true;
|
|
211
|
+
particle.restart(true);
|
|
212
|
+
particle.playing = playing;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (stack[stack.length - 1] !== effect.actionId) return;
|
|
219
|
+
if (effect.lifecycle === 'loop') return;
|
|
220
|
+
if (effect.cue.kind === 'particle') {
|
|
221
|
+
const particle = this._particleTarget(key);
|
|
222
|
+
if (particle) {
|
|
223
|
+
particle.emitting = true;
|
|
224
|
+
particle.restart(true);
|
|
225
|
+
particle.playing = playing;
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
const audio = this._audioTarget(key);
|
|
229
|
+
const node = this._nodesById.get(payload.targetId);
|
|
230
|
+
const opacity = node ? this._resolveOpacity(node) : 1;
|
|
231
|
+
audio?.restartStateMachine(playing, speed, opacity, reason => this._reportAudioRejection(key, reason));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private _particleTarget(key: string) {
|
|
236
|
+
const target = parseEffectTargetKey(key);
|
|
237
|
+
return this._nodesById.get(target.targetId)?.particles[target.slot];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private _audioTarget(key: string) {
|
|
241
|
+
const target = parseEffectTargetKey(key);
|
|
242
|
+
return this._nodesById.get(target.targetId)?.audio[target.slot];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private _reportAudioRejection(key: string, reason: unknown): void {
|
|
246
|
+
if (this._reportedAudioRejections.has(key)) return;
|
|
247
|
+
this._reportedAudioRejections.add(key);
|
|
248
|
+
const target = parseEffectTargetKey(key);
|
|
249
|
+
const sourcePath = this._effectPayloads.get(key)?.sourcePath
|
|
250
|
+
?? `$.nodes[id=${JSON.stringify(target.targetId)}].components[audio=${target.slot}]`;
|
|
251
|
+
this._diagnostics.push(audioAutoplayRejectedDiagnostic(sourcePath, reason));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function effectTargetKey(payload: HyaAnimation2DEffectPayload): string {
|
|
256
|
+
return `${payload.targetId}\u0000${payload.slot}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function parseEffectTargetKey(key: string): { readonly targetId: string; readonly slot: number } {
|
|
260
|
+
const separator = key.lastIndexOf('\u0000');
|
|
261
|
+
return {
|
|
262
|
+
targetId: key.slice(0, separator),
|
|
263
|
+
slot: Number(key.slice(separator + 1)),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function ownerCount(owners: ReadonlyMap<string, readonly string[]>): number {
|
|
268
|
+
let count = 0;
|
|
269
|
+
for (const stack of owners.values()) count += stack.length;
|
|
270
|
+
return count;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function numericPoseValue(value: unknown, bindingId: string, size: number): Readonly<Float32Array> {
|
|
274
|
+
if (!(value instanceof Float32Array) || value.length < size) {
|
|
275
|
+
throw new TypeError(`Animation2D numeric binding "${bindingId}" requires ${size} Float32 values.`);
|
|
276
|
+
}
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function clamp(value: number, min: number, max: number): number {
|
|
281
|
+
return Math.min(max, Math.max(min, value));
|
|
282
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { System, type Entity, type World } from '@haiyue/engine';
|
|
2
|
+
import type { AssetManager } from '@haiyue/engine/assets';
|
|
3
|
+
import { Animation2DComponent } from './Animation2DComponent';
|
|
4
|
+
import { Animation2DRuntime } from './Animation2DRuntime';
|
|
5
|
+
|
|
6
|
+
export interface Animation2DSystemOptions {
|
|
7
|
+
priority?: number;
|
|
8
|
+
/** Shared engine asset manager used by sprite2d resources. */
|
|
9
|
+
assetManager?: AssetManager;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Animation2DSystem extends System {
|
|
13
|
+
private readonly _assetManager: AssetManager | undefined;
|
|
14
|
+
|
|
15
|
+
constructor(options: Animation2DSystemOptions = {}) {
|
|
16
|
+
super({ all: [Animation2DComponent] });
|
|
17
|
+
this.name = 'Animation2DSystem';
|
|
18
|
+
if (options.priority !== undefined) this.priority = options.priority;
|
|
19
|
+
this._assetManager = options.assetManager;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override update(world: World, _time: number, delta: number): this {
|
|
23
|
+
if (this.disabled) return this;
|
|
24
|
+
const entities = this.entitySet.get(world);
|
|
25
|
+
if (!entities) return this;
|
|
26
|
+
for (const entity of entities) this._updateEntity(entity, delta);
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override destroy(): this {
|
|
31
|
+
for (const world of this.usedBy) {
|
|
32
|
+
const entities = this.entitySet.get(world);
|
|
33
|
+
if (!entities) continue;
|
|
34
|
+
for (const entity of entities) entity.getComponent(Animation2DComponent)?._disposeRuntime();
|
|
35
|
+
}
|
|
36
|
+
return super.destroy();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private _updateEntity(entity: Entity, delta: number): void {
|
|
40
|
+
const component = entity.getComponent(Animation2DComponent);
|
|
41
|
+
if (!component) return;
|
|
42
|
+
if (component.disabled) {
|
|
43
|
+
component._disposeRuntime();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (component.completed && component.animation.endBehavior === 'destroy' && !component.playing) return;
|
|
47
|
+
component._runtime ??= new Animation2DRuntime(entity, component.animation, component.runtimeExtensions, this._assetManager);
|
|
48
|
+
if (component.playing && delta > 0 && component.speed > 0) {
|
|
49
|
+
component.currentTime += delta * 0.001 * component.speed;
|
|
50
|
+
if (component.currentTime >= component.animation.duration) {
|
|
51
|
+
if (component.loop) {
|
|
52
|
+
component.currentTime %= component.animation.duration;
|
|
53
|
+
component._forceParticleSeek = true;
|
|
54
|
+
} else {
|
|
55
|
+
component.currentTime = component.animation.duration;
|
|
56
|
+
component.playing = false;
|
|
57
|
+
component.completed = true;
|
|
58
|
+
if (component.animation.endBehavior === 'destroy') {
|
|
59
|
+
component._disposeRuntime();
|
|
60
|
+
component._needsApply = false;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
component._needsApply = true;
|
|
66
|
+
}
|
|
67
|
+
if (!component._needsApply) return;
|
|
68
|
+
component._runtime.apply(component.currentTime, component.playing, component.speed, component._forceParticleSeek);
|
|
69
|
+
component._needsApply = false;
|
|
70
|
+
component._forceParticleSeek = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANIMATION_MIME_TYPE,
|
|
3
|
+
parseAnimation,
|
|
4
|
+
type AnimationResource,
|
|
5
|
+
type AnimationParseOptions,
|
|
6
|
+
type ParsedAnimation,
|
|
7
|
+
} from '@haiyue/animation-spec';
|
|
8
|
+
import type { AssetLoaderRegistration } from '@haiyue/engine/assets';
|
|
9
|
+
|
|
10
|
+
export const HAIYUE_ANIMATION_ASSET_TYPE = 'animation/haiyue' as const;
|
|
11
|
+
|
|
12
|
+
export interface AnimationAssetLoaderOptions {
|
|
13
|
+
parse?: AnimationParseOptions;
|
|
14
|
+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let nextAnimationLoaderId = 1;
|
|
18
|
+
|
|
19
|
+
/** Creates an AssetManager loader without installing source-format converters in the runtime. */
|
|
20
|
+
export function createAnimationAssetLoader(
|
|
21
|
+
options: AnimationAssetLoaderOptions = {},
|
|
22
|
+
): AssetLoaderRegistration<ParsedAnimation> {
|
|
23
|
+
const fetchResource = options.fetch ?? fetch;
|
|
24
|
+
const loaderId = nextAnimationLoaderId++;
|
|
25
|
+
return {
|
|
26
|
+
type: HAIYUE_ANIMATION_ASSET_TYPE,
|
|
27
|
+
extensions: ['.hya'],
|
|
28
|
+
mimeTypes: [ANIMATION_MIME_TYPE],
|
|
29
|
+
aliases: ['hya'],
|
|
30
|
+
async load(url, context) {
|
|
31
|
+
context.setPhase('loading');
|
|
32
|
+
const networkKey = `network:hya:${url}`;
|
|
33
|
+
let buffer = context.cache.network.get(networkKey) as ArrayBuffer | undefined;
|
|
34
|
+
let responseUrl = '';
|
|
35
|
+
if (!buffer) {
|
|
36
|
+
const response = await fetchResource(url, { signal: context.signal });
|
|
37
|
+
if (!response.ok) throw new Error(`Failed to load Haiyue animation (${response.status} ${response.statusText}): ${url}`);
|
|
38
|
+
responseUrl = response.url;
|
|
39
|
+
buffer = await response.arrayBuffer();
|
|
40
|
+
if (context.signal.aborted) throw context.signal.reason;
|
|
41
|
+
context.cache.network.set(networkKey, buffer, buffer.byteLength);
|
|
42
|
+
}
|
|
43
|
+
context.reportProgress(buffer.byteLength, buffer.byteLength);
|
|
44
|
+
context.setPhase('parsing');
|
|
45
|
+
const parsedKey = `parsed:hya:${loaderId}:${url}`;
|
|
46
|
+
let animation = context.cache.parsed.get(parsedKey) as ParsedAnimation | undefined;
|
|
47
|
+
if (!animation) {
|
|
48
|
+
animation = resolveAnimationResourceUris(
|
|
49
|
+
parseAnimation(buffer, options.parse),
|
|
50
|
+
responseUrl || absoluteAnimationUrl(url),
|
|
51
|
+
);
|
|
52
|
+
context.cache.parsed.set(parsedKey, animation, buffer.byteLength);
|
|
53
|
+
}
|
|
54
|
+
return animation;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Package-relative resources resolve from the fetched HYA, including redirects. */
|
|
60
|
+
function resolveAnimationResourceUris(animation: ParsedAnimation, sourceUrl: string): ParsedAnimation {
|
|
61
|
+
if (!sourceUrl || animation.resources.length === 0) return animation;
|
|
62
|
+
const resources = animation.resources.map(resource => {
|
|
63
|
+
try {
|
|
64
|
+
return Object.freeze({ ...resource, uri: new URL(resource.uri, sourceUrl).href });
|
|
65
|
+
} catch {
|
|
66
|
+
return resource;
|
|
67
|
+
}
|
|
68
|
+
}) as readonly AnimationResource[];
|
|
69
|
+
return Object.freeze({ ...animation, resources: Object.freeze(resources) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function absoluteAnimationUrl(url: string): string {
|
|
73
|
+
try {
|
|
74
|
+
return new URL(url).href;
|
|
75
|
+
} catch {
|
|
76
|
+
const base = typeof globalThis.location === 'object' ? globalThis.location.href : '';
|
|
77
|
+
if (!base) return '';
|
|
78
|
+
try {
|
|
79
|
+
return new URL(url, base).href;
|
|
80
|
+
} catch {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { AnimationAudioComponent, AnimationNode } from '@haiyue/animation-spec';
|
|
2
|
+
|
|
3
|
+
/** HTML media-backed audio clip with composition-time seek and playback-rate synchronization. */
|
|
4
|
+
export class AnimationAudioClip {
|
|
5
|
+
private readonly element: HTMLAudioElement | null;
|
|
6
|
+
private readonly nodeStart: number;
|
|
7
|
+
private readonly nodeEnd: number;
|
|
8
|
+
private readonly startOffset: number;
|
|
9
|
+
private readonly baseVolume: number;
|
|
10
|
+
private readonly clipRate: number;
|
|
11
|
+
private readonly loop: boolean;
|
|
12
|
+
private playPending = false;
|
|
13
|
+
private destroyed = false;
|
|
14
|
+
private stateMachineActive = false;
|
|
15
|
+
private playRequestEpoch = 0;
|
|
16
|
+
|
|
17
|
+
constructor(uri: string, component: AnimationAudioComponent, node: Readonly<AnimationNode>, animationDuration: number) {
|
|
18
|
+
this.nodeStart = node.start ?? 0;
|
|
19
|
+
this.nodeEnd = this.nodeStart + (node.duration ?? animationDuration - this.nodeStart);
|
|
20
|
+
this.startOffset = component.startOffset ?? 0;
|
|
21
|
+
this.baseVolume = component.volume ?? 1;
|
|
22
|
+
this.clipRate = component.playbackRate ?? 1;
|
|
23
|
+
this.loop = component.loop ?? false;
|
|
24
|
+
this.element = typeof Audio === 'undefined' ? null : new Audio(uri);
|
|
25
|
+
if (this.element) {
|
|
26
|
+
this.element.preload = 'auto';
|
|
27
|
+
this.element.loop = this.loop;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
sync(compositionTime: number, playing: boolean, speed: number, opacity: number): void {
|
|
32
|
+
const element = this.element;
|
|
33
|
+
if (!element || this.destroyed) return;
|
|
34
|
+
const active = compositionTime >= this.nodeStart && compositionTime <= this.nodeEnd;
|
|
35
|
+
element.volume = clamp(this.baseVolume * opacity, 0, 1);
|
|
36
|
+
element.playbackRate = clamp(this.clipRate * speed, 0.0625, 16);
|
|
37
|
+
if (!active || !playing || speed <= 0) {
|
|
38
|
+
this._cancelPendingPlay();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let target = this.startOffset + (compositionTime - this.nodeStart) * this.clipRate;
|
|
42
|
+
if (this.loop && Number.isFinite(element.duration) && element.duration > 0) target %= element.duration;
|
|
43
|
+
if (Number.isFinite(target) && Math.abs(element.currentTime - target) > 0.12) {
|
|
44
|
+
try { element.currentTime = Math.max(0, target); } catch { /* Metadata may not be loaded yet. */ }
|
|
45
|
+
}
|
|
46
|
+
if (element.paused) this._requestPlay();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
enterStateMachine(
|
|
50
|
+
playing: boolean,
|
|
51
|
+
speed: number,
|
|
52
|
+
opacity: number,
|
|
53
|
+
rejected: (reason: unknown) => void,
|
|
54
|
+
): void {
|
|
55
|
+
const element = this.element;
|
|
56
|
+
if (!element || this.destroyed) return;
|
|
57
|
+
this.stateMachineActive = true;
|
|
58
|
+
element.volume = clamp(this.baseVolume * opacity, 0, 1);
|
|
59
|
+
element.playbackRate = clamp(this.clipRate * Math.max(speed, 0.0625), 0.0625, 16);
|
|
60
|
+
try { element.currentTime = Math.max(0, this.startOffset); } catch { /* Metadata may not be loaded yet. */ }
|
|
61
|
+
if (playing && speed > 0) this._requestPlay(rejected);
|
|
62
|
+
else this._cancelPendingPlay();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
restartStateMachine(
|
|
66
|
+
playing: boolean,
|
|
67
|
+
speed: number,
|
|
68
|
+
opacity: number,
|
|
69
|
+
rejected: (reason: unknown) => void,
|
|
70
|
+
): void {
|
|
71
|
+
this.enterStateMachine(playing, speed, opacity, rejected);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setStateMachinePlaying(
|
|
75
|
+
playing: boolean,
|
|
76
|
+
speed: number,
|
|
77
|
+
rejected: (reason: unknown) => void,
|
|
78
|
+
): void {
|
|
79
|
+
const element = this.element;
|
|
80
|
+
if (!element || this.destroyed || !this.stateMachineActive) return;
|
|
81
|
+
element.playbackRate = clamp(this.clipRate * Math.max(speed, 0.0625), 0.0625, 16);
|
|
82
|
+
if (playing && speed > 0) this._requestPlay(rejected);
|
|
83
|
+
else this._cancelPendingPlay();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
updateStateMachineProperties(speed: number, opacity: number): void {
|
|
87
|
+
const element = this.element;
|
|
88
|
+
if (!element || this.destroyed || !this.stateMachineActive) return;
|
|
89
|
+
element.volume = clamp(this.baseVolume * opacity, 0, 1);
|
|
90
|
+
element.playbackRate = clamp(this.clipRate * Math.max(speed, 0.0625), 0.0625, 16);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exitStateMachine(): void {
|
|
94
|
+
this.stateMachineActive = false;
|
|
95
|
+
this._cancelPendingPlay();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pause(): void { this._cancelPendingPlay(); }
|
|
99
|
+
|
|
100
|
+
destroy(): void {
|
|
101
|
+
if (this.destroyed) return;
|
|
102
|
+
this.destroyed = true;
|
|
103
|
+
this.stateMachineActive = false;
|
|
104
|
+
const element = this.element;
|
|
105
|
+
if (!element) return;
|
|
106
|
+
this._cancelPendingPlay();
|
|
107
|
+
element.removeAttribute('src');
|
|
108
|
+
element.load();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private _requestPlay(rejected?: (reason: unknown) => void): void {
|
|
112
|
+
const element = this.element;
|
|
113
|
+
if (!element || this.playPending || this.destroyed) return;
|
|
114
|
+
const epoch = ++this.playRequestEpoch;
|
|
115
|
+
const stateMachineRequest = rejected !== undefined;
|
|
116
|
+
this.playPending = true;
|
|
117
|
+
void element.play()
|
|
118
|
+
.then(() => {
|
|
119
|
+
if (epoch !== this.playRequestEpoch
|
|
120
|
+
|| this.destroyed
|
|
121
|
+
|| (stateMachineRequest && !this.stateMachineActive)) element.pause();
|
|
122
|
+
})
|
|
123
|
+
.catch(reason => {
|
|
124
|
+
if (epoch === this.playRequestEpoch) rejected?.(reason);
|
|
125
|
+
})
|
|
126
|
+
.finally(() => {
|
|
127
|
+
if (epoch === this.playRequestEpoch) this.playPending = false;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private _cancelPendingPlay(): void {
|
|
132
|
+
this.playRequestEpoch++;
|
|
133
|
+
this.playPending = false;
|
|
134
|
+
this.element?.pause();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
139
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
140
|
+
}
|