@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,2934 @@
|
|
|
1
|
+
import { hyaStateMachineChannelCapability, HYA_STATE_MACHINE_EXTENSION_ID, parseHyaStateMachineExtension, ANIMATION_VECTOR_SHAPE_EXTENSION_ID, parseAnimation } from '@haiyue/animation-spec';
|
|
2
|
+
export { HYA_STATE_MACHINE_CHANNEL_REGISTRY, hyaStateMachineChannelCapability } from '@haiyue/animation-spec';
|
|
3
|
+
import { Component, System } from '@haiyue/engine';
|
|
4
|
+
import { UniqueCheckType, ComponentLifecycleFlags } from '@haiyue/engine/ecs';
|
|
5
|
+
import { r as runAnimationStateMachineControllerUpdateTransaction } from './chunks/AnimationStateMachineController-BIcAh-sT.js';
|
|
6
|
+
import { A as Animation2DRuntime, c as createAnimation2DStateMachineController } from './chunks/Animation2DStateMachine-9IonguNa.js';
|
|
7
|
+
import '@haiyue/engine/geometry';
|
|
8
|
+
import '@haiyue/engine/components';
|
|
9
|
+
|
|
10
|
+
class Animation2DStateMachineActionRuntimeHandle {
|
|
11
|
+
duration;
|
|
12
|
+
clipId;
|
|
13
|
+
action;
|
|
14
|
+
targetTime = 0;
|
|
15
|
+
playing = false;
|
|
16
|
+
destroyed = false;
|
|
17
|
+
pendingDestroy = false;
|
|
18
|
+
_checkpoint;
|
|
19
|
+
_checkpointTargetTime = 0;
|
|
20
|
+
_checkpointPlaying = false;
|
|
21
|
+
constructor(clipId, action) {
|
|
22
|
+
this.duration = action.duration;
|
|
23
|
+
this.clipId = clipId;
|
|
24
|
+
this.action = action;
|
|
25
|
+
this._checkpoint = action.createSynchronizedState();
|
|
26
|
+
}
|
|
27
|
+
captureTransactionState() {
|
|
28
|
+
this._checkpointTargetTime = this.targetTime;
|
|
29
|
+
this._checkpointPlaying = this.playing;
|
|
30
|
+
this.action.captureSynchronizedState(this._checkpoint);
|
|
31
|
+
}
|
|
32
|
+
restoreTransactionState() {
|
|
33
|
+
this.targetTime = this._checkpointTargetTime;
|
|
34
|
+
this.playing = this._checkpointPlaying;
|
|
35
|
+
this.pendingDestroy = false;
|
|
36
|
+
this.action.restoreSynchronizedState(this._checkpoint);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Transactional state-machine owner for the existing 2D mixer. It never
|
|
41
|
+
* samples tracks itself: absolute controller playheads are synchronized into
|
|
42
|
+
* the mixer actions, and the caller evaluates the ordinary shared pose buffer.
|
|
43
|
+
*/
|
|
44
|
+
class Animation2DStateMachineMixerAdapter {
|
|
45
|
+
mixer;
|
|
46
|
+
_handles = new Set();
|
|
47
|
+
_transactionHandles = [];
|
|
48
|
+
_transactionCreated = [];
|
|
49
|
+
_transactionPendingDestroy = [];
|
|
50
|
+
_transactionActive = false;
|
|
51
|
+
_destroyed = false;
|
|
52
|
+
constructor(mixer) {
|
|
53
|
+
this.mixer = mixer;
|
|
54
|
+
}
|
|
55
|
+
get liveActionCount() { return this._handles.size; }
|
|
56
|
+
get liveBindingCount() { return this.mixer.liveBindingCount; }
|
|
57
|
+
createAction(clipId, options) {
|
|
58
|
+
this._requireActive();
|
|
59
|
+
const handle = new Animation2DStateMachineActionRuntimeHandle(clipId, this.mixer.createAction(clipId, options));
|
|
60
|
+
this._handles.add(handle);
|
|
61
|
+
if (this._transactionActive)
|
|
62
|
+
this._transactionCreated.push(handle);
|
|
63
|
+
return handle;
|
|
64
|
+
}
|
|
65
|
+
play(handle) {
|
|
66
|
+
this._requireHandle(handle);
|
|
67
|
+
this.mixer.play(handle.action);
|
|
68
|
+
handle.playing = true;
|
|
69
|
+
}
|
|
70
|
+
stop(handle) {
|
|
71
|
+
this._requireHandle(handle);
|
|
72
|
+
this.mixer.stop(handle.action);
|
|
73
|
+
handle.playing = false;
|
|
74
|
+
}
|
|
75
|
+
fade(handle, _targetWeight, _durationSeconds) {
|
|
76
|
+
this._requireHandle(handle);
|
|
77
|
+
// The controller is the unique transition clock and applies exact weights.
|
|
78
|
+
}
|
|
79
|
+
setWeight(handle, weight) {
|
|
80
|
+
this._requireHandle(handle);
|
|
81
|
+
this.mixer.setWeight(handle.action, weight);
|
|
82
|
+
}
|
|
83
|
+
setTime(handle, timeSeconds) {
|
|
84
|
+
this._requireHandle(handle);
|
|
85
|
+
handle.targetTime = timeSeconds;
|
|
86
|
+
this.mixer.setTime(handle.action, timeSeconds);
|
|
87
|
+
}
|
|
88
|
+
setTimeScale(handle, timeScale) {
|
|
89
|
+
this._requireHandle(handle);
|
|
90
|
+
this.mixer.setTimeScale(handle.action, timeScale);
|
|
91
|
+
}
|
|
92
|
+
destroyAction(handle) {
|
|
93
|
+
if (handle.destroyed)
|
|
94
|
+
return;
|
|
95
|
+
this._requireHandle(handle);
|
|
96
|
+
if (this._transactionActive) {
|
|
97
|
+
if (!handle.pendingDestroy) {
|
|
98
|
+
handle.pendingDestroy = true;
|
|
99
|
+
this._transactionPendingDestroy.push(handle);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this._destroyHandleNow(handle);
|
|
104
|
+
}
|
|
105
|
+
beginControllerTransaction() {
|
|
106
|
+
this._requireActive();
|
|
107
|
+
if (this._transactionActive) {
|
|
108
|
+
throw new Error('Animation2D state-machine adapter transaction is already active.');
|
|
109
|
+
}
|
|
110
|
+
this._transactionActive = true;
|
|
111
|
+
this._transactionHandles.length = 0;
|
|
112
|
+
this._transactionCreated.length = 0;
|
|
113
|
+
this._transactionPendingDestroy.length = 0;
|
|
114
|
+
for (const handle of this._handles) {
|
|
115
|
+
handle.captureTransactionState();
|
|
116
|
+
this._transactionHandles.push(handle);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
commitControllerTransaction() {
|
|
120
|
+
if (!this._transactionActive) {
|
|
121
|
+
throw new Error('Animation2D state-machine adapter has no active transaction.');
|
|
122
|
+
}
|
|
123
|
+
this._transactionActive = false;
|
|
124
|
+
for (const handle of this._transactionPendingDestroy)
|
|
125
|
+
this._destroyHandleNow(handle);
|
|
126
|
+
this._clearTransaction();
|
|
127
|
+
}
|
|
128
|
+
rollbackControllerTransaction() {
|
|
129
|
+
if (!this._transactionActive)
|
|
130
|
+
return;
|
|
131
|
+
this._transactionActive = false;
|
|
132
|
+
for (let index = this._transactionCreated.length - 1; index >= 0; index--) {
|
|
133
|
+
this._destroyHandleNow(this._transactionCreated[index]);
|
|
134
|
+
}
|
|
135
|
+
for (const handle of this._transactionHandles) {
|
|
136
|
+
if (!handle.destroyed)
|
|
137
|
+
handle.restoreTransactionState();
|
|
138
|
+
}
|
|
139
|
+
this._clearTransaction();
|
|
140
|
+
}
|
|
141
|
+
destroy() {
|
|
142
|
+
if (this._destroyed)
|
|
143
|
+
return;
|
|
144
|
+
this.rollbackControllerTransaction();
|
|
145
|
+
for (const handle of [...this._handles])
|
|
146
|
+
this._destroyHandleNow(handle);
|
|
147
|
+
this._destroyed = true;
|
|
148
|
+
}
|
|
149
|
+
_destroyHandleNow(handle) {
|
|
150
|
+
if (handle.destroyed)
|
|
151
|
+
return;
|
|
152
|
+
this.mixer.removeAction(handle.action);
|
|
153
|
+
handle.playing = false;
|
|
154
|
+
handle.pendingDestroy = false;
|
|
155
|
+
handle.destroyed = true;
|
|
156
|
+
this._handles.delete(handle);
|
|
157
|
+
}
|
|
158
|
+
_clearTransaction() {
|
|
159
|
+
this._transactionHandles.length = 0;
|
|
160
|
+
this._transactionCreated.length = 0;
|
|
161
|
+
this._transactionPendingDestroy.length = 0;
|
|
162
|
+
}
|
|
163
|
+
_requireHandle(handle) {
|
|
164
|
+
this._requireActive();
|
|
165
|
+
if (handle.destroyed || !this._handles.has(handle)) {
|
|
166
|
+
throw new Error('Animation2D state-machine action belongs to another adapter or was destroyed.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
_requireActive() {
|
|
170
|
+
if (this._destroyed)
|
|
171
|
+
throw new Error('Animation2D state-machine adapter has been destroyed.');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class AnimationStateMachineChannelError extends Error {
|
|
176
|
+
diagnostic;
|
|
177
|
+
name = 'AnimationStateMachineChannelError';
|
|
178
|
+
constructor(diagnostic) {
|
|
179
|
+
super(`${diagnostic.message} (${diagnostic.path})`);
|
|
180
|
+
this.diagnostic = diagnostic;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Audio owns one HTMLMediaElement playhead and is intentionally not amplitude
|
|
185
|
+
* mixed. It is executable only when the graph can never overlap two motions.
|
|
186
|
+
*/
|
|
187
|
+
function audioStateMachineCompatibilityDiagnostic(definition, path = '$.stateMachine') {
|
|
188
|
+
const capability = hyaStateMachineChannelCapability('audio');
|
|
189
|
+
if (definition.layers.length !== 1) {
|
|
190
|
+
return audioRangeDiagnostic(`${path}.layers`, 'Audio requires exactly one state-machine layer.');
|
|
191
|
+
}
|
|
192
|
+
const layer = definition.layers[0];
|
|
193
|
+
if ((layer.weight ?? 1) !== 1 || (layer.blendMode ?? 'override') !== 'override') {
|
|
194
|
+
return audioRangeDiagnostic(`${path}.layers[0]`, 'Audio requires one full-weight override layer because media amplitude is not pose-mixable.');
|
|
195
|
+
}
|
|
196
|
+
for (let stateIndex = 0; stateIndex < layer.states.length; stateIndex++) {
|
|
197
|
+
if (motionContainsBlendTree(layer.states[stateIndex].motion)) {
|
|
198
|
+
return audioRangeDiagnostic(`${path}.layers[0].states[${stateIndex}].motion`, 'Audio cannot run inside a Blend Tree because two media playheads would overlap.');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (let transitionIndex = 0; transitionIndex < layer.transitions.length; transitionIndex++) {
|
|
202
|
+
if (layer.transitions[transitionIndex].duration > 0) {
|
|
203
|
+
return audioRangeDiagnostic(`${path}.layers[0].transitions[${transitionIndex}].duration`, 'Audio transitions must be immediate; non-zero cross-fades are an unmixable media range.');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (capability.transition !== 'immediate-only') {
|
|
207
|
+
throw new Error('The canonical audio channel contract is internally inconsistent.');
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function assertAudioStateMachineCompatible(definition, path) {
|
|
212
|
+
const diagnostic = audioStateMachineCompatibilityDiagnostic(definition, path);
|
|
213
|
+
if (diagnostic)
|
|
214
|
+
throw new AnimationStateMachineChannelError(diagnostic);
|
|
215
|
+
}
|
|
216
|
+
function audioAutoplayRejectedDiagnostic(path, reason) {
|
|
217
|
+
const suffix = reason instanceof Error && reason.message
|
|
218
|
+
? ` Browser reason: ${reason.message}`
|
|
219
|
+
: '';
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
code: 'W_STATE_MACHINE_CHANNEL_AUDIO_AUTOPLAY_REJECTED',
|
|
222
|
+
severity: 'warning',
|
|
223
|
+
channelId: 'audio',
|
|
224
|
+
path,
|
|
225
|
+
message: `Audio play() was rejected; the playhead remains paused until a later user-authorized resume.${suffix}`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function audioRangeDiagnostic(path, message) {
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
code: 'E_STATE_MACHINE_CHANNEL_AUDIO_UNMIXABLE_RANGE',
|
|
231
|
+
severity: 'error',
|
|
232
|
+
channelId: 'audio',
|
|
233
|
+
path,
|
|
234
|
+
message,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function motionContainsBlendTree(motion) {
|
|
238
|
+
return motion.kind !== 'clip';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// HYA v2 keeps track times in Float32 while clip ranges remain JSON numbers.
|
|
242
|
+
// At ordinary multi-second timestamps the Float32 ULP is already larger than
|
|
243
|
+
// 1e-7, so boundary comparisons must absorb that representation difference.
|
|
244
|
+
const TIME_EPSILON = 1e-6;
|
|
245
|
+
/** Returns the canonical built-in state-machine payload from a parsed HYA document. */
|
|
246
|
+
function getHyaStateMachineExtension(animation) {
|
|
247
|
+
const value = animation.extensions[HYA_STATE_MACHINE_EXTENSION_ID];
|
|
248
|
+
if (value === undefined) {
|
|
249
|
+
throw new RangeError(`HYA document does not contain the built-in "${HYA_STATE_MACHINE_EXTENSION_ID}" extension.`);
|
|
250
|
+
}
|
|
251
|
+
return parseHyaStateMachineExtension(value, animation.duration);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Adapts named ranges from one HYA composition into source-independent 2D
|
|
255
|
+
* clips. The returned clips share no scene objects; one pose is applied to one
|
|
256
|
+
* Animation2DRuntime hierarchy by the state-machine runtime.
|
|
257
|
+
*/
|
|
258
|
+
function createHyaAnimation2DClips(animation, extension = getHyaStateMachineExtension(animation)) {
|
|
259
|
+
assertChannelCompatibleDocument(animation, extension);
|
|
260
|
+
const nodesById = new Map(animation.nodes.map(node => [node.id, node]));
|
|
261
|
+
return Object.freeze(extension.clips.map(range => createClip(animation, range, nodesById)));
|
|
262
|
+
}
|
|
263
|
+
function createClip(animation, range, nodesById) {
|
|
264
|
+
const runtimeDuration = Math.fround(range.duration);
|
|
265
|
+
const tracks = [];
|
|
266
|
+
const effects = [];
|
|
267
|
+
for (let index = 0; index < animation.tracks.length; index++) {
|
|
268
|
+
const source = animation.tracks[index];
|
|
269
|
+
const node = nodesById.get(source.node);
|
|
270
|
+
tracks.push(sliceNumericTrack(source, node, range, runtimeDuration, index));
|
|
271
|
+
}
|
|
272
|
+
for (let index = 0; index < animation.nodes.length; index++) {
|
|
273
|
+
const node = animation.nodes[index];
|
|
274
|
+
tracks.push(createVisibilityTrack(node, range, animation.duration, index));
|
|
275
|
+
appendComponentChannels(node, index, range, runtimeDuration, tracks, effects, animation.duration);
|
|
276
|
+
}
|
|
277
|
+
return Object.freeze({
|
|
278
|
+
format: 'haiyue-animation2d-clip@1',
|
|
279
|
+
id: range.id,
|
|
280
|
+
name: range.name ?? range.id,
|
|
281
|
+
duration: runtimeDuration,
|
|
282
|
+
tracks: Object.freeze(tracks),
|
|
283
|
+
...(effects.length === 0 ? {} : { effects: Object.freeze(effects) }),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function appendComponentChannels(node, nodeIndex, range, runtimeDuration, tracks, effects, compositionDuration) {
|
|
287
|
+
let particleSlot = 0;
|
|
288
|
+
let audioSlot = 0;
|
|
289
|
+
const components = node.components ?? [];
|
|
290
|
+
for (let componentIndex = 0; componentIndex < components.length; componentIndex++) {
|
|
291
|
+
const component = components[componentIndex];
|
|
292
|
+
const path = `$.nodes[${nodeIndex}].components[${componentIndex}]`;
|
|
293
|
+
if (component.type === 'sprite2d') {
|
|
294
|
+
const sprite = component;
|
|
295
|
+
if (!sprite.uvRectTrack)
|
|
296
|
+
continue;
|
|
297
|
+
tracks.push(sliceStepVectorTrack(sprite.uvRectTrack, range, runtimeDuration, `${range.id}:component:${nodeIndex}:${componentIndex}:sprite-uv`, Object.freeze({
|
|
298
|
+
id: `${node.id}.components.${componentIndex}.sprite.uv-rect`,
|
|
299
|
+
targetId: node.id,
|
|
300
|
+
path: `components.${componentIndex}.sprite.uv-rect`,
|
|
301
|
+
strategy: 'discrete',
|
|
302
|
+
defaultValue: Object.freeze([...(sprite.uvRect ?? [0, 0, 1, 1])]),
|
|
303
|
+
})));
|
|
304
|
+
}
|
|
305
|
+
else if (component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID) {
|
|
306
|
+
const vector = component;
|
|
307
|
+
if (!vector.morph)
|
|
308
|
+
continue;
|
|
309
|
+
tracks.push(sliceComponentNumericTrack(vector.morph, range, runtimeDuration, `${range.id}:component:${nodeIndex}:${componentIndex}:vector-morph`, Object.freeze({
|
|
310
|
+
id: `${node.id}.components.${componentIndex}.vector.morph`,
|
|
311
|
+
targetId: node.id,
|
|
312
|
+
path: `components.${componentIndex}.vector.morph`,
|
|
313
|
+
strategy: 'continuous',
|
|
314
|
+
valueSize: vector.morph.valueSize,
|
|
315
|
+
defaultValue: vector.morphRelative
|
|
316
|
+
? new Float32Array(vector.morph.valueSize)
|
|
317
|
+
: Object.freeze(Array.from(vector.values)),
|
|
318
|
+
}), `${path}.morph`));
|
|
319
|
+
}
|
|
320
|
+
else if (component.type === 'org.haiyue.vector-path-morph@1') {
|
|
321
|
+
const morph = component;
|
|
322
|
+
tracks.push(sliceComponentNumericTrack(morph, range, runtimeDuration, `${range.id}:component:${nodeIndex}:${componentIndex}:path-morph`, Object.freeze({
|
|
323
|
+
id: `${node.id}.components.${componentIndex}.path.morph`,
|
|
324
|
+
targetId: node.id,
|
|
325
|
+
path: `components.${componentIndex}.path.morph`,
|
|
326
|
+
strategy: 'continuous',
|
|
327
|
+
valueSize: morph.valueSize,
|
|
328
|
+
defaultValue: Object.freeze(Array.from(morph.values).slice(0, morph.valueSize)),
|
|
329
|
+
}), path));
|
|
330
|
+
}
|
|
331
|
+
if (component.type === 'particle2d') {
|
|
332
|
+
appendEffectCue('particle', component, particleSlot++, node, path, range, effects, compositionDuration);
|
|
333
|
+
}
|
|
334
|
+
else if (component.type === 'audio') {
|
|
335
|
+
appendEffectCue('audio', component, audioSlot++, node, path, range, effects, compositionDuration);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function appendEffectCue(kind, component, slot, node, sourcePath, range, effects, compositionDuration) {
|
|
340
|
+
const nodeStart = node.start ?? 0;
|
|
341
|
+
const nodeEnd = nodeStart + (node.duration ?? compositionDuration - nodeStart);
|
|
342
|
+
const start = Math.max(nodeStart, range.start);
|
|
343
|
+
const end = Math.min(nodeEnd, range.start + range.duration);
|
|
344
|
+
if (end <= start + TIME_EPSILON)
|
|
345
|
+
return;
|
|
346
|
+
const payload = Object.freeze({
|
|
347
|
+
targetId: node.id,
|
|
348
|
+
slot,
|
|
349
|
+
sourcePath,
|
|
350
|
+
component,
|
|
351
|
+
});
|
|
352
|
+
effects.push(Object.freeze({
|
|
353
|
+
id: `${range.id}:${kind}:${node.id}:${slot}`,
|
|
354
|
+
kind,
|
|
355
|
+
start: Math.max(0, Math.fround(start - range.start)),
|
|
356
|
+
end: Math.min(Math.fround(range.duration), Math.fround(end - range.start)),
|
|
357
|
+
loopBehavior: kind === 'particle' ? 'restart' : 'continue',
|
|
358
|
+
payload,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
function sliceNumericTrack(source, node, range, runtimeDuration, sourceIndex) {
|
|
362
|
+
const globalTimes = collectTrackTimes(source, range);
|
|
363
|
+
const times = new Float32Array(globalTimes.length);
|
|
364
|
+
const values = new Float32Array(globalTimes.length * source.valueSize);
|
|
365
|
+
for (let key = 0; key < globalTimes.length; key++) {
|
|
366
|
+
times[key] = key === 0
|
|
367
|
+
? 0
|
|
368
|
+
: key === globalTimes.length - 1
|
|
369
|
+
? runtimeDuration
|
|
370
|
+
: Math.min(runtimeDuration, Math.max(0, Math.fround(globalTimes[key] - range.start)));
|
|
371
|
+
sampleTrack(source, globalTimes[key], values, key * source.valueSize);
|
|
372
|
+
}
|
|
373
|
+
assertStrictFloatTimes(times, `${range.id}:${source.node}:${source.property}`);
|
|
374
|
+
const binding = createNumericBinding(source, node);
|
|
375
|
+
const interpolation = source.times.length < 2 ? 'step' : source.interpolation;
|
|
376
|
+
const easings = interpolation === 'cubic-bezier'
|
|
377
|
+
? sliceEasings(source, globalTimes)
|
|
378
|
+
: undefined;
|
|
379
|
+
const spatialTangents = source.spatialTangents
|
|
380
|
+
? sliceSpatialTangents(source, globalTimes)
|
|
381
|
+
: undefined;
|
|
382
|
+
return Object.freeze({
|
|
383
|
+
id: `${range.id}:track:${sourceIndex}`,
|
|
384
|
+
binding,
|
|
385
|
+
interpolation,
|
|
386
|
+
times,
|
|
387
|
+
values,
|
|
388
|
+
...(easings ? { easings } : {}),
|
|
389
|
+
...(spatialTangents ? { spatialTangents } : {}),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function createNumericBinding(track, node) {
|
|
393
|
+
const transform = node.transform;
|
|
394
|
+
const path = track.property === 'opacity'
|
|
395
|
+
? 'opacity'
|
|
396
|
+
: `transform.${track.property}`;
|
|
397
|
+
const defaultValue = track.property === 'position'
|
|
398
|
+
? transform?.position ?? [0, 0]
|
|
399
|
+
: track.property === 'scale'
|
|
400
|
+
? transform?.scale ?? [1, 1]
|
|
401
|
+
: track.property === 'rotation'
|
|
402
|
+
? [transform?.rotation ?? 0]
|
|
403
|
+
: [transform?.opacity ?? 1];
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
id: `${node.id}.${path}`,
|
|
406
|
+
targetId: node.id,
|
|
407
|
+
path,
|
|
408
|
+
strategy: track.property === 'rotation' ? 'rotation' : 'continuous',
|
|
409
|
+
valueSize: track.valueSize,
|
|
410
|
+
defaultValue: Object.freeze(Array.from(defaultValue)),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function sliceComponentNumericTrack(source, range, runtimeDuration, id, binding, debugPath) {
|
|
414
|
+
const track = vectorTrackAsParsedTrack(source, debugPath);
|
|
415
|
+
const globalTimes = collectTrackTimes(track, range);
|
|
416
|
+
const times = new Float32Array(globalTimes.length);
|
|
417
|
+
const values = new Float32Array(globalTimes.length * source.valueSize);
|
|
418
|
+
for (let key = 0; key < globalTimes.length; key++) {
|
|
419
|
+
times[key] = key === 0
|
|
420
|
+
? 0
|
|
421
|
+
: key === globalTimes.length - 1
|
|
422
|
+
? runtimeDuration
|
|
423
|
+
: Math.min(runtimeDuration, Math.max(0, Math.fround(globalTimes[key] - range.start)));
|
|
424
|
+
sampleTrack(track, globalTimes[key], values, key * source.valueSize);
|
|
425
|
+
}
|
|
426
|
+
assertStrictFloatTimes(times, id);
|
|
427
|
+
const interpolation = source.times.length < 2 ? 'step' : source.interpolation;
|
|
428
|
+
const easings = interpolation === 'cubic-bezier'
|
|
429
|
+
? sliceEasings(track, globalTimes)
|
|
430
|
+
: undefined;
|
|
431
|
+
return Object.freeze({
|
|
432
|
+
id,
|
|
433
|
+
binding,
|
|
434
|
+
interpolation,
|
|
435
|
+
times,
|
|
436
|
+
values,
|
|
437
|
+
...(easings ? { easings } : {}),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function sliceStepVectorTrack(source, range, runtimeDuration, id, binding) {
|
|
441
|
+
const globalTimes = collectVectorTrackTimes(source, range);
|
|
442
|
+
const times = new Float32Array(globalTimes.length);
|
|
443
|
+
const values = [];
|
|
444
|
+
for (let key = 0; key < globalTimes.length; key++) {
|
|
445
|
+
times[key] = key === 0
|
|
446
|
+
? 0
|
|
447
|
+
: key === globalTimes.length - 1
|
|
448
|
+
? runtimeDuration
|
|
449
|
+
: Math.min(runtimeDuration, Math.max(0, Math.fround(globalTimes[key] - range.start)));
|
|
450
|
+
const frame = findArraySegment(source.times, globalTimes[key]);
|
|
451
|
+
const offset = frame * source.valueSize;
|
|
452
|
+
const sampled = new Float32Array(source.valueSize);
|
|
453
|
+
for (let component = 0; component < source.valueSize; component++) {
|
|
454
|
+
sampled[component] = source.values[offset + component] ?? 0;
|
|
455
|
+
}
|
|
456
|
+
values.push(sampled);
|
|
457
|
+
}
|
|
458
|
+
assertStrictFloatTimes(times, id);
|
|
459
|
+
return Object.freeze({
|
|
460
|
+
id,
|
|
461
|
+
binding,
|
|
462
|
+
interpolation: 'step',
|
|
463
|
+
times,
|
|
464
|
+
values: Object.freeze(values),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function collectVectorTrackTimes(track, range) {
|
|
468
|
+
const end = range.start + range.duration;
|
|
469
|
+
const result = [range.start];
|
|
470
|
+
for (let index = 0; index < track.times.length; index++) {
|
|
471
|
+
const time = track.times[index];
|
|
472
|
+
if (time > range.start + TIME_EPSILON && time < end - TIME_EPSILON)
|
|
473
|
+
result.push(time);
|
|
474
|
+
}
|
|
475
|
+
result.push(end);
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
function vectorTrackAsParsedTrack(source, debugPath) {
|
|
479
|
+
return {
|
|
480
|
+
node: debugPath,
|
|
481
|
+
property: 'opacity',
|
|
482
|
+
interpolation: source.interpolation,
|
|
483
|
+
times: source.times instanceof Float32Array ? source.times : Float32Array.from(source.times),
|
|
484
|
+
values: source.values instanceof Float32Array ? source.values : Float32Array.from(source.values),
|
|
485
|
+
valueSize: source.valueSize,
|
|
486
|
+
...(source.easings === undefined ? {} : {
|
|
487
|
+
easings: source.easings instanceof Float32Array
|
|
488
|
+
? source.easings
|
|
489
|
+
: Float32Array.from(source.easings),
|
|
490
|
+
}),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function findArraySegment(times, time) {
|
|
494
|
+
if (times.length <= 1 || time <= times[0])
|
|
495
|
+
return 0;
|
|
496
|
+
if (time >= times[times.length - 1])
|
|
497
|
+
return times.length - 1;
|
|
498
|
+
let low = 0;
|
|
499
|
+
let high = times.length - 1;
|
|
500
|
+
while (low + 1 < high) {
|
|
501
|
+
const middle = (low + high) >>> 1;
|
|
502
|
+
if (times[middle] <= time)
|
|
503
|
+
low = middle;
|
|
504
|
+
else
|
|
505
|
+
high = middle;
|
|
506
|
+
}
|
|
507
|
+
return low;
|
|
508
|
+
}
|
|
509
|
+
function createVisibilityTrack(node, range, compositionDuration, nodeIndex) {
|
|
510
|
+
const nodeStart = node.start ?? 0;
|
|
511
|
+
const nodeEnd = nodeStart + (node.duration ?? compositionDuration - nodeStart);
|
|
512
|
+
const globalTimes = [range.start];
|
|
513
|
+
if (nodeStart > range.start + TIME_EPSILON && nodeStart < range.start + range.duration - TIME_EPSILON) {
|
|
514
|
+
globalTimes.push(nodeStart);
|
|
515
|
+
}
|
|
516
|
+
if (nodeEnd > range.start + TIME_EPSILON && nodeEnd < range.start + range.duration - TIME_EPSILON) {
|
|
517
|
+
globalTimes.push(nodeEnd);
|
|
518
|
+
}
|
|
519
|
+
globalTimes.sort((left, right) => left - right);
|
|
520
|
+
const times = new Float32Array(globalTimes.length);
|
|
521
|
+
const values = new Array(globalTimes.length);
|
|
522
|
+
for (let index = 0; index < globalTimes.length; index++) {
|
|
523
|
+
const globalTime = globalTimes[index];
|
|
524
|
+
times[index] = Math.fround(globalTime - range.start);
|
|
525
|
+
values[index] = globalTime >= nodeStart - TIME_EPSILON && globalTime < nodeEnd - TIME_EPSILON;
|
|
526
|
+
}
|
|
527
|
+
assertStrictFloatTimes(times, `${range.id}:${node.id}:visibility`);
|
|
528
|
+
return Object.freeze({
|
|
529
|
+
id: `${range.id}:visibility:${nodeIndex}`,
|
|
530
|
+
binding: Object.freeze({
|
|
531
|
+
id: `${node.id}.visibility`,
|
|
532
|
+
targetId: node.id,
|
|
533
|
+
path: 'visibility',
|
|
534
|
+
strategy: 'discrete',
|
|
535
|
+
defaultValue: true,
|
|
536
|
+
}),
|
|
537
|
+
interpolation: 'step',
|
|
538
|
+
times,
|
|
539
|
+
values: Object.freeze(values),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
function collectTrackTimes(track, range) {
|
|
543
|
+
const end = range.start + range.duration;
|
|
544
|
+
const result = [range.start];
|
|
545
|
+
for (let index = 0; index < track.times.length; index++) {
|
|
546
|
+
const time = track.times[index];
|
|
547
|
+
if (time > range.start + TIME_EPSILON && time < end - TIME_EPSILON)
|
|
548
|
+
result.push(time);
|
|
549
|
+
}
|
|
550
|
+
result.push(end);
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
function sampleTrack(track, time, output, outputOffset) {
|
|
554
|
+
const segment = findSegment(track.times, time);
|
|
555
|
+
const startOffset = segment * track.valueSize;
|
|
556
|
+
const endOffset = Math.min(segment + 1, track.times.length - 1) * track.valueSize;
|
|
557
|
+
let progress = 0;
|
|
558
|
+
if (segment < track.times.length - 1 && track.interpolation !== 'step') {
|
|
559
|
+
const start = track.times[segment];
|
|
560
|
+
const end = track.times[segment + 1];
|
|
561
|
+
const linear = Math.min(1, Math.max(0, (time - start) / Math.max(TIME_EPSILON, end - start)));
|
|
562
|
+
progress = track.interpolation === 'cubic-bezier'
|
|
563
|
+
? cubicBezierYForX(track.easings, segment, linear)
|
|
564
|
+
: linear;
|
|
565
|
+
}
|
|
566
|
+
if (track.spatialTangents && segment < track.times.length - 1) {
|
|
567
|
+
const tangentOffset = segment * 4;
|
|
568
|
+
for (let component = 0; component < 2; component++) {
|
|
569
|
+
const start = track.values[startOffset + component];
|
|
570
|
+
const end = track.values[endOffset + component];
|
|
571
|
+
output[outputOffset + component] = spatialCoordinate(progress, start, start + track.spatialTangents[tangentOffset + component], end + track.spatialTangents[tangentOffset + 2 + component], end);
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
for (let component = 0; component < track.valueSize; component++) {
|
|
576
|
+
const start = track.values[startOffset + component];
|
|
577
|
+
output[outputOffset + component] = start
|
|
578
|
+
+ (track.values[endOffset + component] - start) * progress;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function sliceSpatialTangents(track, globalTimes) {
|
|
582
|
+
const result = new Float32Array(Math.max(0, globalTimes.length - 1) * 4);
|
|
583
|
+
for (let interval = 0; interval < globalTimes.length - 1; interval++) {
|
|
584
|
+
const startTime = globalTimes[interval];
|
|
585
|
+
const endTime = globalTimes[interval + 1];
|
|
586
|
+
const midpoint = (startTime + endTime) * 0.5;
|
|
587
|
+
const sourceSegment = findSegment(track.times, midpoint);
|
|
588
|
+
if (sourceSegment >= track.times.length - 1)
|
|
589
|
+
continue;
|
|
590
|
+
const sourceStartTime = track.times[sourceSegment];
|
|
591
|
+
const sourceEndTime = track.times[sourceSegment + 1];
|
|
592
|
+
if (endTime <= sourceStartTime + TIME_EPSILON || startTime >= sourceEndTime - TIME_EPSILON)
|
|
593
|
+
continue;
|
|
594
|
+
const linearStart = clamp01((startTime - sourceStartTime) / (sourceEndTime - sourceStartTime));
|
|
595
|
+
const linearEnd = clamp01((endTime - sourceStartTime) / (sourceEndTime - sourceStartTime));
|
|
596
|
+
const progressStart = track.interpolation === 'cubic-bezier'
|
|
597
|
+
? cubicBezierYForX(track.easings, sourceSegment, linearStart)
|
|
598
|
+
: linearStart;
|
|
599
|
+
const progressEnd = track.interpolation === 'cubic-bezier'
|
|
600
|
+
? cubicBezierYForX(track.easings, sourceSegment, linearEnd)
|
|
601
|
+
: linearEnd;
|
|
602
|
+
const valueOffset = sourceSegment * 2;
|
|
603
|
+
const tangentOffset = sourceSegment * 4;
|
|
604
|
+
const delta = progressEnd - progressStart;
|
|
605
|
+
for (let component = 0; component < 2; component++) {
|
|
606
|
+
const start = track.values[valueOffset + component];
|
|
607
|
+
const end = track.values[valueOffset + 2 + component];
|
|
608
|
+
const control1 = start + track.spatialTangents[tangentOffset + component];
|
|
609
|
+
const control2 = end + track.spatialTangents[tangentOffset + 2 + component];
|
|
610
|
+
const derivativeStart = spatialDerivative(progressStart, start, control1, control2, end);
|
|
611
|
+
const derivativeEnd = spatialDerivative(progressEnd, start, control1, control2, end);
|
|
612
|
+
result[interval * 4 + component] = derivativeStart * delta / 3;
|
|
613
|
+
result[interval * 4 + 2 + component] = -derivativeEnd * delta / 3;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
function spatialCoordinate(value, start, first, second, end) {
|
|
619
|
+
const inverse = 1 - value;
|
|
620
|
+
return inverse * inverse * inverse * start
|
|
621
|
+
+ 3 * inverse * inverse * value * first
|
|
622
|
+
+ 3 * inverse * value * value * second
|
|
623
|
+
+ value * value * value * end;
|
|
624
|
+
}
|
|
625
|
+
function spatialDerivative(value, start, first, second, end) {
|
|
626
|
+
const inverse = 1 - value;
|
|
627
|
+
return 3 * inverse * inverse * (first - start)
|
|
628
|
+
+ 6 * inverse * value * (second - first)
|
|
629
|
+
+ 3 * value * value * (end - second);
|
|
630
|
+
}
|
|
631
|
+
function sliceEasings(track, globalTimes) {
|
|
632
|
+
const result = new Float32Array(Math.max(0, globalTimes.length - 1) * 4);
|
|
633
|
+
for (let interval = 0; interval < globalTimes.length - 1; interval++) {
|
|
634
|
+
const startTime = globalTimes[interval];
|
|
635
|
+
const endTime = globalTimes[interval + 1];
|
|
636
|
+
const firstSourceTime = track.times[0];
|
|
637
|
+
const lastSourceTime = track.times[track.times.length - 1];
|
|
638
|
+
if (track.times.length < 2
|
|
639
|
+
|| endTime <= firstSourceTime + TIME_EPSILON
|
|
640
|
+
|| startTime >= lastSourceTime - TIME_EPSILON) {
|
|
641
|
+
result.set([1 / 3, 1 / 3, 2 / 3, 2 / 3], interval * 4);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const sourceSegment = findSegment(track.times, (startTime + endTime) * 0.5);
|
|
645
|
+
const sourceStart = track.times[sourceSegment];
|
|
646
|
+
const sourceEnd = track.times[sourceSegment + 1];
|
|
647
|
+
const duration = sourceEnd - sourceStart;
|
|
648
|
+
const xStart = Math.min(1, Math.max(0, (startTime - sourceStart) / duration));
|
|
649
|
+
const xEnd = Math.min(1, Math.max(0, (endTime - sourceStart) / duration));
|
|
650
|
+
const easingOffset = sourceSegment * 4;
|
|
651
|
+
const subcurve = sliceCubicBezier(track.easings[easingOffset], track.easings[easingOffset + 1], track.easings[easingOffset + 2], track.easings[easingOffset + 3], xStart, xEnd, track, sourceSegment);
|
|
652
|
+
result.set(subcurve, interval * 4);
|
|
653
|
+
}
|
|
654
|
+
return result;
|
|
655
|
+
}
|
|
656
|
+
function sliceCubicBezier(x1, y1, x2, y2, xStart, xEnd, track, sourceSegment) {
|
|
657
|
+
if (xStart <= TIME_EPSILON && xEnd >= 1 - TIME_EPSILON)
|
|
658
|
+
return [x1, y1, x2, y2];
|
|
659
|
+
const t0 = solveCubicX(xStart, x1, x2);
|
|
660
|
+
const t1 = solveCubicX(xEnd, x1, x2);
|
|
661
|
+
const start = cubicPoint(t0, x1, y1, x2, y2);
|
|
662
|
+
const end = cubicPoint(t1, x1, y1, x2, y2);
|
|
663
|
+
const delta = t1 - t0;
|
|
664
|
+
const startDerivative = cubicPointDerivative(t0, x1, y1, x2, y2);
|
|
665
|
+
const endDerivative = cubicPointDerivative(t1, x1, y1, x2, y2);
|
|
666
|
+
const control1 = [start[0] + startDerivative[0] * delta / 3, start[1] + startDerivative[1] * delta / 3];
|
|
667
|
+
const control2 = [end[0] - endDerivative[0] * delta / 3, end[1] - endDerivative[1] * delta / 3];
|
|
668
|
+
const width = end[0] - start[0];
|
|
669
|
+
const height = end[1] - start[1];
|
|
670
|
+
if (Math.abs(height) <= 1e-8) {
|
|
671
|
+
if (sourceSegmentHasMotion(track, sourceSegment)) {
|
|
672
|
+
throw new RangeError(`HYA cubic track "${track.node}.${track.property}" cannot be range-sliced at a zero-progress easing interval without losing fidelity.`);
|
|
673
|
+
}
|
|
674
|
+
return [1 / 3, 1 / 3, 2 / 3, 2 / 3];
|
|
675
|
+
}
|
|
676
|
+
return [
|
|
677
|
+
clamp01((control1[0] - start[0]) / width),
|
|
678
|
+
(control1[1] - start[1]) / height,
|
|
679
|
+
clamp01((control2[0] - start[0]) / width),
|
|
680
|
+
(control2[1] - start[1]) / height,
|
|
681
|
+
];
|
|
682
|
+
}
|
|
683
|
+
function sourceSegmentHasMotion(track, segment) {
|
|
684
|
+
const start = segment * track.valueSize;
|
|
685
|
+
const end = (segment + 1) * track.valueSize;
|
|
686
|
+
for (let component = 0; component < track.valueSize; component++) {
|
|
687
|
+
if (Math.abs(track.values[start + component] - track.values[end + component]) > 1e-8)
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
function findSegment(times, time) {
|
|
693
|
+
if (times.length <= 1 || time <= times[0])
|
|
694
|
+
return 0;
|
|
695
|
+
if (time >= times[times.length - 1])
|
|
696
|
+
return times.length - 1;
|
|
697
|
+
let low = 0;
|
|
698
|
+
let high = times.length - 1;
|
|
699
|
+
while (low + 1 < high) {
|
|
700
|
+
const middle = (low + high) >>> 1;
|
|
701
|
+
if (times[middle] <= time)
|
|
702
|
+
low = middle;
|
|
703
|
+
else
|
|
704
|
+
high = middle;
|
|
705
|
+
}
|
|
706
|
+
return low;
|
|
707
|
+
}
|
|
708
|
+
function cubicBezierYForX(easings, segment, x) {
|
|
709
|
+
const offset = segment * 4;
|
|
710
|
+
const x1 = easings[offset];
|
|
711
|
+
const y1 = easings[offset + 1];
|
|
712
|
+
const x2 = easings[offset + 2];
|
|
713
|
+
const y2 = easings[offset + 3];
|
|
714
|
+
return cubicCoordinate$1(solveCubicX(x, x1, x2), y1, y2);
|
|
715
|
+
}
|
|
716
|
+
function solveCubicX(x, x1, x2) {
|
|
717
|
+
let parameter = x;
|
|
718
|
+
for (let iteration = 0; iteration < 6; iteration++) {
|
|
719
|
+
const estimate = cubicCoordinate$1(parameter, x1, x2) - x;
|
|
720
|
+
const derivative = cubicDerivative$1(parameter, x1, x2);
|
|
721
|
+
if (Math.abs(derivative) < 1e-7)
|
|
722
|
+
break;
|
|
723
|
+
parameter = clamp01(parameter - estimate / derivative);
|
|
724
|
+
}
|
|
725
|
+
let low = 0;
|
|
726
|
+
let high = 1;
|
|
727
|
+
for (let iteration = 0; iteration < 18; iteration++) {
|
|
728
|
+
const estimate = cubicCoordinate$1(parameter, x1, x2);
|
|
729
|
+
if (Math.abs(estimate - x) < 1e-8)
|
|
730
|
+
break;
|
|
731
|
+
if (estimate < x)
|
|
732
|
+
low = parameter;
|
|
733
|
+
else
|
|
734
|
+
high = parameter;
|
|
735
|
+
parameter = (low + high) * 0.5;
|
|
736
|
+
}
|
|
737
|
+
return parameter;
|
|
738
|
+
}
|
|
739
|
+
function cubicPoint(value, x1, y1, x2, y2) {
|
|
740
|
+
return [cubicCoordinate$1(value, x1, x2), cubicCoordinate$1(value, y1, y2)];
|
|
741
|
+
}
|
|
742
|
+
function cubicPointDerivative(value, x1, y1, x2, y2) {
|
|
743
|
+
return [cubicDerivative$1(value, x1, x2), cubicDerivative$1(value, y1, y2)];
|
|
744
|
+
}
|
|
745
|
+
function cubicCoordinate$1(value, first, second) {
|
|
746
|
+
const inverse = 1 - value;
|
|
747
|
+
return 3 * inverse * inverse * value * first
|
|
748
|
+
+ 3 * inverse * value * value * second
|
|
749
|
+
+ value * value * value;
|
|
750
|
+
}
|
|
751
|
+
function cubicDerivative$1(value, first, second) {
|
|
752
|
+
const inverse = 1 - value;
|
|
753
|
+
return 3 * inverse * inverse * first
|
|
754
|
+
+ 6 * inverse * value * (second - first)
|
|
755
|
+
+ 3 * value * value * (1 - second);
|
|
756
|
+
}
|
|
757
|
+
function assertChannelCompatibleDocument(animation, extension) {
|
|
758
|
+
let hasAudio = false;
|
|
759
|
+
for (let nodeIndex = 0; nodeIndex < animation.nodes.length; nodeIndex++) {
|
|
760
|
+
const node = animation.nodes[nodeIndex];
|
|
761
|
+
const components = node.components ?? [];
|
|
762
|
+
for (let componentIndex = 0; componentIndex < components.length; componentIndex++) {
|
|
763
|
+
const component = components[componentIndex];
|
|
764
|
+
const path = `$.nodes[${nodeIndex}].components[${componentIndex}]`;
|
|
765
|
+
if (component.type === 'audio')
|
|
766
|
+
hasAudio = true;
|
|
767
|
+
const unsupported = unsupportedInlineChannel(component, path);
|
|
768
|
+
if (unsupported)
|
|
769
|
+
throw new AnimationStateMachineChannelError(unsupported);
|
|
770
|
+
}
|
|
771
|
+
for (let effectIndex = 0; effectIndex < (node.effects?.length ?? 0); effectIndex++) {
|
|
772
|
+
const effect = node.effects[effectIndex];
|
|
773
|
+
if (Object.keys(effect).some(key => key.endsWith('Track'))) {
|
|
774
|
+
throw new AnimationStateMachineChannelError(unsupportedDiagnostic('visual-effect', `$.nodes[${nodeIndex}].effects[${effectIndex}]`, 'Animated visual-effect properties are not yet writable from the shared pose buffer.'));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const composites = node.composite
|
|
778
|
+
? ('layers' in node.composite ? node.composite.layers : [node.composite])
|
|
779
|
+
: [];
|
|
780
|
+
for (let compositeIndex = 0; compositeIndex < composites.length; compositeIndex++) {
|
|
781
|
+
if (composites[compositeIndex].expansionTrack) {
|
|
782
|
+
throw new AnimationStateMachineChannelError(unsupportedDiagnostic('composite-expansion', `$.nodes[${nodeIndex}].composite.layers[${compositeIndex}].expansionTrack`, 'Animated composite expansion is not yet writable from the shared pose buffer.'));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (hasAudio) {
|
|
787
|
+
assertAudioStateMachineCompatible(extension.stateMachine, `$.extensions.${HYA_STATE_MACHINE_EXTENSION_ID}.stateMachine`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function unsupportedInlineChannel(component, path) {
|
|
791
|
+
if (component.type === 'shape2d'
|
|
792
|
+
|| component.type === 'path2d'
|
|
793
|
+
|| component.type === 'sprite2d'
|
|
794
|
+
|| component.type === 'particle2d'
|
|
795
|
+
|| component.type === 'audio'
|
|
796
|
+
|| component.type === 'org.haiyue.vector-stroke@1'
|
|
797
|
+
|| component.type === 'org.haiyue.vector-path-morph@1')
|
|
798
|
+
return null;
|
|
799
|
+
if (component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID) {
|
|
800
|
+
const vector = component;
|
|
801
|
+
if (vectorHasUnsupportedInlineTrack(vector)) {
|
|
802
|
+
return unsupportedDiagnostic('vector-paint', path, 'Vector paint and modifier tracks are not yet writable from the shared pose buffer; vector morph is supported.');
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
if (component.type === 'text2d') {
|
|
807
|
+
const text = component;
|
|
808
|
+
const documents = text.documents;
|
|
809
|
+
const animators = text.animators;
|
|
810
|
+
if ((documents?.length ?? 0) > 1 || animators?.some(recordHasNestedTrack)) {
|
|
811
|
+
return unsupportedDiagnostic('text-animator', path, 'Animated text documents and animator tracks are not yet writable from the shared pose buffer.');
|
|
812
|
+
}
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
return unsupportedDiagnostic('visual-effect', path, `Component "${component.type}" has no registered state-machine channel strategy.`);
|
|
816
|
+
}
|
|
817
|
+
function vectorHasUnsupportedInlineTrack(vector) {
|
|
818
|
+
const fill = vector.fill;
|
|
819
|
+
if (fill) {
|
|
820
|
+
if (fill.kind === 'solid') {
|
|
821
|
+
if (fill.colorTrack || fill.opacityTrack)
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
else if (fill.startTrack || fill.endTrack || fill.stopsTrack || fill.opacityTrack)
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
const stroke = vector.stroke;
|
|
828
|
+
if (stroke && (stroke.colorTrack || stroke.opacityTrack || stroke.widthTrack || stroke.dashOffsetTrack
|
|
829
|
+
|| (stroke.gradient && (stroke.gradient.startTrack || stroke.gradient.endTrack
|
|
830
|
+
|| stroke.gradient.stopsTrack || stroke.gradient.opacityTrack))))
|
|
831
|
+
return true;
|
|
832
|
+
return vector.modifiers?.some(modifier => modifier.kind === 'round-corners'
|
|
833
|
+
? modifier.radiusTrack !== undefined
|
|
834
|
+
: modifier.startTrack !== undefined || modifier.endTrack !== undefined
|
|
835
|
+
|| modifier.offsetTrack !== undefined) ?? false;
|
|
836
|
+
}
|
|
837
|
+
function recordHasNestedTrack(record) {
|
|
838
|
+
if (Object.keys(record).some(key => key.endsWith('Track')))
|
|
839
|
+
return true;
|
|
840
|
+
return Object.values(record).some(value => value !== null && typeof value === 'object'
|
|
841
|
+
&& !Array.isArray(value)
|
|
842
|
+
&& recordHasNestedTrack(value));
|
|
843
|
+
}
|
|
844
|
+
function unsupportedDiagnostic(channelId, path, message) {
|
|
845
|
+
return Object.freeze({
|
|
846
|
+
code: hyaStateMachineChannelCapability(channelId).diagnosticCode,
|
|
847
|
+
severity: 'error',
|
|
848
|
+
channelId,
|
|
849
|
+
path,
|
|
850
|
+
message,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
function assertStrictFloatTimes(times, trackId) {
|
|
854
|
+
for (let index = 1; index < times.length; index++) {
|
|
855
|
+
if (times[index] <= times[index - 1]) {
|
|
856
|
+
throw new RangeError(`HYA state-machine clip "${trackId}" has time keys that collapse after Float32 conversion.`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function clamp01(value) {
|
|
861
|
+
return Math.min(1, Math.max(0, value));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Plays multiple named animation ranges and a state machine from one HYA
|
|
866
|
+
* asset. Transitions blend one pose onto one generated scene hierarchy.
|
|
867
|
+
*/
|
|
868
|
+
class Animation2DStateMachineComponent extends Component {
|
|
869
|
+
static UniqueCheckType = UniqueCheckType.SYMBOL | UniqueCheckType.REPLACE;
|
|
870
|
+
static UniqueSymbol = Symbol.for('Animation2DStateMachineComponent');
|
|
871
|
+
static Lifecycle = ComponentLifecycleFlags.EntityRemoveComponent
|
|
872
|
+
| ComponentLifecycleFlags.EntityRemoveFromWorld;
|
|
873
|
+
animation;
|
|
874
|
+
stateMachineExtension;
|
|
875
|
+
runtimeExtensions;
|
|
876
|
+
playing;
|
|
877
|
+
speed;
|
|
878
|
+
_runtime = null;
|
|
879
|
+
_needsUpdate = true;
|
|
880
|
+
_parametersByName;
|
|
881
|
+
_parameterValues = new Map();
|
|
882
|
+
constructor(source, options = {}) {
|
|
883
|
+
super('Animation2DStateMachineComponent');
|
|
884
|
+
this.animation = isParsed(source) ? source : parseAnimation(source);
|
|
885
|
+
this.stateMachineExtension = getHyaStateMachineExtension(this.animation);
|
|
886
|
+
this.runtimeExtensions = options.runtimeExtensions;
|
|
887
|
+
this.playing = options.autoplay ?? true;
|
|
888
|
+
this.speed = finiteSpeed(options.speed ?? 1);
|
|
889
|
+
this._parametersByName = new Map(this.stateMachineExtension.stateMachine.parameters.map(parameter => [parameter.name, parameter]));
|
|
890
|
+
}
|
|
891
|
+
get runtimeStats() {
|
|
892
|
+
return this._runtime?.stats ?? EMPTY_RUNTIME_STATS;
|
|
893
|
+
}
|
|
894
|
+
get layerSnapshots() {
|
|
895
|
+
if (this._runtime)
|
|
896
|
+
return this._runtime.layerSnapshots;
|
|
897
|
+
return Object.freeze(this.stateMachineExtension.stateMachine.layers.map(layer => Object.freeze({
|
|
898
|
+
layerId: layer.id,
|
|
899
|
+
currentStateId: layer.initialStateId,
|
|
900
|
+
currentTime: 0,
|
|
901
|
+
transitionId: null,
|
|
902
|
+
sourceStateId: null,
|
|
903
|
+
destinationStateId: null,
|
|
904
|
+
transitionProgress: 0,
|
|
905
|
+
})));
|
|
906
|
+
}
|
|
907
|
+
get channelDiagnostics() {
|
|
908
|
+
return this._runtime?.diagnostics ?? EMPTY_CHANNEL_DIAGNOSTICS;
|
|
909
|
+
}
|
|
910
|
+
get liveActionCount() { return this._runtime?.liveActionCount ?? 0; }
|
|
911
|
+
get liveBindingCount() { return this._runtime?.liveBindingCount ?? 0; }
|
|
912
|
+
get sideEffectOwnerCount() { return this._runtime?.sideEffectOwnerCount ?? 0; }
|
|
913
|
+
play() {
|
|
914
|
+
this.playing = true;
|
|
915
|
+
this._runtime?.setPlaying(true);
|
|
916
|
+
this._needsUpdate = true;
|
|
917
|
+
return this;
|
|
918
|
+
}
|
|
919
|
+
pause() {
|
|
920
|
+
this.playing = false;
|
|
921
|
+
this._runtime?.setPlaying(false);
|
|
922
|
+
return this;
|
|
923
|
+
}
|
|
924
|
+
setSpeed(speed) {
|
|
925
|
+
this.speed = finiteSpeed(speed);
|
|
926
|
+
return this;
|
|
927
|
+
}
|
|
928
|
+
setFloat(name, value) {
|
|
929
|
+
if (!Number.isFinite(value))
|
|
930
|
+
throw new RangeError('Float parameters must be finite.');
|
|
931
|
+
this._setParameter(name, 'float', value);
|
|
932
|
+
this._runtime?.controller.setFloat(name, value);
|
|
933
|
+
return this;
|
|
934
|
+
}
|
|
935
|
+
setInteger(name, value) {
|
|
936
|
+
if (!Number.isSafeInteger(value))
|
|
937
|
+
throw new RangeError('Integer parameters must be safe integers.');
|
|
938
|
+
this._setParameter(name, 'integer', value);
|
|
939
|
+
this._runtime?.controller.setInteger(name, value);
|
|
940
|
+
return this;
|
|
941
|
+
}
|
|
942
|
+
setBoolean(name, value) {
|
|
943
|
+
if (typeof value !== 'boolean')
|
|
944
|
+
throw new TypeError('Boolean parameters require a boolean.');
|
|
945
|
+
this._setParameter(name, 'boolean', value);
|
|
946
|
+
this._runtime?.controller.setBoolean(name, value);
|
|
947
|
+
return this;
|
|
948
|
+
}
|
|
949
|
+
setTrigger(name) {
|
|
950
|
+
this._setParameter(name, 'trigger', true);
|
|
951
|
+
this._runtime?.controller.setTrigger(name);
|
|
952
|
+
return this;
|
|
953
|
+
}
|
|
954
|
+
resetTrigger(name) {
|
|
955
|
+
this._setParameter(name, 'trigger', false);
|
|
956
|
+
this._runtime?.controller.resetTrigger(name);
|
|
957
|
+
return this;
|
|
958
|
+
}
|
|
959
|
+
getParameter(name) {
|
|
960
|
+
const parameter = this._requireParameter(name);
|
|
961
|
+
if (this._runtime)
|
|
962
|
+
return this._runtime.controller.getParameter(name);
|
|
963
|
+
if (this._parameterValues.has(name))
|
|
964
|
+
return this._parameterValues.get(name);
|
|
965
|
+
return parameter.type === 'trigger' ? false : parameter.defaultValue;
|
|
966
|
+
}
|
|
967
|
+
reset() {
|
|
968
|
+
this._parameterValues.clear();
|
|
969
|
+
this._runtime?.reset();
|
|
970
|
+
this._needsUpdate = this._runtime === null;
|
|
971
|
+
return this;
|
|
972
|
+
}
|
|
973
|
+
onEntityRemoveComponent(_entity, component) {
|
|
974
|
+
if (component === this)
|
|
975
|
+
this._disposeRuntime();
|
|
976
|
+
}
|
|
977
|
+
onEntityRemoveFromWorld(_entity, _world) {
|
|
978
|
+
this._disposeRuntime();
|
|
979
|
+
}
|
|
980
|
+
clone() {
|
|
981
|
+
const clone = new Animation2DStateMachineComponent(this.animation, {
|
|
982
|
+
autoplay: this.playing,
|
|
983
|
+
speed: this.speed,
|
|
984
|
+
...(this.runtimeExtensions ? { runtimeExtensions: this.runtimeExtensions } : {}),
|
|
985
|
+
});
|
|
986
|
+
for (const parameter of this.stateMachineExtension.stateMachine.parameters) {
|
|
987
|
+
clone._parameterValues.set(parameter.name, this.getParameter(parameter.name));
|
|
988
|
+
}
|
|
989
|
+
clone.disabled = this.disabled;
|
|
990
|
+
return clone;
|
|
991
|
+
}
|
|
992
|
+
_getParameterValues() {
|
|
993
|
+
return this._parameterValues;
|
|
994
|
+
}
|
|
995
|
+
_disposeRuntime() {
|
|
996
|
+
if (!this._runtime)
|
|
997
|
+
return;
|
|
998
|
+
for (const parameter of this.stateMachineExtension.stateMachine.parameters) {
|
|
999
|
+
this._parameterValues.set(parameter.name, this._runtime.controller.getParameter(parameter.name));
|
|
1000
|
+
}
|
|
1001
|
+
this._runtime.destroy();
|
|
1002
|
+
this._runtime = null;
|
|
1003
|
+
this._needsUpdate = true;
|
|
1004
|
+
}
|
|
1005
|
+
_setParameter(name, type, value) {
|
|
1006
|
+
const parameter = this._requireParameter(name);
|
|
1007
|
+
if (parameter.type !== type) {
|
|
1008
|
+
throw new TypeError(`State-machine parameter "${name}" is ${parameter.type}, not ${type}.`);
|
|
1009
|
+
}
|
|
1010
|
+
this._parameterValues.set(name, value);
|
|
1011
|
+
this._needsUpdate = true;
|
|
1012
|
+
}
|
|
1013
|
+
_requireParameter(name) {
|
|
1014
|
+
const parameter = this._parametersByName.get(name);
|
|
1015
|
+
if (!parameter)
|
|
1016
|
+
throw new ReferenceError(`Unknown state-machine parameter "${name}".`);
|
|
1017
|
+
return parameter;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const EMPTY_RUNTIME_STATS = Object.freeze({
|
|
1021
|
+
nodeCount: 0,
|
|
1022
|
+
visualCount: 0,
|
|
1023
|
+
unsupportedComponentCount: 0,
|
|
1024
|
+
pendingResourceCount: 0,
|
|
1025
|
+
failedResourceCount: 0,
|
|
1026
|
+
textCount: 0,
|
|
1027
|
+
particleCount: 0,
|
|
1028
|
+
audioCount: 0,
|
|
1029
|
+
});
|
|
1030
|
+
const EMPTY_CHANNEL_DIAGNOSTICS = Object.freeze([]);
|
|
1031
|
+
function isParsed(source) {
|
|
1032
|
+
return typeof source === 'object' && source !== null && 'source' in source;
|
|
1033
|
+
}
|
|
1034
|
+
function finiteSpeed(value) {
|
|
1035
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1036
|
+
throw new RangeError('Animation2D state-machine speed must be finite and non-negative.');
|
|
1037
|
+
}
|
|
1038
|
+
return value;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** State-machine-only pose writer kept out of the ordinary Animation2D bundle. */
|
|
1042
|
+
class Animation2DStateMachineVisualRuntime extends Animation2DRuntime {
|
|
1043
|
+
_particleOwners = new Map();
|
|
1044
|
+
_audioOwners = new Map();
|
|
1045
|
+
_effectPayloads = new Map();
|
|
1046
|
+
_diagnostics = [];
|
|
1047
|
+
_reportedAudioRejections = new Set();
|
|
1048
|
+
constructor(owner, animation, runtimeExtensions, assetManager) {
|
|
1049
|
+
super(owner, animation, runtimeExtensions, assetManager);
|
|
1050
|
+
for (const node of this._nodes) {
|
|
1051
|
+
for (const particle of node.particles) {
|
|
1052
|
+
particle.playing = false;
|
|
1053
|
+
particle.emitting = false;
|
|
1054
|
+
particle.clear();
|
|
1055
|
+
}
|
|
1056
|
+
for (const audio of node.audio)
|
|
1057
|
+
audio.exitStateMachine();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
get diagnostics() {
|
|
1061
|
+
return this._diagnostics;
|
|
1062
|
+
}
|
|
1063
|
+
get sideEffectOwnerCount() {
|
|
1064
|
+
return ownerCount(this._particleOwners) + ownerCount(this._audioOwners);
|
|
1065
|
+
}
|
|
1066
|
+
resetSideEffects() {
|
|
1067
|
+
this._particleOwners.clear();
|
|
1068
|
+
this._audioOwners.clear();
|
|
1069
|
+
this._effectPayloads.clear();
|
|
1070
|
+
for (const node of this._nodes) {
|
|
1071
|
+
for (const particle of node.particles) {
|
|
1072
|
+
particle.playing = false;
|
|
1073
|
+
particle.emitting = false;
|
|
1074
|
+
particle.clear();
|
|
1075
|
+
}
|
|
1076
|
+
for (const audio of node.audio)
|
|
1077
|
+
audio.exitStateMachine();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
applyPose(pose, playing = this._playing, speed = this._speed) {
|
|
1081
|
+
this._playing = playing;
|
|
1082
|
+
this._speed = speed;
|
|
1083
|
+
this._opacityMemo.clear();
|
|
1084
|
+
for (const node of this._nodes) {
|
|
1085
|
+
node.entity.disabled = false;
|
|
1086
|
+
node.transform.setPosition(node.initialX, node.initialY);
|
|
1087
|
+
node.transform.rotation = node.initialRotation;
|
|
1088
|
+
node.transform.setScale(node.initialScaleX, node.initialScaleY);
|
|
1089
|
+
node.opacity = node.initialOpacity;
|
|
1090
|
+
}
|
|
1091
|
+
for (const channel of pose.channels) {
|
|
1092
|
+
const node = this._nodesById.get(channel.binding.targetId);
|
|
1093
|
+
if (!node) {
|
|
1094
|
+
throw new ReferenceError(`Animation2D pose references unknown node "${channel.binding.targetId}".`);
|
|
1095
|
+
}
|
|
1096
|
+
switch (channel.binding.path) {
|
|
1097
|
+
case 'transform.position': {
|
|
1098
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 2);
|
|
1099
|
+
node.transform.setPosition(value[0], -value[1]);
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
case 'transform.rotation': {
|
|
1103
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 1);
|
|
1104
|
+
node.transform.rotation = -value[0];
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case 'transform.scale': {
|
|
1108
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 2);
|
|
1109
|
+
node.transform.setScale(value[0], value[1]);
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
case 'opacity': {
|
|
1113
|
+
const value = numericPoseValue(channel.value, channel.binding.id, 1);
|
|
1114
|
+
node.opacity = clamp(value[0], 0, 1);
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
case 'visibility':
|
|
1118
|
+
if (typeof channel.value !== 'boolean') {
|
|
1119
|
+
throw new TypeError(`Animation2D visibility binding "${channel.binding.id}" requires a boolean.`);
|
|
1120
|
+
}
|
|
1121
|
+
node.entity.disabled = !channel.value;
|
|
1122
|
+
break;
|
|
1123
|
+
default:
|
|
1124
|
+
if (!this._applyStateMachineVisualChannel(node, channel.binding.path, channel.value)) {
|
|
1125
|
+
throw new RangeError(`Animation2D state-machine runtime does not support pose binding path "${channel.binding.path}".`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
for (const node of this._nodes)
|
|
1130
|
+
this._applyVisualOpacity(node);
|
|
1131
|
+
for (const [key, owners] of this._audioOwners) {
|
|
1132
|
+
if (owners.length === 0)
|
|
1133
|
+
continue;
|
|
1134
|
+
const target = parseEffectTargetKey(key);
|
|
1135
|
+
const node = this._nodesById.get(target.targetId);
|
|
1136
|
+
this._audioTarget(key)?.updateStateMachineProperties(speed, node ? this._resolveOpacity(node) : 1);
|
|
1137
|
+
}
|
|
1138
|
+
for (const effect of pose.effects)
|
|
1139
|
+
this._applyEffect(effect, playing, speed);
|
|
1140
|
+
this._lastAppliedTime = Number.NaN;
|
|
1141
|
+
}
|
|
1142
|
+
setPlaying(playing) {
|
|
1143
|
+
this._playing = playing;
|
|
1144
|
+
for (const [key, owners] of this._particleOwners) {
|
|
1145
|
+
if (owners.length === 0)
|
|
1146
|
+
continue;
|
|
1147
|
+
const target = this._particleTarget(key);
|
|
1148
|
+
if (target)
|
|
1149
|
+
target.playing = playing;
|
|
1150
|
+
}
|
|
1151
|
+
for (const [key, owners] of this._audioOwners) {
|
|
1152
|
+
if (owners.length === 0)
|
|
1153
|
+
continue;
|
|
1154
|
+
const target = this._audioTarget(key);
|
|
1155
|
+
target?.setStateMachinePlaying(playing, this._speed, reason => this._reportAudioRejection(key, reason));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
destroy() {
|
|
1159
|
+
this.resetSideEffects();
|
|
1160
|
+
super.destroy();
|
|
1161
|
+
}
|
|
1162
|
+
_applyVisualOpacity(node) {
|
|
1163
|
+
const opacity = this._resolveOpacity(node);
|
|
1164
|
+
if (node.deferredVisuals.length > 0 && this._isNodeActive(node)) {
|
|
1165
|
+
for (const deferred of node.deferredVisuals.splice(0))
|
|
1166
|
+
this._materializeCoreVisual(node, deferred);
|
|
1167
|
+
}
|
|
1168
|
+
for (const visual of node.visuals) {
|
|
1169
|
+
const alpha = visual.baseAlpha * opacity;
|
|
1170
|
+
if (Math.abs(alpha - visual.lastAlpha) < 1e-6)
|
|
1171
|
+
continue;
|
|
1172
|
+
visual.color[3] = alpha;
|
|
1173
|
+
visual.component.revision++;
|
|
1174
|
+
visual.lastAlpha = alpha;
|
|
1175
|
+
}
|
|
1176
|
+
for (const extension of node.extensions)
|
|
1177
|
+
extension.setOpacity?.(opacity);
|
|
1178
|
+
}
|
|
1179
|
+
_applyEffect(effect, playing, speed) {
|
|
1180
|
+
const payload = effect.cue.payload;
|
|
1181
|
+
if (!payload || typeof payload.targetId !== 'string' || !Number.isSafeInteger(payload.slot)) {
|
|
1182
|
+
throw new TypeError(`Animation2D effect cue "${effect.cue.id}" has an invalid HYA target payload.`);
|
|
1183
|
+
}
|
|
1184
|
+
const key = effectTargetKey(payload);
|
|
1185
|
+
this._effectPayloads.set(key, payload);
|
|
1186
|
+
const owners = effect.cue.kind === 'particle'
|
|
1187
|
+
? this._particleOwners
|
|
1188
|
+
: this._audioOwners;
|
|
1189
|
+
const stack = owners.get(key) ?? [];
|
|
1190
|
+
if (!owners.has(key))
|
|
1191
|
+
owners.set(key, stack);
|
|
1192
|
+
if (effect.lifecycle === 'enter') {
|
|
1193
|
+
const existing = stack.indexOf(effect.actionId);
|
|
1194
|
+
if (existing >= 0)
|
|
1195
|
+
stack.splice(existing, 1);
|
|
1196
|
+
stack.push(effect.actionId);
|
|
1197
|
+
if (effect.cue.kind === 'particle') {
|
|
1198
|
+
const particle = this._particleTarget(key);
|
|
1199
|
+
if (particle) {
|
|
1200
|
+
particle.emitting = true;
|
|
1201
|
+
particle.restart(true);
|
|
1202
|
+
particle.playing = playing;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
const audio = this._audioTarget(key);
|
|
1207
|
+
const node = this._nodesById.get(payload.targetId);
|
|
1208
|
+
const opacity = node ? this._resolveOpacity(node) : 1;
|
|
1209
|
+
audio?.enterStateMachine(playing, speed, opacity, reason => this._reportAudioRejection(key, reason));
|
|
1210
|
+
}
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (effect.lifecycle === 'exit') {
|
|
1214
|
+
const index = stack.indexOf(effect.actionId);
|
|
1215
|
+
const wasDominant = index === stack.length - 1;
|
|
1216
|
+
if (index >= 0)
|
|
1217
|
+
stack.splice(index, 1);
|
|
1218
|
+
if (stack.length === 0) {
|
|
1219
|
+
owners.delete(key);
|
|
1220
|
+
if (effect.cue.kind === 'particle') {
|
|
1221
|
+
const particle = this._particleTarget(key);
|
|
1222
|
+
if (particle) {
|
|
1223
|
+
particle.playing = false;
|
|
1224
|
+
particle.emitting = false;
|
|
1225
|
+
particle.clear();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
else
|
|
1229
|
+
this._audioTarget(key)?.exitStateMachine();
|
|
1230
|
+
}
|
|
1231
|
+
else if (wasDominant && effect.cue.kind === 'particle') {
|
|
1232
|
+
const particle = this._particleTarget(key);
|
|
1233
|
+
if (particle) {
|
|
1234
|
+
particle.emitting = true;
|
|
1235
|
+
particle.restart(true);
|
|
1236
|
+
particle.playing = playing;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (stack[stack.length - 1] !== effect.actionId)
|
|
1242
|
+
return;
|
|
1243
|
+
if (effect.lifecycle === 'loop')
|
|
1244
|
+
return;
|
|
1245
|
+
if (effect.cue.kind === 'particle') {
|
|
1246
|
+
const particle = this._particleTarget(key);
|
|
1247
|
+
if (particle) {
|
|
1248
|
+
particle.emitting = true;
|
|
1249
|
+
particle.restart(true);
|
|
1250
|
+
particle.playing = playing;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
const audio = this._audioTarget(key);
|
|
1255
|
+
const node = this._nodesById.get(payload.targetId);
|
|
1256
|
+
const opacity = node ? this._resolveOpacity(node) : 1;
|
|
1257
|
+
audio?.restartStateMachine(playing, speed, opacity, reason => this._reportAudioRejection(key, reason));
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
_particleTarget(key) {
|
|
1261
|
+
const target = parseEffectTargetKey(key);
|
|
1262
|
+
return this._nodesById.get(target.targetId)?.particles[target.slot];
|
|
1263
|
+
}
|
|
1264
|
+
_audioTarget(key) {
|
|
1265
|
+
const target = parseEffectTargetKey(key);
|
|
1266
|
+
return this._nodesById.get(target.targetId)?.audio[target.slot];
|
|
1267
|
+
}
|
|
1268
|
+
_reportAudioRejection(key, reason) {
|
|
1269
|
+
if (this._reportedAudioRejections.has(key))
|
|
1270
|
+
return;
|
|
1271
|
+
this._reportedAudioRejections.add(key);
|
|
1272
|
+
const target = parseEffectTargetKey(key);
|
|
1273
|
+
const sourcePath = this._effectPayloads.get(key)?.sourcePath
|
|
1274
|
+
?? `$.nodes[id=${JSON.stringify(target.targetId)}].components[audio=${target.slot}]`;
|
|
1275
|
+
this._diagnostics.push(audioAutoplayRejectedDiagnostic(sourcePath, reason));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function effectTargetKey(payload) {
|
|
1279
|
+
return `${payload.targetId}\u0000${payload.slot}`;
|
|
1280
|
+
}
|
|
1281
|
+
function parseEffectTargetKey(key) {
|
|
1282
|
+
const separator = key.lastIndexOf('\u0000');
|
|
1283
|
+
return {
|
|
1284
|
+
targetId: key.slice(0, separator),
|
|
1285
|
+
slot: Number(key.slice(separator + 1)),
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function ownerCount(owners) {
|
|
1289
|
+
let count = 0;
|
|
1290
|
+
for (const stack of owners.values())
|
|
1291
|
+
count += stack.length;
|
|
1292
|
+
return count;
|
|
1293
|
+
}
|
|
1294
|
+
function numericPoseValue(value, bindingId, size) {
|
|
1295
|
+
if (!(value instanceof Float32Array) || value.length < size) {
|
|
1296
|
+
throw new TypeError(`Animation2D numeric binding "${bindingId}" requires ${size} Float32 values.`);
|
|
1297
|
+
}
|
|
1298
|
+
return value;
|
|
1299
|
+
}
|
|
1300
|
+
function clamp(value, min, max) {
|
|
1301
|
+
return Math.min(max, Math.max(min, value));
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Stateful allocation-free sampler. Rotation values are radians and LINEAR
|
|
1306
|
+
* sampling follows the shortest signed arc in [-PI, PI).
|
|
1307
|
+
*/
|
|
1308
|
+
class Animation2DSampler {
|
|
1309
|
+
track;
|
|
1310
|
+
output;
|
|
1311
|
+
discreteValue;
|
|
1312
|
+
_cursor = 0;
|
|
1313
|
+
constructor(track) {
|
|
1314
|
+
validateTrack(track);
|
|
1315
|
+
this.track = track;
|
|
1316
|
+
this.output = new Float32Array(isNumericBinding(track.binding)
|
|
1317
|
+
? track.binding.valueSize
|
|
1318
|
+
: 0);
|
|
1319
|
+
}
|
|
1320
|
+
sample(timeSeconds, out = this.output) {
|
|
1321
|
+
if (!Number.isFinite(timeSeconds)) {
|
|
1322
|
+
throw new RangeError(`Animation2D sample time must be finite; received ${timeSeconds}.`);
|
|
1323
|
+
}
|
|
1324
|
+
const keyIndex = this._findKey(timeSeconds);
|
|
1325
|
+
if (!isNumericBinding(this.track.binding)) {
|
|
1326
|
+
this.discreteValue = this.track.values[keyIndex];
|
|
1327
|
+
return this.discreteValue;
|
|
1328
|
+
}
|
|
1329
|
+
const track = this.track;
|
|
1330
|
+
const size = track.binding.valueSize;
|
|
1331
|
+
if (out.length < size) {
|
|
1332
|
+
throw new RangeError(`Animation2D sampler output requires ${size} values; received ${out.length}.`);
|
|
1333
|
+
}
|
|
1334
|
+
const times = track.times;
|
|
1335
|
+
if (keyIndex >= times.length - 1 || track.interpolation === 'step') {
|
|
1336
|
+
copyNumericKey(track, keyIndex, out);
|
|
1337
|
+
return out;
|
|
1338
|
+
}
|
|
1339
|
+
const startTime = times[keyIndex];
|
|
1340
|
+
const linearAlpha = (timeSeconds - startTime) / (times[keyIndex + 1] - startTime);
|
|
1341
|
+
const alpha = track.interpolation === 'cubic-bezier'
|
|
1342
|
+
? sampleCubicEasing(track.easings, keyIndex, linearAlpha)
|
|
1343
|
+
: linearAlpha;
|
|
1344
|
+
const startOffset = keyIndex * size;
|
|
1345
|
+
const endOffset = startOffset + size;
|
|
1346
|
+
if (track.binding.strategy === 'rotation') {
|
|
1347
|
+
const start = track.values[startOffset];
|
|
1348
|
+
const delta = wrapAngle(track.values[endOffset] - start);
|
|
1349
|
+
out[0] = wrapAngle(start + delta * alpha);
|
|
1350
|
+
return out;
|
|
1351
|
+
}
|
|
1352
|
+
if (track.spatialTangents) {
|
|
1353
|
+
const tangentOffset = keyIndex * 4;
|
|
1354
|
+
for (let component = 0; component < 2; component++) {
|
|
1355
|
+
const start = track.values[startOffset + component];
|
|
1356
|
+
const end = track.values[endOffset + component];
|
|
1357
|
+
out[component] = cubicSpatialCoordinate(alpha, start, start + track.spatialTangents[tangentOffset + component], end + track.spatialTangents[tangentOffset + 2 + component], end);
|
|
1358
|
+
}
|
|
1359
|
+
return out;
|
|
1360
|
+
}
|
|
1361
|
+
for (let component = 0; component < size; component++) {
|
|
1362
|
+
const start = track.values[startOffset + component];
|
|
1363
|
+
out[component] = start
|
|
1364
|
+
+ (track.values[endOffset + component] - start) * alpha;
|
|
1365
|
+
}
|
|
1366
|
+
return out;
|
|
1367
|
+
}
|
|
1368
|
+
resetCursor() {
|
|
1369
|
+
this._cursor = 0;
|
|
1370
|
+
}
|
|
1371
|
+
_findKey(timeSeconds) {
|
|
1372
|
+
const times = this.track.times;
|
|
1373
|
+
const last = times.length - 1;
|
|
1374
|
+
if (last === 0 || timeSeconds <= times[0]) {
|
|
1375
|
+
this._cursor = 0;
|
|
1376
|
+
return 0;
|
|
1377
|
+
}
|
|
1378
|
+
if (timeSeconds >= times[last]) {
|
|
1379
|
+
this._cursor = Math.max(0, last - 1);
|
|
1380
|
+
return last;
|
|
1381
|
+
}
|
|
1382
|
+
let cursor = Math.min(this._cursor, last - 1);
|
|
1383
|
+
if (timeSeconds >= times[cursor] && timeSeconds < times[cursor + 1])
|
|
1384
|
+
return cursor;
|
|
1385
|
+
if (timeSeconds >= times[cursor + 1]) {
|
|
1386
|
+
while (cursor + 1 < last && timeSeconds >= times[cursor + 1])
|
|
1387
|
+
cursor++;
|
|
1388
|
+
this._cursor = cursor;
|
|
1389
|
+
return cursor;
|
|
1390
|
+
}
|
|
1391
|
+
let low = 0;
|
|
1392
|
+
let high = cursor;
|
|
1393
|
+
while (low <= high) {
|
|
1394
|
+
const middle = (low + high) >>> 1;
|
|
1395
|
+
if (timeSeconds < times[middle]) {
|
|
1396
|
+
high = middle - 1;
|
|
1397
|
+
}
|
|
1398
|
+
else if (timeSeconds >= times[middle + 1]) {
|
|
1399
|
+
low = middle + 1;
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
this._cursor = middle;
|
|
1403
|
+
return middle;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
this._cursor = Math.max(0, low);
|
|
1407
|
+
return this._cursor;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function wrapAngle(value) {
|
|
1411
|
+
const twoPi = Math.PI * 2;
|
|
1412
|
+
let wrapped = (value + Math.PI) % twoPi;
|
|
1413
|
+
if (wrapped < 0)
|
|
1414
|
+
wrapped += twoPi;
|
|
1415
|
+
return wrapped - Math.PI;
|
|
1416
|
+
}
|
|
1417
|
+
function isNumericBinding(binding) {
|
|
1418
|
+
return binding.strategy === 'continuous' || binding.strategy === 'rotation';
|
|
1419
|
+
}
|
|
1420
|
+
function copyNumericKey(track, keyIndex, out) {
|
|
1421
|
+
const size = track.binding.valueSize;
|
|
1422
|
+
const offset = keyIndex * size;
|
|
1423
|
+
for (let component = 0; component < size; component++) {
|
|
1424
|
+
out[component] = track.values[offset + component];
|
|
1425
|
+
}
|
|
1426
|
+
if (track.binding.strategy === 'rotation')
|
|
1427
|
+
out[0] = wrapAngle(out[0]);
|
|
1428
|
+
}
|
|
1429
|
+
function validateTrack(track) {
|
|
1430
|
+
if (!track.id)
|
|
1431
|
+
throw new RangeError('Animation2D track id must not be empty.');
|
|
1432
|
+
if (!track.binding.id)
|
|
1433
|
+
throw new RangeError('Animation2D binding id must not be empty.');
|
|
1434
|
+
if (track.times.length === 0) {
|
|
1435
|
+
throw new RangeError(`Animation2D track "${track.id}" must contain at least one key.`);
|
|
1436
|
+
}
|
|
1437
|
+
for (let index = 0; index < track.times.length; index++) {
|
|
1438
|
+
const time = track.times[index];
|
|
1439
|
+
if (!Number.isFinite(time) || (index > 0 && time <= track.times[index - 1])) {
|
|
1440
|
+
throw new RangeError(`Animation2D track "${track.id}" times must be finite and strictly increasing.`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (isNumericBinding(track.binding)) {
|
|
1444
|
+
const numericTrack = track;
|
|
1445
|
+
const size = track.binding.valueSize ?? 0;
|
|
1446
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
1447
|
+
throw new RangeError(`Animation2D numeric binding "${track.binding.id}" requires a positive valueSize.`);
|
|
1448
|
+
}
|
|
1449
|
+
if (track.binding.strategy === 'rotation' && size !== 1) {
|
|
1450
|
+
throw new RangeError(`Animation2D rotation binding "${track.binding.id}" must have valueSize 1.`);
|
|
1451
|
+
}
|
|
1452
|
+
const expected = track.times.length * size;
|
|
1453
|
+
if (!(track.values instanceof Float32Array) || track.values.length !== expected) {
|
|
1454
|
+
throw new RangeError(`Animation2D track "${track.id}" requires ${expected} numeric values.`);
|
|
1455
|
+
}
|
|
1456
|
+
if (numericTrack.interpolation === 'cubic-bezier') {
|
|
1457
|
+
const expectedEasings = Math.max(0, track.times.length - 1) * 4;
|
|
1458
|
+
if (!(numericTrack.easings instanceof Float32Array) || numericTrack.easings.length !== expectedEasings) {
|
|
1459
|
+
throw new RangeError(`Animation2D track "${track.id}" requires ${expectedEasings} cubic easing values.`);
|
|
1460
|
+
}
|
|
1461
|
+
for (let index = 0; index < numericTrack.easings.length; index += 4) {
|
|
1462
|
+
const x1 = numericTrack.easings[index];
|
|
1463
|
+
const x2 = numericTrack.easings[index + 2];
|
|
1464
|
+
if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
|
|
1465
|
+
throw new RangeError(`Animation2D track "${track.id}" cubic easing x controls must be in [0, 1].`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
else if (numericTrack.easings !== undefined) {
|
|
1470
|
+
throw new RangeError(`Animation2D track "${track.id}" easings require cubic-bezier interpolation.`);
|
|
1471
|
+
}
|
|
1472
|
+
if (numericTrack.spatialTangents !== undefined) {
|
|
1473
|
+
const expectedTangents = Math.max(0, track.times.length - 1) * 4;
|
|
1474
|
+
if (track.binding.path !== 'transform.position' || size !== 2) {
|
|
1475
|
+
throw new RangeError(`Animation2D track "${track.id}" spatialTangents require transform.position/2.`);
|
|
1476
|
+
}
|
|
1477
|
+
if (numericTrack.interpolation === 'step') {
|
|
1478
|
+
throw new RangeError(`Animation2D track "${track.id}" spatialTangents cannot use step.`);
|
|
1479
|
+
}
|
|
1480
|
+
if (!(numericTrack.spatialTangents instanceof Float32Array)
|
|
1481
|
+
|| numericTrack.spatialTangents.length !== expectedTangents) {
|
|
1482
|
+
throw new RangeError(`Animation2D track "${track.id}" requires ${expectedTangents} spatialTangents.`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
if (track.interpolation !== 'step') {
|
|
1488
|
+
throw new RangeError(`Animation2D ${track.binding.strategy} track "${track.id}" must use step interpolation.`);
|
|
1489
|
+
}
|
|
1490
|
+
if (track.values.length !== track.times.length) {
|
|
1491
|
+
throw new RangeError(`Animation2D track "${track.id}" requires one discrete value per key.`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
function sampleCubicEasing(easings, segment, x) {
|
|
1495
|
+
const offset = segment * 4;
|
|
1496
|
+
const x1 = easings[offset];
|
|
1497
|
+
const y1 = easings[offset + 1];
|
|
1498
|
+
const x2 = easings[offset + 2];
|
|
1499
|
+
const y2 = easings[offset + 3];
|
|
1500
|
+
let parameter = x;
|
|
1501
|
+
for (let iteration = 0; iteration < 6; iteration++) {
|
|
1502
|
+
const estimate = cubicCoordinate(parameter, x1, x2) - x;
|
|
1503
|
+
const derivative = cubicDerivative(parameter, x1, x2);
|
|
1504
|
+
if (Math.abs(derivative) < 1e-6)
|
|
1505
|
+
break;
|
|
1506
|
+
parameter = Math.min(1, Math.max(0, parameter - estimate / derivative));
|
|
1507
|
+
}
|
|
1508
|
+
return cubicCoordinate(parameter, y1, y2);
|
|
1509
|
+
}
|
|
1510
|
+
function cubicCoordinate(value, first, second) {
|
|
1511
|
+
const inverse = 1 - value;
|
|
1512
|
+
return 3 * inverse * inverse * value * first
|
|
1513
|
+
+ 3 * inverse * value * value * second
|
|
1514
|
+
+ value * value * value;
|
|
1515
|
+
}
|
|
1516
|
+
function cubicDerivative(value, first, second) {
|
|
1517
|
+
const inverse = 1 - value;
|
|
1518
|
+
return 3 * inverse * inverse * first
|
|
1519
|
+
+ 6 * inverse * value * (second - first)
|
|
1520
|
+
+ 3 * value * value * (1 - second);
|
|
1521
|
+
}
|
|
1522
|
+
function cubicSpatialCoordinate(value, start, first, second, end) {
|
|
1523
|
+
const inverse = 1 - value;
|
|
1524
|
+
return inverse * inverse * inverse * start
|
|
1525
|
+
+ 3 * inverse * inverse * value * first
|
|
1526
|
+
+ 3 * inverse * value * value * second
|
|
1527
|
+
+ value * value * value * end;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Mutable mixer-owned playback state. Effect lifecycle records and sampler
|
|
1532
|
+
* outputs are allocated at construction and reused on every frame.
|
|
1533
|
+
*/
|
|
1534
|
+
class Animation2DActionRuntime {
|
|
1535
|
+
id;
|
|
1536
|
+
clip;
|
|
1537
|
+
order;
|
|
1538
|
+
runtimeTracks;
|
|
1539
|
+
runtimeEffects;
|
|
1540
|
+
_owner;
|
|
1541
|
+
_effectActive;
|
|
1542
|
+
_pendingEffects = [];
|
|
1543
|
+
_pendingEffectOutput = {
|
|
1544
|
+
emit: effect => { this._pendingEffects.push(effect); },
|
|
1545
|
+
};
|
|
1546
|
+
_valid = true;
|
|
1547
|
+
_status = 'idle';
|
|
1548
|
+
_paused = false;
|
|
1549
|
+
_time = 0;
|
|
1550
|
+
_rawTime = 0;
|
|
1551
|
+
_timeScale;
|
|
1552
|
+
_weight;
|
|
1553
|
+
_loop;
|
|
1554
|
+
_repetitions;
|
|
1555
|
+
_clampWhenFinished;
|
|
1556
|
+
_blendMode;
|
|
1557
|
+
_mask;
|
|
1558
|
+
_layer;
|
|
1559
|
+
_effectsStarted = false;
|
|
1560
|
+
_fadeActive = false;
|
|
1561
|
+
_fadeStart = 0;
|
|
1562
|
+
_fadeEnd = 0;
|
|
1563
|
+
_fadeFrom = 1;
|
|
1564
|
+
_fadeTo = 1;
|
|
1565
|
+
_fadeValue = 1;
|
|
1566
|
+
constructor(owner, clip, id, order, options = {}) {
|
|
1567
|
+
validateClip(clip);
|
|
1568
|
+
this._owner = owner;
|
|
1569
|
+
this.clip = clip;
|
|
1570
|
+
this.id = id;
|
|
1571
|
+
this.order = order;
|
|
1572
|
+
this._loop = options.loop ?? 'once';
|
|
1573
|
+
this._repetitions = validateRepetitions(options.repetitions ?? (this._loop === 'once' ? 1 : Infinity));
|
|
1574
|
+
this._clampWhenFinished = options.clampWhenFinished ?? false;
|
|
1575
|
+
this._timeScale = finiteNumber(options.timeScale ?? 1, 'Animation2DAction.timeScale');
|
|
1576
|
+
this._weight = nonNegativeNumber(options.weight ?? 1, 'Animation2DAction.weight');
|
|
1577
|
+
this._fadeValue = this._weight;
|
|
1578
|
+
this._layer = integerNumber(options.layer ?? 0, 'Animation2DAction.layer');
|
|
1579
|
+
this._blendMode = options.blendMode ?? 'override';
|
|
1580
|
+
this._mask = options.mask ?? null;
|
|
1581
|
+
const tracks = [];
|
|
1582
|
+
for (let index = 0; index < clip.tracks.length; index++) {
|
|
1583
|
+
const track = clip.tracks[index];
|
|
1584
|
+
const sampler = new Animation2DSampler(track);
|
|
1585
|
+
const reference = isNumericBinding(track.binding)
|
|
1586
|
+
? new Float32Array(track.binding.valueSize)
|
|
1587
|
+
: new Float32Array(0);
|
|
1588
|
+
if (reference.length > 0)
|
|
1589
|
+
sampler.sample(0, reference);
|
|
1590
|
+
tracks.push({ track, sampler, reference });
|
|
1591
|
+
}
|
|
1592
|
+
this.runtimeTracks = tracks;
|
|
1593
|
+
const effects = [];
|
|
1594
|
+
const sourceEffects = clip.effects ?? [];
|
|
1595
|
+
for (let index = 0; index < sourceEffects.length; index++) {
|
|
1596
|
+
const cue = sourceEffects[index];
|
|
1597
|
+
const start = cue.start;
|
|
1598
|
+
const end = cue.end ?? clip.duration;
|
|
1599
|
+
validateEffect(cue, start, end, clip.duration);
|
|
1600
|
+
effects.push({
|
|
1601
|
+
cue,
|
|
1602
|
+
start,
|
|
1603
|
+
end,
|
|
1604
|
+
fullSpan: start === 0 && end === clip.duration,
|
|
1605
|
+
loopBehavior: cue.loopBehavior ?? (cue.kind === 'particle' ? 'restart' : 'continue'),
|
|
1606
|
+
enterEvent: effectEvent(id, clip.id, cue, 'enter'),
|
|
1607
|
+
exitEvent: effectEvent(id, clip.id, cue, 'exit'),
|
|
1608
|
+
restartEvent: effectEvent(id, clip.id, cue, 'restart'),
|
|
1609
|
+
loopEvent: effectEvent(id, clip.id, cue, 'loop'),
|
|
1610
|
+
seekEvent: effectEvent(id, clip.id, cue, 'seek'),
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
this.runtimeEffects = effects;
|
|
1614
|
+
this._effectActive = new Uint8Array(effects.length);
|
|
1615
|
+
}
|
|
1616
|
+
get duration() { return this.clip.duration; }
|
|
1617
|
+
get status() { return this._status; }
|
|
1618
|
+
get paused() { return this._paused; }
|
|
1619
|
+
set paused(value) {
|
|
1620
|
+
this._assertValid();
|
|
1621
|
+
this._paused = Boolean(value);
|
|
1622
|
+
if (this._paused && this._status === 'running')
|
|
1623
|
+
this._status = 'paused';
|
|
1624
|
+
else if (!this._paused && this._status === 'paused')
|
|
1625
|
+
this._status = 'running';
|
|
1626
|
+
}
|
|
1627
|
+
get time() { return this._time; }
|
|
1628
|
+
set time(value) { this.seek(value); }
|
|
1629
|
+
get timeScale() { return this._timeScale; }
|
|
1630
|
+
set timeScale(value) {
|
|
1631
|
+
this._assertValid();
|
|
1632
|
+
this._timeScale = finiteNumber(value, 'Animation2DAction.timeScale');
|
|
1633
|
+
}
|
|
1634
|
+
get weight() { return this._weight; }
|
|
1635
|
+
set weight(value) {
|
|
1636
|
+
this._assertValid();
|
|
1637
|
+
this._weight = nonNegativeNumber(value, 'Animation2DAction.weight');
|
|
1638
|
+
this._fadeActive = false;
|
|
1639
|
+
this._fadeValue = this._weight;
|
|
1640
|
+
}
|
|
1641
|
+
get layer() { return this._layer; }
|
|
1642
|
+
set layer(value) {
|
|
1643
|
+
this._assertValid();
|
|
1644
|
+
this._layer = integerNumber(value, 'Animation2DAction.layer');
|
|
1645
|
+
}
|
|
1646
|
+
get loop() { return this._loop; }
|
|
1647
|
+
set loop(value) {
|
|
1648
|
+
this._assertValid();
|
|
1649
|
+
if (value !== 'once' && value !== 'repeat' && value !== 'ping-pong') {
|
|
1650
|
+
throw new RangeError(`Unsupported Animation2D loop mode "${String(value)}".`);
|
|
1651
|
+
}
|
|
1652
|
+
this._loop = value;
|
|
1653
|
+
this._setSampleTime(0);
|
|
1654
|
+
}
|
|
1655
|
+
get repetitions() { return this._repetitions; }
|
|
1656
|
+
set repetitions(value) {
|
|
1657
|
+
this._assertValid();
|
|
1658
|
+
this._repetitions = validateRepetitions(value);
|
|
1659
|
+
}
|
|
1660
|
+
get clampWhenFinished() { return this._clampWhenFinished; }
|
|
1661
|
+
set clampWhenFinished(value) {
|
|
1662
|
+
this._assertValid();
|
|
1663
|
+
this._clampWhenFinished = Boolean(value);
|
|
1664
|
+
}
|
|
1665
|
+
get blendMode() { return this._blendMode; }
|
|
1666
|
+
set blendMode(value) {
|
|
1667
|
+
this._assertValid();
|
|
1668
|
+
if (value !== 'override' && value !== 'additive') {
|
|
1669
|
+
throw new RangeError(`Unsupported Animation2D blend mode "${String(value)}".`);
|
|
1670
|
+
}
|
|
1671
|
+
this._blendMode = value;
|
|
1672
|
+
}
|
|
1673
|
+
get mask() { return this._mask; }
|
|
1674
|
+
set mask(value) {
|
|
1675
|
+
this._assertValid();
|
|
1676
|
+
this._mask = value;
|
|
1677
|
+
}
|
|
1678
|
+
get effectiveWeight() {
|
|
1679
|
+
if (!this._valid || this._paused || (this._status !== 'running'
|
|
1680
|
+
&& !(this._status === 'finished' && this._clampWhenFinished)))
|
|
1681
|
+
return 0;
|
|
1682
|
+
return this._fadeFactor(this._owner.time);
|
|
1683
|
+
}
|
|
1684
|
+
play() {
|
|
1685
|
+
this._assertValid();
|
|
1686
|
+
if (this._status === 'finished' && !this._clampWhenFinished) {
|
|
1687
|
+
this._rawTime = this._timeScale < 0 ? this.clip.duration : 0;
|
|
1688
|
+
this._setSampleTime(0);
|
|
1689
|
+
this._effectsStarted = false;
|
|
1690
|
+
}
|
|
1691
|
+
this._paused = false;
|
|
1692
|
+
this._status = 'running';
|
|
1693
|
+
return this;
|
|
1694
|
+
}
|
|
1695
|
+
pause() {
|
|
1696
|
+
this.paused = true;
|
|
1697
|
+
return this;
|
|
1698
|
+
}
|
|
1699
|
+
stop() {
|
|
1700
|
+
this._assertValid();
|
|
1701
|
+
this._queueActiveExits();
|
|
1702
|
+
this._paused = false;
|
|
1703
|
+
this._status = 'stopped';
|
|
1704
|
+
this._effectsStarted = false;
|
|
1705
|
+
return this;
|
|
1706
|
+
}
|
|
1707
|
+
reset() {
|
|
1708
|
+
this._assertValid();
|
|
1709
|
+
this._queueActiveExits();
|
|
1710
|
+
this._paused = false;
|
|
1711
|
+
this._status = 'idle';
|
|
1712
|
+
this._rawTime = 0;
|
|
1713
|
+
this._time = 0;
|
|
1714
|
+
this._effectsStarted = false;
|
|
1715
|
+
this._fadeActive = false;
|
|
1716
|
+
this._fadeValue = this._weight;
|
|
1717
|
+
this._resetSamplerCursors();
|
|
1718
|
+
return this;
|
|
1719
|
+
}
|
|
1720
|
+
seek(timeSeconds) {
|
|
1721
|
+
this._assertValid();
|
|
1722
|
+
const next = Math.min(this.clip.duration, Math.max(0, finiteNumber(timeSeconds, 'Animation2DAction.time')));
|
|
1723
|
+
for (let index = 0; index < this.runtimeEffects.length; index++) {
|
|
1724
|
+
const wasActive = this._effectActive[index] !== 0;
|
|
1725
|
+
const active = effectActiveAt(this.runtimeEffects[index], next, this.clip.duration);
|
|
1726
|
+
const effect = this.runtimeEffects[index];
|
|
1727
|
+
if (wasActive && !active)
|
|
1728
|
+
this._pendingEffects.push(effect.exitEvent);
|
|
1729
|
+
this._pendingEffects.push(effect.seekEvent);
|
|
1730
|
+
if (!wasActive && active)
|
|
1731
|
+
this._pendingEffects.push(effect.enterEvent);
|
|
1732
|
+
this._effectActive[index] = active ? 1 : 0;
|
|
1733
|
+
}
|
|
1734
|
+
this._rawTime = next;
|
|
1735
|
+
this._time = next;
|
|
1736
|
+
this._effectsStarted = true;
|
|
1737
|
+
this._resetSamplerCursors();
|
|
1738
|
+
return this;
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Advances a controller-owned absolute playhead without classifying the
|
|
1742
|
+
* move as a seek. Crossed side-effect boundaries are queued exactly once.
|
|
1743
|
+
*/
|
|
1744
|
+
synchronizeTime(timeSeconds) {
|
|
1745
|
+
this._assertValid();
|
|
1746
|
+
const after = this._clampSynchronizedTime(timeSeconds);
|
|
1747
|
+
const terminalDirection = this._terminalDirection(after);
|
|
1748
|
+
if (!this._effectsStarted) {
|
|
1749
|
+
this._emitInitialEffects(this._pendingEffectOutput);
|
|
1750
|
+
this._effectsStarted = true;
|
|
1751
|
+
}
|
|
1752
|
+
this._emitEffectCrossings(this._rawTime, after, this._pendingEffectOutput, terminalDirection);
|
|
1753
|
+
this._rawTime = after;
|
|
1754
|
+
this._setSampleTime(terminalDirection);
|
|
1755
|
+
this._syncEffectActivity(terminalDirection);
|
|
1756
|
+
this._resetSamplerCursors();
|
|
1757
|
+
return this;
|
|
1758
|
+
}
|
|
1759
|
+
reverse() {
|
|
1760
|
+
this._assertValid();
|
|
1761
|
+
this._timeScale = -Math.max(Number.EPSILON, Math.abs(this._timeScale));
|
|
1762
|
+
return this;
|
|
1763
|
+
}
|
|
1764
|
+
fadeTo(targetWeight, durationSeconds) {
|
|
1765
|
+
this._assertValid();
|
|
1766
|
+
const target = nonNegativeNumber(targetWeight, 'Animation2DAction.fade target');
|
|
1767
|
+
const duration = nonNegativeNumber(durationSeconds, 'Animation2DAction.fade duration');
|
|
1768
|
+
this._scheduleFade(this._fadeFactor(this._owner.time), target, duration);
|
|
1769
|
+
return this;
|
|
1770
|
+
}
|
|
1771
|
+
fadeIn(durationSeconds) {
|
|
1772
|
+
this._assertValid();
|
|
1773
|
+
const duration = nonNegativeNumber(durationSeconds, 'Animation2DAction.fade duration');
|
|
1774
|
+
this._scheduleFade(0, this._weight > 0 ? this._weight : 1, duration);
|
|
1775
|
+
return this;
|
|
1776
|
+
}
|
|
1777
|
+
fadeOut(durationSeconds) {
|
|
1778
|
+
return this.fadeTo(0, durationSeconds);
|
|
1779
|
+
}
|
|
1780
|
+
crossFadeFrom(source, durationSeconds) {
|
|
1781
|
+
this._assertSibling(source);
|
|
1782
|
+
source.play().fadeOut(durationSeconds);
|
|
1783
|
+
this.play().fadeIn(durationSeconds);
|
|
1784
|
+
return this;
|
|
1785
|
+
}
|
|
1786
|
+
crossFadeTo(destination, durationSeconds) {
|
|
1787
|
+
this._assertSibling(destination);
|
|
1788
|
+
destination.crossFadeFrom(this, durationSeconds);
|
|
1789
|
+
return this;
|
|
1790
|
+
}
|
|
1791
|
+
acceptsBinding(binding) {
|
|
1792
|
+
const include = this._mask?.include;
|
|
1793
|
+
if (include && include.length > 0) {
|
|
1794
|
+
let accepted = false;
|
|
1795
|
+
for (let index = 0; index < include.length; index++) {
|
|
1796
|
+
if (include[index] === binding.id) {
|
|
1797
|
+
accepted = true;
|
|
1798
|
+
break;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (!accepted)
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
const exclude = this._mask?.exclude;
|
|
1805
|
+
if (exclude) {
|
|
1806
|
+
for (let index = 0; index < exclude.length; index++) {
|
|
1807
|
+
if (exclude[index] === binding.id)
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return true;
|
|
1812
|
+
}
|
|
1813
|
+
flushPendingEffects(out) {
|
|
1814
|
+
for (let index = 0; index < this._pendingEffects.length; index++) {
|
|
1815
|
+
out.emit(this._pendingEffects[index]);
|
|
1816
|
+
}
|
|
1817
|
+
this._pendingEffects.length = 0;
|
|
1818
|
+
}
|
|
1819
|
+
advance(mixerDelta, out) {
|
|
1820
|
+
if (!this._valid)
|
|
1821
|
+
return;
|
|
1822
|
+
this._finishFade(this._owner.time);
|
|
1823
|
+
this.flushPendingEffects(out);
|
|
1824
|
+
if (this._paused || this._status !== 'running')
|
|
1825
|
+
return;
|
|
1826
|
+
if (!this._effectsStarted) {
|
|
1827
|
+
this._emitInitialEffects(out);
|
|
1828
|
+
this._effectsStarted = true;
|
|
1829
|
+
}
|
|
1830
|
+
const actionDelta = mixerDelta * this._timeScale;
|
|
1831
|
+
if (actionDelta === 0 || this.clip.duration === 0)
|
|
1832
|
+
return;
|
|
1833
|
+
const before = this._rawTime;
|
|
1834
|
+
let after = before + actionDelta;
|
|
1835
|
+
let terminalDirection = 0;
|
|
1836
|
+
const totalDuration = this._loop === 'once'
|
|
1837
|
+
? this.clip.duration
|
|
1838
|
+
: this._repetitions === Infinity
|
|
1839
|
+
? Infinity
|
|
1840
|
+
: this.clip.duration * this._repetitions;
|
|
1841
|
+
if (totalDuration !== Infinity) {
|
|
1842
|
+
if (actionDelta > 0 && after >= totalDuration) {
|
|
1843
|
+
after = totalDuration;
|
|
1844
|
+
terminalDirection = 1;
|
|
1845
|
+
}
|
|
1846
|
+
else if (actionDelta < 0 && after <= 0) {
|
|
1847
|
+
after = 0;
|
|
1848
|
+
terminalDirection = -1;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
this._emitEffectCrossings(before, after, out, terminalDirection);
|
|
1852
|
+
this._rawTime = after;
|
|
1853
|
+
this._setSampleTime(terminalDirection);
|
|
1854
|
+
this._syncEffectActivity(terminalDirection);
|
|
1855
|
+
if (terminalDirection !== 0) {
|
|
1856
|
+
this._status = 'finished';
|
|
1857
|
+
if (!this._clampWhenFinished)
|
|
1858
|
+
this._queueActiveExits();
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
/** @internal Allocates one checkpoint that can be reused for every update. */
|
|
1862
|
+
createSynchronizedState() {
|
|
1863
|
+
return {
|
|
1864
|
+
status: this._status,
|
|
1865
|
+
paused: this._paused,
|
|
1866
|
+
time: this._time,
|
|
1867
|
+
rawTime: this._rawTime,
|
|
1868
|
+
timeScale: this._timeScale,
|
|
1869
|
+
weight: this._weight,
|
|
1870
|
+
loop: this._loop,
|
|
1871
|
+
repetitions: this._repetitions,
|
|
1872
|
+
clampWhenFinished: this._clampWhenFinished,
|
|
1873
|
+
blendMode: this._blendMode,
|
|
1874
|
+
mask: this._mask,
|
|
1875
|
+
layer: this._layer,
|
|
1876
|
+
effectsStarted: this._effectsStarted,
|
|
1877
|
+
fadeActive: this._fadeActive,
|
|
1878
|
+
fadeStart: this._fadeStart,
|
|
1879
|
+
fadeEnd: this._fadeEnd,
|
|
1880
|
+
fadeFrom: this._fadeFrom,
|
|
1881
|
+
fadeTo: this._fadeTo,
|
|
1882
|
+
fadeValue: this._fadeValue,
|
|
1883
|
+
effectActive: new Uint8Array(this._effectActive.length),
|
|
1884
|
+
pendingEffects: [],
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
/** @internal Captures playhead, fade, and effect-cursor state without sampling. */
|
|
1888
|
+
captureSynchronizedState(state) {
|
|
1889
|
+
this._assertValid();
|
|
1890
|
+
if (state.effectActive.length !== this._effectActive.length) {
|
|
1891
|
+
throw new RangeError('Animation2D action checkpoint has an incompatible effect count.');
|
|
1892
|
+
}
|
|
1893
|
+
state.status = this._status;
|
|
1894
|
+
state.paused = this._paused;
|
|
1895
|
+
state.time = this._time;
|
|
1896
|
+
state.rawTime = this._rawTime;
|
|
1897
|
+
state.timeScale = this._timeScale;
|
|
1898
|
+
state.weight = this._weight;
|
|
1899
|
+
state.loop = this._loop;
|
|
1900
|
+
state.repetitions = this._repetitions;
|
|
1901
|
+
state.clampWhenFinished = this._clampWhenFinished;
|
|
1902
|
+
state.blendMode = this._blendMode;
|
|
1903
|
+
state.mask = this._mask;
|
|
1904
|
+
state.layer = this._layer;
|
|
1905
|
+
state.effectsStarted = this._effectsStarted;
|
|
1906
|
+
state.fadeActive = this._fadeActive;
|
|
1907
|
+
state.fadeStart = this._fadeStart;
|
|
1908
|
+
state.fadeEnd = this._fadeEnd;
|
|
1909
|
+
state.fadeFrom = this._fadeFrom;
|
|
1910
|
+
state.fadeTo = this._fadeTo;
|
|
1911
|
+
state.fadeValue = this._fadeValue;
|
|
1912
|
+
state.effectActive.set(this._effectActive);
|
|
1913
|
+
state.pendingEffects.length = 0;
|
|
1914
|
+
for (const effect of this._pendingEffects)
|
|
1915
|
+
state.pendingEffects.push(effect);
|
|
1916
|
+
}
|
|
1917
|
+
/** @internal Restores an exact controller transaction checkpoint. */
|
|
1918
|
+
restoreSynchronizedState(state) {
|
|
1919
|
+
this._assertValid();
|
|
1920
|
+
if (state.effectActive.length !== this._effectActive.length) {
|
|
1921
|
+
throw new RangeError('Animation2D action checkpoint has an incompatible effect count.');
|
|
1922
|
+
}
|
|
1923
|
+
this._status = state.status;
|
|
1924
|
+
this._paused = state.paused;
|
|
1925
|
+
this._time = state.time;
|
|
1926
|
+
this._rawTime = state.rawTime;
|
|
1927
|
+
this._timeScale = state.timeScale;
|
|
1928
|
+
this._weight = state.weight;
|
|
1929
|
+
this._loop = state.loop;
|
|
1930
|
+
this._repetitions = state.repetitions;
|
|
1931
|
+
this._clampWhenFinished = state.clampWhenFinished;
|
|
1932
|
+
this._blendMode = state.blendMode;
|
|
1933
|
+
this._mask = state.mask;
|
|
1934
|
+
this._layer = state.layer;
|
|
1935
|
+
this._effectsStarted = state.effectsStarted;
|
|
1936
|
+
this._fadeActive = state.fadeActive;
|
|
1937
|
+
this._fadeStart = state.fadeStart;
|
|
1938
|
+
this._fadeEnd = state.fadeEnd;
|
|
1939
|
+
this._fadeFrom = state.fadeFrom;
|
|
1940
|
+
this._fadeTo = state.fadeTo;
|
|
1941
|
+
this._fadeValue = state.fadeValue;
|
|
1942
|
+
this._effectActive.set(state.effectActive);
|
|
1943
|
+
this._pendingEffects.length = 0;
|
|
1944
|
+
for (const effect of state.pendingEffects)
|
|
1945
|
+
this._pendingEffects.push(effect);
|
|
1946
|
+
this._resetSamplerCursors();
|
|
1947
|
+
}
|
|
1948
|
+
invalidate() {
|
|
1949
|
+
if (!this._valid)
|
|
1950
|
+
return;
|
|
1951
|
+
this._valid = false;
|
|
1952
|
+
this._status = 'stopped';
|
|
1953
|
+
this._paused = false;
|
|
1954
|
+
this._fadeActive = false;
|
|
1955
|
+
this._pendingEffects.length = 0;
|
|
1956
|
+
this._effectActive.fill(0);
|
|
1957
|
+
}
|
|
1958
|
+
_setSampleTime(terminalDirection) {
|
|
1959
|
+
const duration = this.clip.duration;
|
|
1960
|
+
if (duration === 0) {
|
|
1961
|
+
this._time = 0;
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (this._loop === 'once') {
|
|
1965
|
+
this._time = Math.min(duration, Math.max(0, this._rawTime));
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
if (terminalDirection > 0 && this._loop === 'repeat') {
|
|
1969
|
+
this._time = duration;
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
const traversal = Math.floor(this._rawTime / duration);
|
|
1973
|
+
let phase = this._rawTime - traversal * duration;
|
|
1974
|
+
if (phase < 0)
|
|
1975
|
+
phase += duration;
|
|
1976
|
+
if (this._loop === 'repeat') {
|
|
1977
|
+
this._time = phase;
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
this._time = isEven(traversal) ? phase : duration - phase;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
_emitInitialEffects(out) {
|
|
1984
|
+
for (let index = 0; index < this.runtimeEffects.length; index++) {
|
|
1985
|
+
const active = effectActiveAt(this.runtimeEffects[index], this._time, this.clip.duration);
|
|
1986
|
+
this._effectActive[index] = active ? 1 : 0;
|
|
1987
|
+
if (active)
|
|
1988
|
+
out.emit(this.runtimeEffects[index].enterEvent);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
_emitEffectCrossings(before, after, out, terminalDirection = 0) {
|
|
1992
|
+
if (before === after || this.runtimeEffects.length === 0)
|
|
1993
|
+
return;
|
|
1994
|
+
if (this._loop === 'repeat') {
|
|
1995
|
+
this._emitRepeatEffectCrossings(before, after, out, terminalDirection);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (this._loop === 'once') {
|
|
1999
|
+
this._emitOnceEffectCrossings(before, after, out);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
this._emitPingPongEffectCrossings(before, after, out, terminalDirection);
|
|
2003
|
+
}
|
|
2004
|
+
_emitOnceEffectCrossings(before, after, out) {
|
|
2005
|
+
for (let index = 0; index < this.runtimeEffects.length; index++) {
|
|
2006
|
+
const effect = this.runtimeEffects[index];
|
|
2007
|
+
if (after > before) {
|
|
2008
|
+
if (effect.start > before && effect.start <= after)
|
|
2009
|
+
out.emit(effect.enterEvent);
|
|
2010
|
+
if (effect.end > before && effect.end <= after)
|
|
2011
|
+
out.emit(effect.exitEvent);
|
|
2012
|
+
}
|
|
2013
|
+
else {
|
|
2014
|
+
if (effect.end < before && effect.end >= after)
|
|
2015
|
+
out.emit(effect.enterEvent);
|
|
2016
|
+
if (effect.start < before && effect.start >= after)
|
|
2017
|
+
out.emit(effect.exitEvent);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
_emitRepeatEffectCrossings(before, after, out, terminalDirection) {
|
|
2022
|
+
const duration = this.clip.duration;
|
|
2023
|
+
const minimum = Math.min(before, after);
|
|
2024
|
+
const maximum = Math.max(before, after);
|
|
2025
|
+
const firstCycle = Math.floor(minimum / duration) - 1;
|
|
2026
|
+
const lastCycle = Math.floor(maximum / duration) + 1;
|
|
2027
|
+
for (let effectIndex = 0; effectIndex < this.runtimeEffects.length; effectIndex++) {
|
|
2028
|
+
const effect = this.runtimeEffects[effectIndex];
|
|
2029
|
+
if (effect.fullSpan) {
|
|
2030
|
+
if (after > before) {
|
|
2031
|
+
for (let cycle = firstCycle; cycle <= lastCycle; cycle++) {
|
|
2032
|
+
const boundary = cycle * duration;
|
|
2033
|
+
if (boundary > before && boundary <= after) {
|
|
2034
|
+
if (terminalDirection > 0 && boundary === after)
|
|
2035
|
+
out.emit(effect.exitEvent);
|
|
2036
|
+
else
|
|
2037
|
+
this._emitLoopEffect(effect, out);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
for (let cycle = lastCycle; cycle >= firstCycle; cycle--) {
|
|
2043
|
+
const boundary = cycle * duration;
|
|
2044
|
+
if (boundary < before && boundary >= after) {
|
|
2045
|
+
if (terminalDirection < 0 && boundary === after)
|
|
2046
|
+
out.emit(effect.exitEvent);
|
|
2047
|
+
else
|
|
2048
|
+
this._emitLoopEffect(effect, out);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (after > before) {
|
|
2055
|
+
for (let cycle = firstCycle; cycle <= lastCycle; cycle++) {
|
|
2056
|
+
const cycleStart = cycle * duration;
|
|
2057
|
+
const enter = cycleStart + effect.start;
|
|
2058
|
+
const exit = cycleStart + effect.end;
|
|
2059
|
+
if (enter > before && enter <= after
|
|
2060
|
+
&& !(terminalDirection > 0 && enter === after))
|
|
2061
|
+
out.emit(effect.enterEvent);
|
|
2062
|
+
if (exit > before && exit <= after)
|
|
2063
|
+
out.emit(effect.exitEvent);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
else {
|
|
2067
|
+
for (let cycle = lastCycle; cycle >= firstCycle; cycle--) {
|
|
2068
|
+
const cycleStart = cycle * duration;
|
|
2069
|
+
const enter = cycleStart + effect.end;
|
|
2070
|
+
const exit = cycleStart + effect.start;
|
|
2071
|
+
if (enter < before && enter >= after
|
|
2072
|
+
&& !(terminalDirection < 0 && enter === after))
|
|
2073
|
+
out.emit(effect.enterEvent);
|
|
2074
|
+
if (exit < before && exit >= after)
|
|
2075
|
+
out.emit(effect.exitEvent);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
_emitPingPongEffectCrossings(before, after, out, terminalDirection) {
|
|
2081
|
+
const duration = this.clip.duration;
|
|
2082
|
+
const minimum = Math.min(before, after);
|
|
2083
|
+
const maximum = Math.max(before, after);
|
|
2084
|
+
const firstTraversal = Math.floor(minimum / duration) - 1;
|
|
2085
|
+
const lastTraversal = Math.floor(maximum / duration) + 1;
|
|
2086
|
+
for (let effectIndex = 0; effectIndex < this.runtimeEffects.length; effectIndex++) {
|
|
2087
|
+
const effect = this.runtimeEffects[effectIndex];
|
|
2088
|
+
if (effect.fullSpan) {
|
|
2089
|
+
if (after > before) {
|
|
2090
|
+
for (let traversal = firstTraversal; traversal <= lastTraversal; traversal++) {
|
|
2091
|
+
const boundary = traversal * duration;
|
|
2092
|
+
if (boundary <= before || boundary > after)
|
|
2093
|
+
continue;
|
|
2094
|
+
if (terminalDirection > 0 && boundary === after)
|
|
2095
|
+
out.emit(effect.exitEvent);
|
|
2096
|
+
else
|
|
2097
|
+
this._emitLoopEffect(effect, out);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
else {
|
|
2101
|
+
for (let traversal = lastTraversal; traversal >= firstTraversal; traversal--) {
|
|
2102
|
+
const boundary = traversal * duration;
|
|
2103
|
+
if (boundary >= before || boundary < after)
|
|
2104
|
+
continue;
|
|
2105
|
+
if (terminalDirection < 0 && boundary === after)
|
|
2106
|
+
out.emit(effect.exitEvent);
|
|
2107
|
+
else
|
|
2108
|
+
this._emitLoopEffect(effect, out);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
if (after > before) {
|
|
2114
|
+
for (let traversal = firstTraversal; traversal <= lastTraversal; traversal++) {
|
|
2115
|
+
const traversalStart = traversal * duration;
|
|
2116
|
+
const forward = isEven(traversal);
|
|
2117
|
+
const enter = traversalStart + (forward ? effect.start : duration - effect.end);
|
|
2118
|
+
const exit = traversalStart + (forward ? effect.end : duration - effect.start);
|
|
2119
|
+
if (enter > before && enter <= after
|
|
2120
|
+
&& !(terminalDirection > 0 && enter === after))
|
|
2121
|
+
out.emit(effect.enterEvent);
|
|
2122
|
+
if (exit > before && exit <= after)
|
|
2123
|
+
out.emit(effect.exitEvent);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
else {
|
|
2127
|
+
for (let traversal = lastTraversal; traversal >= firstTraversal; traversal--) {
|
|
2128
|
+
const traversalStart = traversal * duration;
|
|
2129
|
+
const forward = isEven(traversal);
|
|
2130
|
+
const forwardEnter = traversalStart
|
|
2131
|
+
+ (forward ? effect.start : duration - effect.end);
|
|
2132
|
+
const forwardExit = traversalStart
|
|
2133
|
+
+ (forward ? effect.end : duration - effect.start);
|
|
2134
|
+
if (forwardExit < before && forwardExit >= after
|
|
2135
|
+
&& !(terminalDirection < 0 && forwardExit === after))
|
|
2136
|
+
out.emit(effect.enterEvent);
|
|
2137
|
+
if (forwardEnter < before && forwardEnter >= after)
|
|
2138
|
+
out.emit(effect.exitEvent);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
_emitLoopEffect(effect, out) {
|
|
2144
|
+
out.emit(effect.loopBehavior === 'restart' ? effect.restartEvent : effect.loopEvent);
|
|
2145
|
+
}
|
|
2146
|
+
_syncEffectActivity(terminalDirection = 0) {
|
|
2147
|
+
for (let index = 0; index < this.runtimeEffects.length; index++) {
|
|
2148
|
+
const effect = this.runtimeEffects[index];
|
|
2149
|
+
const active = effect.fullSpan && this._loop === 'ping-pong'
|
|
2150
|
+
? terminalDirection === 0
|
|
2151
|
+
: effectActiveAt(effect, this._time, this.clip.duration);
|
|
2152
|
+
this._effectActive[index] = active ? 1 : 0;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
_queueActiveExits() {
|
|
2156
|
+
for (let index = 0; index < this._effectActive.length; index++) {
|
|
2157
|
+
if (this._effectActive[index] === 0)
|
|
2158
|
+
continue;
|
|
2159
|
+
this._pendingEffects.push(this.runtimeEffects[index].exitEvent);
|
|
2160
|
+
this._effectActive[index] = 0;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
_scheduleFade(from, to, duration) {
|
|
2164
|
+
this._fadeFrom = from;
|
|
2165
|
+
this._fadeTo = to;
|
|
2166
|
+
this._fadeStart = this._owner.time;
|
|
2167
|
+
this._fadeEnd = this._fadeStart + duration;
|
|
2168
|
+
if (duration === 0) {
|
|
2169
|
+
this._weight = to;
|
|
2170
|
+
this._fadeValue = to;
|
|
2171
|
+
this._fadeActive = false;
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
this._fadeValue = from;
|
|
2175
|
+
this._fadeActive = true;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
_fadeFactor(time) {
|
|
2179
|
+
if (!this._fadeActive)
|
|
2180
|
+
return this._fadeValue;
|
|
2181
|
+
if (time <= this._fadeStart)
|
|
2182
|
+
return this._fadeFrom;
|
|
2183
|
+
if (time >= this._fadeEnd)
|
|
2184
|
+
return this._fadeTo;
|
|
2185
|
+
const alpha = (time - this._fadeStart) / (this._fadeEnd - this._fadeStart);
|
|
2186
|
+
return this._fadeFrom + (this._fadeTo - this._fadeFrom) * alpha;
|
|
2187
|
+
}
|
|
2188
|
+
_finishFade(time) {
|
|
2189
|
+
if (!this._fadeActive || time < this._fadeEnd)
|
|
2190
|
+
return;
|
|
2191
|
+
this._weight = this._fadeTo;
|
|
2192
|
+
this._fadeValue = this._fadeTo;
|
|
2193
|
+
this._fadeActive = false;
|
|
2194
|
+
}
|
|
2195
|
+
_assertSibling(action) {
|
|
2196
|
+
this._assertValid();
|
|
2197
|
+
if (action._owner !== this._owner || !action._valid) {
|
|
2198
|
+
throw new RangeError('Animation2D cross-fade actions must belong to the same active mixer.');
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
_clampSynchronizedTime(timeSeconds) {
|
|
2202
|
+
const next = finiteNumber(timeSeconds, 'Animation2DAction synchronized time');
|
|
2203
|
+
if (this._loop === 'once')
|
|
2204
|
+
return Math.min(this.clip.duration, Math.max(0, next));
|
|
2205
|
+
if (this._repetitions === Infinity)
|
|
2206
|
+
return next;
|
|
2207
|
+
return Math.min(this.clip.duration * this._repetitions, Math.max(0, next));
|
|
2208
|
+
}
|
|
2209
|
+
_terminalDirection(time) {
|
|
2210
|
+
if (this._loop === 'once') {
|
|
2211
|
+
if (time === this.clip.duration && this._rawTime < time)
|
|
2212
|
+
return 1;
|
|
2213
|
+
if (time === 0 && this._rawTime > time)
|
|
2214
|
+
return -1;
|
|
2215
|
+
return 0;
|
|
2216
|
+
}
|
|
2217
|
+
if (this._repetitions === Infinity)
|
|
2218
|
+
return 0;
|
|
2219
|
+
const totalDuration = this.clip.duration * this._repetitions;
|
|
2220
|
+
if (time === totalDuration && this._rawTime < time)
|
|
2221
|
+
return 1;
|
|
2222
|
+
if (time === 0 && this._rawTime > time)
|
|
2223
|
+
return -1;
|
|
2224
|
+
return 0;
|
|
2225
|
+
}
|
|
2226
|
+
_resetSamplerCursors() {
|
|
2227
|
+
for (let index = 0; index < this.runtimeTracks.length; index++) {
|
|
2228
|
+
this.runtimeTracks[index].sampler.resetCursor();
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
_assertValid() {
|
|
2232
|
+
this._owner.assertActive();
|
|
2233
|
+
if (!this._valid)
|
|
2234
|
+
throw new Error(`Animation2D action "${this.id}" is no longer valid.`);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
function effectEvent(actionId, clipId, cue, lifecycle) {
|
|
2238
|
+
return { actionId, clipId, cue, lifecycle };
|
|
2239
|
+
}
|
|
2240
|
+
function effectActiveAt(effect, time, duration) {
|
|
2241
|
+
if (effect.fullSpan)
|
|
2242
|
+
return time >= 0 && time < duration;
|
|
2243
|
+
return time >= effect.start && time < effect.end;
|
|
2244
|
+
}
|
|
2245
|
+
function validateClip(clip) {
|
|
2246
|
+
if (clip.format !== 'haiyue-animation2d-clip@1') {
|
|
2247
|
+
throw new RangeError(`Unsupported Animation2D clip format "${String(clip.format)}".`);
|
|
2248
|
+
}
|
|
2249
|
+
if (!clip.id)
|
|
2250
|
+
throw new RangeError('Animation2D clip id must not be empty.');
|
|
2251
|
+
if (!Number.isFinite(clip.duration) || clip.duration < 0) {
|
|
2252
|
+
throw new RangeError(`Animation2D clip duration must be finite and non-negative; received ${clip.duration}.`);
|
|
2253
|
+
}
|
|
2254
|
+
for (let index = 0; index < clip.tracks.length; index++) {
|
|
2255
|
+
const track = clip.tracks[index];
|
|
2256
|
+
const first = track.times[0];
|
|
2257
|
+
const last = track.times[track.times.length - 1];
|
|
2258
|
+
if (first !== undefined && (first < 0 || last > clip.duration)) {
|
|
2259
|
+
throw new RangeError(`Animation2D track "${track.id}" keys must lie within its clip.`);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
function validateEffect(cue, start, end, duration) {
|
|
2264
|
+
if (!cue.id)
|
|
2265
|
+
throw new RangeError('Animation2D effect cue id must not be empty.');
|
|
2266
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)
|
|
2267
|
+
|| start < 0 || end < start || end > duration) {
|
|
2268
|
+
throw new RangeError(`Animation2D effect "${cue.id}" interval must lie within its clip.`);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
function validateRepetitions(value) {
|
|
2272
|
+
if (value === Infinity)
|
|
2273
|
+
return value;
|
|
2274
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2275
|
+
throw new RangeError(`Animation2D repetitions must be a positive integer or Infinity; received ${value}.`);
|
|
2276
|
+
}
|
|
2277
|
+
return value;
|
|
2278
|
+
}
|
|
2279
|
+
function finiteNumber(value, property) {
|
|
2280
|
+
if (!Number.isFinite(value))
|
|
2281
|
+
throw new RangeError(`${property} must be finite; received ${value}.`);
|
|
2282
|
+
return value;
|
|
2283
|
+
}
|
|
2284
|
+
function nonNegativeNumber(value, property) {
|
|
2285
|
+
const result = finiteNumber(value, property);
|
|
2286
|
+
if (result < 0)
|
|
2287
|
+
throw new RangeError(`${property} must be non-negative; received ${value}.`);
|
|
2288
|
+
return result;
|
|
2289
|
+
}
|
|
2290
|
+
function integerNumber(value, property) {
|
|
2291
|
+
if (!Number.isInteger(value))
|
|
2292
|
+
throw new RangeError(`${property} must be an integer; received ${value}.`);
|
|
2293
|
+
return value;
|
|
2294
|
+
}
|
|
2295
|
+
function isEven(value) {
|
|
2296
|
+
return Math.abs(value % 2) === 0;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
/**
|
|
2300
|
+
* Format-independent 2D pose mixer and shared state-machine mixer port.
|
|
2301
|
+
*
|
|
2302
|
+
* Action/track/layer accumulators are retained across frames. Once actions
|
|
2303
|
+
* and their masks/layers have been warmed, update/evaluate allocates no
|
|
2304
|
+
* action- or property-proportional temporary objects.
|
|
2305
|
+
*/
|
|
2306
|
+
class Animation2DMixerRuntime {
|
|
2307
|
+
_clips = new Map();
|
|
2308
|
+
_actions = [];
|
|
2309
|
+
_actionsById = new Map();
|
|
2310
|
+
_accumulators = new Map();
|
|
2311
|
+
_accumulatorList = [];
|
|
2312
|
+
_stateMachineLayers = new Map();
|
|
2313
|
+
_destroyed = false;
|
|
2314
|
+
_time = 0;
|
|
2315
|
+
_timeScale = 1;
|
|
2316
|
+
_nextActionId = 1;
|
|
2317
|
+
_nextActionOrder = 0;
|
|
2318
|
+
_nextStateMachineLayer = 0;
|
|
2319
|
+
constructor(clips = []) {
|
|
2320
|
+
for (let index = 0; index < clips.length; index++)
|
|
2321
|
+
this.registerClip(clips[index]);
|
|
2322
|
+
}
|
|
2323
|
+
get time() { return this._time; }
|
|
2324
|
+
get timeScale() { return this._timeScale; }
|
|
2325
|
+
set timeScale(value) {
|
|
2326
|
+
this.assertActive();
|
|
2327
|
+
if (!Number.isFinite(value)) {
|
|
2328
|
+
throw new RangeError(`Animation2D mixer timeScale must be finite; received ${value}.`);
|
|
2329
|
+
}
|
|
2330
|
+
this._timeScale = value;
|
|
2331
|
+
}
|
|
2332
|
+
get actions() { return this._actions; }
|
|
2333
|
+
get liveBindingCount() { return this._accumulators.size; }
|
|
2334
|
+
registerClip(clip) {
|
|
2335
|
+
this.assertActive();
|
|
2336
|
+
const existing = this._clips.get(clip.id);
|
|
2337
|
+
if (existing && existing !== clip) {
|
|
2338
|
+
throw new RangeError(`Animation2D clip id "${clip.id}" is already registered.`);
|
|
2339
|
+
}
|
|
2340
|
+
this._clips.set(clip.id, clip);
|
|
2341
|
+
return this;
|
|
2342
|
+
}
|
|
2343
|
+
unregisterClip(clipId) {
|
|
2344
|
+
this.assertActive();
|
|
2345
|
+
return this._clips.delete(clipId);
|
|
2346
|
+
}
|
|
2347
|
+
createAction(source, options = {}) {
|
|
2348
|
+
this.assertActive();
|
|
2349
|
+
const clip = typeof source === 'string' ? this._clips.get(source) : source;
|
|
2350
|
+
if (!clip)
|
|
2351
|
+
throw new RangeError(`Animation2D clip "${String(source)}" is not registered.`);
|
|
2352
|
+
const stateOptions = isStateMachineOptions(options) ? options : null;
|
|
2353
|
+
const runtimeOptions = stateOptions ? null : options;
|
|
2354
|
+
const layer = stateOptions
|
|
2355
|
+
? this._stateMachineLayer(stateOptions.layerId)
|
|
2356
|
+
: runtimeOptions.layer ?? 0;
|
|
2357
|
+
const id = runtimeOptions?.id
|
|
2358
|
+
? runtimeOptions.id
|
|
2359
|
+
: `${clip.id}:${stateOptions?.stateId ?? 'action'}:${this._nextActionId++}`;
|
|
2360
|
+
if (!id)
|
|
2361
|
+
throw new RangeError('Animation2D action id must not be empty.');
|
|
2362
|
+
if (this._actionsById.has(id)) {
|
|
2363
|
+
throw new RangeError(`Animation2D action id "${id}" already exists.`);
|
|
2364
|
+
}
|
|
2365
|
+
const actionOptions = stateOptions
|
|
2366
|
+
? {
|
|
2367
|
+
id,
|
|
2368
|
+
layer,
|
|
2369
|
+
loop: stateOptions.loop,
|
|
2370
|
+
blendMode: stateOptions.blendMode,
|
|
2371
|
+
...(stateOptions.mask ? { mask: stateOptions.mask } : {}),
|
|
2372
|
+
}
|
|
2373
|
+
: { ...runtimeOptions, id, layer };
|
|
2374
|
+
const action = new Animation2DActionRuntime(this, clip, id, this._nextActionOrder++, actionOptions);
|
|
2375
|
+
for (let index = 0; index < action.runtimeTracks.length; index++) {
|
|
2376
|
+
this._retainBinding(action.runtimeTracks[index].track.binding, layer);
|
|
2377
|
+
}
|
|
2378
|
+
this._actions.push(action);
|
|
2379
|
+
this._actionsById.set(id, action);
|
|
2380
|
+
return action;
|
|
2381
|
+
}
|
|
2382
|
+
getAction(actionId) {
|
|
2383
|
+
this.assertActive();
|
|
2384
|
+
return this._actionsById.get(actionId) ?? null;
|
|
2385
|
+
}
|
|
2386
|
+
removeAction(action) {
|
|
2387
|
+
this.assertActive();
|
|
2388
|
+
const runtime = typeof action === 'string' ? this._actionsById.get(action) : action;
|
|
2389
|
+
if (!runtime || this._actionsById.get(runtime.id) !== runtime)
|
|
2390
|
+
return false;
|
|
2391
|
+
const index = this._actions.indexOf(runtime);
|
|
2392
|
+
if (index >= 0)
|
|
2393
|
+
this._actions.splice(index, 1);
|
|
2394
|
+
this._actionsById.delete(runtime.id);
|
|
2395
|
+
for (let trackIndex = 0; trackIndex < runtime.runtimeTracks.length; trackIndex++) {
|
|
2396
|
+
this._releaseBinding(runtime.runtimeTracks[trackIndex].track.binding);
|
|
2397
|
+
}
|
|
2398
|
+
runtime.invalidate();
|
|
2399
|
+
return true;
|
|
2400
|
+
}
|
|
2401
|
+
update(deltaSeconds, out) {
|
|
2402
|
+
this.assertActive();
|
|
2403
|
+
if (!Number.isFinite(deltaSeconds)) {
|
|
2404
|
+
throw new RangeError(`Animation2D mixer delta must be finite; received ${deltaSeconds}.`);
|
|
2405
|
+
}
|
|
2406
|
+
const mixerDelta = deltaSeconds * this._timeScale;
|
|
2407
|
+
this._time += mixerDelta;
|
|
2408
|
+
out.reset(this._time);
|
|
2409
|
+
this._advanceActions(mixerDelta, out);
|
|
2410
|
+
this._evaluateInto(out);
|
|
2411
|
+
return out.seal();
|
|
2412
|
+
}
|
|
2413
|
+
evaluate(out) {
|
|
2414
|
+
this.assertActive();
|
|
2415
|
+
out.reset(this._time);
|
|
2416
|
+
for (let index = 0; index < this._actions.length; index++) {
|
|
2417
|
+
this._actions[index].flushPendingEffects(out);
|
|
2418
|
+
}
|
|
2419
|
+
this._evaluateInto(out);
|
|
2420
|
+
return out.seal();
|
|
2421
|
+
}
|
|
2422
|
+
setMixerTime(timeSeconds, out) {
|
|
2423
|
+
this.assertActive();
|
|
2424
|
+
if (!Number.isFinite(timeSeconds)) {
|
|
2425
|
+
throw new RangeError(`Animation2D mixer time must be finite; received ${timeSeconds}.`);
|
|
2426
|
+
}
|
|
2427
|
+
const delta = timeSeconds - this._time;
|
|
2428
|
+
this._time = timeSeconds;
|
|
2429
|
+
out.reset(this._time);
|
|
2430
|
+
this._advanceActions(delta, out);
|
|
2431
|
+
this._evaluateInto(out);
|
|
2432
|
+
return out.seal();
|
|
2433
|
+
}
|
|
2434
|
+
play(action) {
|
|
2435
|
+
this._assertOwned(action);
|
|
2436
|
+
action.play();
|
|
2437
|
+
}
|
|
2438
|
+
stop(action) {
|
|
2439
|
+
this._assertOwned(action);
|
|
2440
|
+
action.stop();
|
|
2441
|
+
}
|
|
2442
|
+
fade(action, targetWeight, durationSeconds) {
|
|
2443
|
+
this._assertOwned(action);
|
|
2444
|
+
action.fadeTo(targetWeight, durationSeconds);
|
|
2445
|
+
}
|
|
2446
|
+
setWeight(action, weight) {
|
|
2447
|
+
this._assertOwned(action);
|
|
2448
|
+
action.weight = weight;
|
|
2449
|
+
}
|
|
2450
|
+
setTime(action, timeSeconds) {
|
|
2451
|
+
this._assertOwned(action);
|
|
2452
|
+
action.synchronizeTime(timeSeconds);
|
|
2453
|
+
}
|
|
2454
|
+
setTimeScale(action, timeScale) {
|
|
2455
|
+
this._assertOwned(action);
|
|
2456
|
+
action.timeScale = timeScale;
|
|
2457
|
+
}
|
|
2458
|
+
destroyAction(action) {
|
|
2459
|
+
this.removeAction(action);
|
|
2460
|
+
}
|
|
2461
|
+
destroy() {
|
|
2462
|
+
if (this._destroyed)
|
|
2463
|
+
return;
|
|
2464
|
+
for (let index = 0; index < this._actions.length; index++)
|
|
2465
|
+
this._actions[index].invalidate();
|
|
2466
|
+
this._actions.length = 0;
|
|
2467
|
+
this._actionsById.clear();
|
|
2468
|
+
this._accumulators.clear();
|
|
2469
|
+
this._accumulatorList.length = 0;
|
|
2470
|
+
this._clips.clear();
|
|
2471
|
+
this._stateMachineLayers.clear();
|
|
2472
|
+
this._destroyed = true;
|
|
2473
|
+
}
|
|
2474
|
+
assertActive() {
|
|
2475
|
+
if (this._destroyed)
|
|
2476
|
+
throw new Error('Animation2D mixer has been destroyed.');
|
|
2477
|
+
}
|
|
2478
|
+
_advanceActions(mixerDelta, out) {
|
|
2479
|
+
for (let index = 0; index < this._actions.length; index++) {
|
|
2480
|
+
this._actions[index].advance(mixerDelta, out);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
_evaluateInto(out) {
|
|
2484
|
+
for (let index = 0; index < this._accumulatorList.length; index++) {
|
|
2485
|
+
resetAccumulator(this._accumulatorList[index]);
|
|
2486
|
+
}
|
|
2487
|
+
for (let actionIndex = 0; actionIndex < this._actions.length; actionIndex++) {
|
|
2488
|
+
const action = this._actions[actionIndex];
|
|
2489
|
+
const weight = action.effectiveWeight;
|
|
2490
|
+
if (weight <= 0)
|
|
2491
|
+
continue;
|
|
2492
|
+
for (let trackIndex = 0; trackIndex < action.runtimeTracks.length; trackIndex++) {
|
|
2493
|
+
const runtimeTrack = action.runtimeTracks[trackIndex];
|
|
2494
|
+
const binding = runtimeTrack.track.binding;
|
|
2495
|
+
if (!action.acceptsBinding(binding))
|
|
2496
|
+
continue;
|
|
2497
|
+
const accumulator = this._accumulators.get(binding.id);
|
|
2498
|
+
if (!accumulator)
|
|
2499
|
+
continue;
|
|
2500
|
+
let layer = accumulator.layersById.get(action.layer);
|
|
2501
|
+
if (!layer) {
|
|
2502
|
+
layer = createLayerAccumulator(binding, action.layer);
|
|
2503
|
+
insertLayer(accumulator, layer);
|
|
2504
|
+
}
|
|
2505
|
+
accumulator.active = true;
|
|
2506
|
+
const sampled = runtimeTrack.sampler.sample(action.time);
|
|
2507
|
+
if (isNumericBinding(binding)) {
|
|
2508
|
+
accumulateNumeric(layer, binding, runtimeTrack, sampled, weight, action.blendMode);
|
|
2509
|
+
}
|
|
2510
|
+
else {
|
|
2511
|
+
accumulateDominant(layer, sampled, weight, action.order);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
for (let index = 0; index < this._accumulatorList.length; index++) {
|
|
2516
|
+
const accumulator = this._accumulatorList[index];
|
|
2517
|
+
if (!accumulator.active)
|
|
2518
|
+
continue;
|
|
2519
|
+
if (isNumericBinding(accumulator.binding)) {
|
|
2520
|
+
finalizeNumeric(accumulator);
|
|
2521
|
+
out.writeNumeric(accumulator.binding, accumulator.result);
|
|
2522
|
+
}
|
|
2523
|
+
else {
|
|
2524
|
+
let hasValue = false;
|
|
2525
|
+
let value;
|
|
2526
|
+
for (let layerIndex = 0; layerIndex < accumulator.layers.length; layerIndex++) {
|
|
2527
|
+
const layer = accumulator.layers[layerIndex];
|
|
2528
|
+
if (!layer.dominantSet)
|
|
2529
|
+
continue;
|
|
2530
|
+
hasValue = true;
|
|
2531
|
+
value = layer.dominantValue;
|
|
2532
|
+
}
|
|
2533
|
+
if (hasValue)
|
|
2534
|
+
out.writeDiscrete(accumulator.binding, value);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
_retainBinding(binding, layer) {
|
|
2539
|
+
const existing = this._accumulators.get(binding.id);
|
|
2540
|
+
if (existing) {
|
|
2541
|
+
if (existing.binding.strategy !== binding.strategy
|
|
2542
|
+
|| existing.binding.valueSize !== binding.valueSize) {
|
|
2543
|
+
throw new RangeError(`Animation2D binding id "${binding.id}" has incompatible contracts.`);
|
|
2544
|
+
}
|
|
2545
|
+
existing.references++;
|
|
2546
|
+
if (!existing.layersById.has(layer))
|
|
2547
|
+
insertLayer(existing, createLayerAccumulator(binding, layer));
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
const size = isNumericBinding(binding) ? binding.valueSize : 0;
|
|
2551
|
+
const accumulator = {
|
|
2552
|
+
binding,
|
|
2553
|
+
result: new Float32Array(size),
|
|
2554
|
+
base: new Float32Array(size),
|
|
2555
|
+
layers: [],
|
|
2556
|
+
layersById: new Map(),
|
|
2557
|
+
references: 1,
|
|
2558
|
+
active: false,
|
|
2559
|
+
};
|
|
2560
|
+
writeDefaultBase(binding, accumulator.base);
|
|
2561
|
+
insertLayer(accumulator, createLayerAccumulator(binding, layer));
|
|
2562
|
+
this._accumulators.set(binding.id, accumulator);
|
|
2563
|
+
this._accumulatorList.push(accumulator);
|
|
2564
|
+
}
|
|
2565
|
+
_releaseBinding(binding) {
|
|
2566
|
+
const accumulator = this._accumulators.get(binding.id);
|
|
2567
|
+
if (!accumulator)
|
|
2568
|
+
return;
|
|
2569
|
+
accumulator.references--;
|
|
2570
|
+
if (accumulator.references > 0)
|
|
2571
|
+
return;
|
|
2572
|
+
this._accumulators.delete(binding.id);
|
|
2573
|
+
const index = this._accumulatorList.indexOf(accumulator);
|
|
2574
|
+
if (index >= 0)
|
|
2575
|
+
this._accumulatorList.splice(index, 1);
|
|
2576
|
+
}
|
|
2577
|
+
_stateMachineLayer(layerId) {
|
|
2578
|
+
const existing = this._stateMachineLayers.get(layerId);
|
|
2579
|
+
if (existing !== undefined)
|
|
2580
|
+
return existing;
|
|
2581
|
+
const layer = this._nextStateMachineLayer++;
|
|
2582
|
+
this._stateMachineLayers.set(layerId, layer);
|
|
2583
|
+
return layer;
|
|
2584
|
+
}
|
|
2585
|
+
_assertOwned(action) {
|
|
2586
|
+
this.assertActive();
|
|
2587
|
+
if (this._actionsById.get(action.id) !== action) {
|
|
2588
|
+
throw new RangeError('Animation2D action does not belong to this mixer.');
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
function resetAccumulator(accumulator) {
|
|
2593
|
+
accumulator.active = false;
|
|
2594
|
+
for (let index = 0; index < accumulator.layers.length; index++) {
|
|
2595
|
+
const layer = accumulator.layers[index];
|
|
2596
|
+
layer.overrideSum.fill(0);
|
|
2597
|
+
layer.additiveSum.fill(0);
|
|
2598
|
+
layer.overrideWeight = 0;
|
|
2599
|
+
layer.rotationAnchor = 0;
|
|
2600
|
+
layer.rotationAnchorSet = false;
|
|
2601
|
+
layer.dominantSet = false;
|
|
2602
|
+
layer.dominantValue = undefined;
|
|
2603
|
+
layer.dominantWeight = -1;
|
|
2604
|
+
layer.dominantOrder = Number.MAX_SAFE_INTEGER;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
function createLayerAccumulator(binding, layer) {
|
|
2608
|
+
const size = isNumericBinding(binding) ? binding.valueSize : 0;
|
|
2609
|
+
return {
|
|
2610
|
+
layer,
|
|
2611
|
+
overrideSum: new Float32Array(size),
|
|
2612
|
+
additiveSum: new Float32Array(size),
|
|
2613
|
+
overrideWeight: 0,
|
|
2614
|
+
rotationAnchor: 0,
|
|
2615
|
+
rotationAnchorSet: false,
|
|
2616
|
+
dominantSet: false,
|
|
2617
|
+
dominantValue: undefined,
|
|
2618
|
+
dominantWeight: -1,
|
|
2619
|
+
dominantOrder: Number.MAX_SAFE_INTEGER,
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
function insertLayer(accumulator, layer) {
|
|
2623
|
+
let index = accumulator.layers.length;
|
|
2624
|
+
while (index > 0 && accumulator.layers[index - 1].layer > layer.layer)
|
|
2625
|
+
index--;
|
|
2626
|
+
accumulator.layers.splice(index, 0, layer);
|
|
2627
|
+
accumulator.layersById.set(layer.layer, layer);
|
|
2628
|
+
}
|
|
2629
|
+
function accumulateNumeric(layer, binding, runtimeTrack, sampled, weight, blendMode) {
|
|
2630
|
+
if (blendMode === 'additive') {
|
|
2631
|
+
if (binding.strategy === 'rotation') {
|
|
2632
|
+
layer.additiveSum[0] = layer.additiveSum[0]
|
|
2633
|
+
+ wrapAngle(sampled[0] - runtimeTrack.reference[0]) * weight;
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
for (let index = 0; index < sampled.length; index++) {
|
|
2637
|
+
layer.additiveSum[index] = layer.additiveSum[index]
|
|
2638
|
+
+ (sampled[index] - runtimeTrack.reference[index]) * weight;
|
|
2639
|
+
}
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (binding.strategy === 'rotation') {
|
|
2643
|
+
if (!layer.rotationAnchorSet) {
|
|
2644
|
+
layer.rotationAnchor = sampled[0];
|
|
2645
|
+
layer.rotationAnchorSet = true;
|
|
2646
|
+
}
|
|
2647
|
+
const unwrapped = layer.rotationAnchor + wrapAngle(sampled[0] - layer.rotationAnchor);
|
|
2648
|
+
layer.overrideSum[0] = layer.overrideSum[0] + unwrapped * weight;
|
|
2649
|
+
}
|
|
2650
|
+
else {
|
|
2651
|
+
for (let index = 0; index < sampled.length; index++) {
|
|
2652
|
+
layer.overrideSum[index] = layer.overrideSum[index] + sampled[index] * weight;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
layer.overrideWeight += weight;
|
|
2656
|
+
}
|
|
2657
|
+
function accumulateDominant(layer, value, weight, order) {
|
|
2658
|
+
if (weight < layer.dominantWeight
|
|
2659
|
+
|| (weight === layer.dominantWeight && order >= layer.dominantOrder))
|
|
2660
|
+
return;
|
|
2661
|
+
layer.dominantSet = true;
|
|
2662
|
+
layer.dominantValue = value;
|
|
2663
|
+
layer.dominantWeight = weight;
|
|
2664
|
+
layer.dominantOrder = order;
|
|
2665
|
+
}
|
|
2666
|
+
function finalizeNumeric(accumulator) {
|
|
2667
|
+
const binding = accumulator.binding;
|
|
2668
|
+
const result = accumulator.result;
|
|
2669
|
+
result.set(accumulator.base);
|
|
2670
|
+
for (let layerIndex = 0; layerIndex < accumulator.layers.length; layerIndex++) {
|
|
2671
|
+
const layer = accumulator.layers[layerIndex];
|
|
2672
|
+
const weight = layer.overrideWeight;
|
|
2673
|
+
if (weight > 0) {
|
|
2674
|
+
if (binding.strategy === 'rotation') {
|
|
2675
|
+
const target = layer.overrideSum[0] / weight;
|
|
2676
|
+
const current = target + wrapAngle(result[0] - target);
|
|
2677
|
+
const alpha = Math.min(1, weight);
|
|
2678
|
+
result[0] = current + (target - current) * alpha;
|
|
2679
|
+
}
|
|
2680
|
+
else if (weight < 1) {
|
|
2681
|
+
const baseWeight = 1 - weight;
|
|
2682
|
+
for (let index = 0; index < result.length; index++) {
|
|
2683
|
+
result[index] = layer.overrideSum[index] + result[index] * baseWeight;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
else {
|
|
2687
|
+
const inverseWeight = 1 / weight;
|
|
2688
|
+
for (let index = 0; index < result.length; index++) {
|
|
2689
|
+
result[index] = layer.overrideSum[index] * inverseWeight;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
for (let index = 0; index < result.length; index++) {
|
|
2694
|
+
result[index] = result[index] + layer.additiveSum[index];
|
|
2695
|
+
}
|
|
2696
|
+
if (binding.strategy === 'rotation')
|
|
2697
|
+
result[0] = wrapAngle(result[0]);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function writeDefaultBase(binding, out) {
|
|
2701
|
+
const authored = binding.defaultValue;
|
|
2702
|
+
if (authored && typeof authored === 'object' && 'length' in authored) {
|
|
2703
|
+
const values = authored;
|
|
2704
|
+
for (let index = 0; index < out.length; index++)
|
|
2705
|
+
out[index] = values[index] ?? 0;
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
out.fill(0);
|
|
2709
|
+
if (binding.path === 'transform.scale'
|
|
2710
|
+
|| binding.path === 'opacity'
|
|
2711
|
+
|| binding.path === 'color')
|
|
2712
|
+
out.fill(1);
|
|
2713
|
+
}
|
|
2714
|
+
function isStateMachineOptions(options) {
|
|
2715
|
+
return 'layerId' in options && 'stateId' in options;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
class ReusablePoseChannel {
|
|
2719
|
+
binding;
|
|
2720
|
+
value;
|
|
2721
|
+
_numeric = new Float32Array(0);
|
|
2722
|
+
constructor(binding) {
|
|
2723
|
+
this.binding = binding;
|
|
2724
|
+
}
|
|
2725
|
+
writeNumeric(binding, source) {
|
|
2726
|
+
const size = numericSize(binding);
|
|
2727
|
+
this.binding = binding;
|
|
2728
|
+
if (this._numeric.length !== size)
|
|
2729
|
+
this._numeric = new Float32Array(size);
|
|
2730
|
+
for (let index = 0; index < size; index++)
|
|
2731
|
+
this._numeric[index] = source[index] ?? 0;
|
|
2732
|
+
this.value = this._numeric;
|
|
2733
|
+
}
|
|
2734
|
+
writeDiscrete(binding, value) {
|
|
2735
|
+
this.binding = binding;
|
|
2736
|
+
this.value = value;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* Caller-owned frame-transient pose. Channel records, numeric storage, and
|
|
2741
|
+
* effect arrays are reused after warmup.
|
|
2742
|
+
*/
|
|
2743
|
+
class Animation2DPoseBuffer {
|
|
2744
|
+
_channelPool = [];
|
|
2745
|
+
_channels = [];
|
|
2746
|
+
_effects = [];
|
|
2747
|
+
_sequence = 0;
|
|
2748
|
+
_mixerTime = 0;
|
|
2749
|
+
_sealed = true;
|
|
2750
|
+
get sequence() { return this._sequence; }
|
|
2751
|
+
get mixerTime() { return this._mixerTime; }
|
|
2752
|
+
get channels() { return this._channels; }
|
|
2753
|
+
get effects() { return this._effects; }
|
|
2754
|
+
reset(mixerTime) {
|
|
2755
|
+
if (!Number.isFinite(mixerTime)) {
|
|
2756
|
+
throw new RangeError(`Animation2D pose mixerTime must be finite; received ${mixerTime}.`);
|
|
2757
|
+
}
|
|
2758
|
+
this._mixerTime = mixerTime;
|
|
2759
|
+
this._channels.length = 0;
|
|
2760
|
+
this._effects.length = 0;
|
|
2761
|
+
this._sealed = false;
|
|
2762
|
+
}
|
|
2763
|
+
writeNumeric(binding, value) {
|
|
2764
|
+
const size = numericSize(binding);
|
|
2765
|
+
if (value.length < size) {
|
|
2766
|
+
throw new RangeError(`Animation2D pose channel "${binding.id}" requires ${size} values; received ${value.length}.`);
|
|
2767
|
+
}
|
|
2768
|
+
const channel = this._channelAt(this._channels.length, binding);
|
|
2769
|
+
channel.writeNumeric(binding, value);
|
|
2770
|
+
this._channels.push(channel);
|
|
2771
|
+
}
|
|
2772
|
+
writeDiscrete(binding, value) {
|
|
2773
|
+
const channel = this._channelAt(this._channels.length, binding);
|
|
2774
|
+
channel.writeDiscrete(binding, value);
|
|
2775
|
+
this._channels.push(channel);
|
|
2776
|
+
}
|
|
2777
|
+
emit(effect) {
|
|
2778
|
+
this._effects.push(effect);
|
|
2779
|
+
}
|
|
2780
|
+
seal() {
|
|
2781
|
+
if (!this._sealed) {
|
|
2782
|
+
this._sequence++;
|
|
2783
|
+
this._sealed = true;
|
|
2784
|
+
}
|
|
2785
|
+
return this;
|
|
2786
|
+
}
|
|
2787
|
+
_channelAt(index, binding) {
|
|
2788
|
+
let channel = this._channelPool[index];
|
|
2789
|
+
if (!channel) {
|
|
2790
|
+
channel = new ReusablePoseChannel(binding);
|
|
2791
|
+
this._channelPool.push(channel);
|
|
2792
|
+
}
|
|
2793
|
+
return channel;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
function numericSize(binding) {
|
|
2797
|
+
const size = binding.valueSize ?? 0;
|
|
2798
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
2799
|
+
throw new RangeError(`Animation2D numeric binding "${binding.id}" requires a positive valueSize.`);
|
|
2800
|
+
}
|
|
2801
|
+
return size;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
class Animation2DStateMachineRuntime {
|
|
2805
|
+
extension;
|
|
2806
|
+
_mixer;
|
|
2807
|
+
_adapter;
|
|
2808
|
+
_pose = new Animation2DPoseBuffer();
|
|
2809
|
+
_visual;
|
|
2810
|
+
_destroyed = false;
|
|
2811
|
+
controller;
|
|
2812
|
+
constructor(owner, animation, extension, parameterValues, runtimeExtensions, assetManager) {
|
|
2813
|
+
this.extension = extension;
|
|
2814
|
+
const clips = createHyaAnimation2DClips(animation, extension);
|
|
2815
|
+
this._mixer = new Animation2DMixerRuntime(clips);
|
|
2816
|
+
this._adapter = new Animation2DStateMachineMixerAdapter(this._mixer);
|
|
2817
|
+
let visual;
|
|
2818
|
+
try {
|
|
2819
|
+
this.controller = createAnimation2DStateMachineController(extension.stateMachine, this._adapter);
|
|
2820
|
+
applyParameterValues(this.controller, extension, parameterValues);
|
|
2821
|
+
this.controller.update(0);
|
|
2822
|
+
visual = new Animation2DStateMachineVisualRuntime(owner, animation, runtimeExtensions, assetManager);
|
|
2823
|
+
this._visual = visual;
|
|
2824
|
+
}
|
|
2825
|
+
catch (error) {
|
|
2826
|
+
visual?.destroy();
|
|
2827
|
+
this.controller?.destroy();
|
|
2828
|
+
this._adapter.destroy();
|
|
2829
|
+
this._mixer.destroy();
|
|
2830
|
+
throw error;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
get stats() { return this._visual.stats; }
|
|
2834
|
+
get diagnostics() { return this._visual.diagnostics; }
|
|
2835
|
+
get liveActionCount() { return this._adapter.liveActionCount; }
|
|
2836
|
+
get liveBindingCount() { return this._adapter.liveBindingCount; }
|
|
2837
|
+
get sideEffectOwnerCount() { return this._visual.sideEffectOwnerCount; }
|
|
2838
|
+
get layerSnapshots() {
|
|
2839
|
+
return this.controller.layerSnapshots;
|
|
2840
|
+
}
|
|
2841
|
+
update(deltaSeconds, playing, speed) {
|
|
2842
|
+
this._requireActive();
|
|
2843
|
+
const pose = runAnimationStateMachineControllerUpdateTransaction(this.controller, playing ? deltaSeconds * speed : 0, () => undefined, () => this._mixer.evaluate(this._pose));
|
|
2844
|
+
this._visual.applyPose(pose, playing, speed);
|
|
2845
|
+
}
|
|
2846
|
+
setPlaying(playing) {
|
|
2847
|
+
this._requireActive();
|
|
2848
|
+
this._visual.setPlaying(playing);
|
|
2849
|
+
}
|
|
2850
|
+
reset() {
|
|
2851
|
+
this._requireActive();
|
|
2852
|
+
this._visual.resetSideEffects();
|
|
2853
|
+
this.controller.reset();
|
|
2854
|
+
const pose = runAnimationStateMachineControllerUpdateTransaction(this.controller, 0, () => undefined, () => this._mixer.evaluate(this._pose));
|
|
2855
|
+
this._visual.applyPose(pose, false, 1);
|
|
2856
|
+
}
|
|
2857
|
+
destroy() {
|
|
2858
|
+
if (this._destroyed)
|
|
2859
|
+
return;
|
|
2860
|
+
this._destroyed = true;
|
|
2861
|
+
this.controller.destroy();
|
|
2862
|
+
this._adapter.destroy();
|
|
2863
|
+
this._mixer.destroy();
|
|
2864
|
+
this._visual.destroy();
|
|
2865
|
+
}
|
|
2866
|
+
_requireActive() {
|
|
2867
|
+
if (this._destroyed)
|
|
2868
|
+
throw new Error('Animation2D state-machine runtime has been destroyed.');
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
function applyParameterValues(controller, extension, values) {
|
|
2872
|
+
for (const parameter of extension.stateMachine.parameters) {
|
|
2873
|
+
if (!values.has(parameter.name))
|
|
2874
|
+
continue;
|
|
2875
|
+
const value = values.get(parameter.name);
|
|
2876
|
+
if (parameter.type === 'float')
|
|
2877
|
+
controller.setFloat(parameter.name, value);
|
|
2878
|
+
else if (parameter.type === 'integer')
|
|
2879
|
+
controller.setInteger(parameter.name, value);
|
|
2880
|
+
else if (parameter.type === 'boolean')
|
|
2881
|
+
controller.setBoolean(parameter.name, value);
|
|
2882
|
+
else if (value)
|
|
2883
|
+
controller.setTrigger(parameter.name);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
class Animation2DStateMachineSystem extends System {
|
|
2888
|
+
_assetManager;
|
|
2889
|
+
constructor(options = {}) {
|
|
2890
|
+
super({ all: [Animation2DStateMachineComponent] });
|
|
2891
|
+
this.name = 'Animation2DStateMachineSystem';
|
|
2892
|
+
if (options.priority !== undefined)
|
|
2893
|
+
this.priority = options.priority;
|
|
2894
|
+
this._assetManager = options.assetManager;
|
|
2895
|
+
}
|
|
2896
|
+
update(world, _time, delta) {
|
|
2897
|
+
if (this.disabled)
|
|
2898
|
+
return this;
|
|
2899
|
+
const entities = this.entitySet.get(world);
|
|
2900
|
+
if (!entities)
|
|
2901
|
+
return this;
|
|
2902
|
+
for (const entity of entities)
|
|
2903
|
+
this._updateEntity(entity, delta);
|
|
2904
|
+
return this;
|
|
2905
|
+
}
|
|
2906
|
+
destroy() {
|
|
2907
|
+
for (const world of this.usedBy) {
|
|
2908
|
+
const entities = this.entitySet.get(world);
|
|
2909
|
+
if (!entities)
|
|
2910
|
+
continue;
|
|
2911
|
+
for (const entity of entities) {
|
|
2912
|
+
entity.getComponent(Animation2DStateMachineComponent)?._disposeRuntime();
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return super.destroy();
|
|
2916
|
+
}
|
|
2917
|
+
_updateEntity(entity, delta) {
|
|
2918
|
+
const component = entity.getComponent(Animation2DStateMachineComponent);
|
|
2919
|
+
if (!component)
|
|
2920
|
+
return;
|
|
2921
|
+
if (component.disabled) {
|
|
2922
|
+
component._disposeRuntime();
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
component._runtime ??= new Animation2DStateMachineRuntime(entity, component.animation, component.stateMachineExtension, component._getParameterValues(), component.runtimeExtensions, this._assetManager);
|
|
2926
|
+
if ((!component.playing || component.speed === 0) && !component._needsUpdate)
|
|
2927
|
+
return;
|
|
2928
|
+
component._runtime.update(Math.max(0, delta) * 0.001, component.playing, component.speed);
|
|
2929
|
+
component._needsUpdate = false;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
export { Animation2DStateMachineComponent, Animation2DStateMachineSystem, AnimationStateMachineChannelError, assertAudioStateMachineCompatible, audioStateMachineCompatibilityDiagnostic, createHyaAnimation2DClips, getHyaStateMachineExtension };
|
|
2934
|
+
//# sourceMappingURL=hya-state-machine.js.map
|