@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,1326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANIMATION_VECTOR_SHAPE_EXTENSION_ID,
|
|
3
|
+
AnimationFormatError,
|
|
4
|
+
HYA_STATE_MACHINE_EXTENSION_ID,
|
|
5
|
+
type AnimationAudioComponent,
|
|
6
|
+
type AnimationComponent,
|
|
7
|
+
type AnimationComposite,
|
|
8
|
+
type AnimationCompositeLayer,
|
|
9
|
+
type AnimationLayerEffect,
|
|
10
|
+
type AnimationNode,
|
|
11
|
+
type AnimationPath2DComponent,
|
|
12
|
+
type AnimationParticle2DComponent,
|
|
13
|
+
type AnimationShape2DComponent,
|
|
14
|
+
type AnimationSprite2DComponent,
|
|
15
|
+
type AnimationText2DComponent,
|
|
16
|
+
type AnimationVectorGradientPaint,
|
|
17
|
+
type AnimationVectorShapeComponent,
|
|
18
|
+
type AnimationVectorValueTrack,
|
|
19
|
+
type ParsedAnimation,
|
|
20
|
+
type ParsedAnimationTrack,
|
|
21
|
+
} from '@haiyue/animation-spec';
|
|
22
|
+
import { Entity, Geometry2D, Transform2D } from '@haiyue/engine';
|
|
23
|
+
import { ParticleEmitter2D } from '@haiyue/engine/components';
|
|
24
|
+
import { createRect2D } from '@haiyue/engine/geometry';
|
|
25
|
+
import type { AssetHandle, AssetManager } from '@haiyue/engine/assets';
|
|
26
|
+
import type { Animation2DRuntimeStats } from './Animation2DComponent.js';
|
|
27
|
+
import type { Animation2DExtensionInstance, Animation2DExtensionRegistry } from './Animation2DExtensionRegistry.js';
|
|
28
|
+
import {
|
|
29
|
+
applyVectorPathModifiers,
|
|
30
|
+
tessellateAnimationPath,
|
|
31
|
+
tessellateVectorStrokePath,
|
|
32
|
+
sampleVectorPathMorph,
|
|
33
|
+
type VectorPathMorphComponent,
|
|
34
|
+
type VectorStrokePathComponent,
|
|
35
|
+
} from './AnimationPathTessellator.js';
|
|
36
|
+
import { AnimationVisual2D } from './AnimationVisual2D.js';
|
|
37
|
+
import { AnimationTextRasterizer } from './AnimationTextRasterizer.js';
|
|
38
|
+
import { AnimationAudioClip } from './AnimationAudioClip.js';
|
|
39
|
+
import { createParticle2DEmitter } from './Particle2DDescriptorAdapter.js';
|
|
40
|
+
|
|
41
|
+
export interface Animation2DRuntimeVisual {
|
|
42
|
+
readonly component: AnimationVisual2D;
|
|
43
|
+
readonly sourceComponentIndex: number;
|
|
44
|
+
readonly color: [number, number, number, number];
|
|
45
|
+
readonly baseAlpha: number;
|
|
46
|
+
lastAlpha: number;
|
|
47
|
+
readonly pathMorph?: VectorPathMorphComponent;
|
|
48
|
+
lastMorphTime?: number;
|
|
49
|
+
readonly vectorShape?: AnimationVectorShapeComponent;
|
|
50
|
+
readonly vectorPaint?: 'fill' | 'stroke';
|
|
51
|
+
styleAlpha: number;
|
|
52
|
+
lastVectorTime?: number;
|
|
53
|
+
readonly textRasterizer?: AnimationTextRasterizer;
|
|
54
|
+
readonly sourceEffects?: readonly Readonly<AnimationLayerEffect>[];
|
|
55
|
+
lastEffectTime?: number;
|
|
56
|
+
readonly compositeExpansionTracks?: readonly (Readonly<AnimationVectorValueTrack> | undefined)[];
|
|
57
|
+
lastCompositeTime?: number;
|
|
58
|
+
readonly spriteUvRectTrack?: AnimationVectorValueTrack;
|
|
59
|
+
spriteUvRectFrame?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type CoreVisualComponent =
|
|
63
|
+
| AnimationShape2DComponent
|
|
64
|
+
| AnimationPath2DComponent
|
|
65
|
+
| AnimationSprite2DComponent
|
|
66
|
+
| AnimationText2DComponent
|
|
67
|
+
| AnimationVectorShapeComponent
|
|
68
|
+
| VectorPathMorphComponent
|
|
69
|
+
| VectorStrokePathComponent;
|
|
70
|
+
|
|
71
|
+
interface DeferredCoreVisual {
|
|
72
|
+
readonly component: CoreVisualComponent;
|
|
73
|
+
readonly componentIndex: number;
|
|
74
|
+
readonly order: number;
|
|
75
|
+
readonly sourceGroup: string | undefined;
|
|
76
|
+
readonly composite: Readonly<AnimationComposite> | undefined;
|
|
77
|
+
readonly effects: readonly Readonly<AnimationLayerEffect>[] | undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let nextRuntimeInstanceId = 1;
|
|
81
|
+
|
|
82
|
+
export interface Animation2DRuntimeNode {
|
|
83
|
+
readonly source: Readonly<AnimationNode>;
|
|
84
|
+
readonly entity: Entity;
|
|
85
|
+
readonly content: Entity;
|
|
86
|
+
readonly transform: Transform2D;
|
|
87
|
+
readonly visuals: Animation2DRuntimeVisual[];
|
|
88
|
+
readonly deferredVisuals: DeferredCoreVisual[];
|
|
89
|
+
readonly extensions: Animation2DExtensionInstance[];
|
|
90
|
+
readonly particles: ParticleEmitter2D[];
|
|
91
|
+
readonly audio: AnimationAudioClip[];
|
|
92
|
+
readonly initialX: number;
|
|
93
|
+
readonly initialY: number;
|
|
94
|
+
readonly initialRotation: number;
|
|
95
|
+
readonly initialScaleX: number;
|
|
96
|
+
readonly initialScaleY: number;
|
|
97
|
+
readonly initialOpacity: number;
|
|
98
|
+
opacity: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class Animation2DRuntime {
|
|
102
|
+
private readonly _root: Entity;
|
|
103
|
+
protected readonly _nodes: Animation2DRuntimeNode[] = [];
|
|
104
|
+
protected readonly _nodesById = new Map<string, Animation2DRuntimeNode>();
|
|
105
|
+
private readonly _tracksByNode = new Map<string, ParsedAnimationTrack[]>();
|
|
106
|
+
private readonly _trackCursors: Int32Array;
|
|
107
|
+
private readonly _trackIndices = new Map<ParsedAnimationTrack, number>();
|
|
108
|
+
protected readonly _opacityMemo = new Map<string, number>();
|
|
109
|
+
private readonly _abortController = new AbortController();
|
|
110
|
+
private readonly _assetHandles: AssetHandle<unknown>[] = [];
|
|
111
|
+
private readonly _fontLoads = new Map<string, {
|
|
112
|
+
state: 'loading' | 'loaded' | 'failed';
|
|
113
|
+
readonly rasterizers: Set<AnimationTextRasterizer>;
|
|
114
|
+
}>();
|
|
115
|
+
private readonly _fontFaces: FontFace[] = [];
|
|
116
|
+
private readonly _instanceId = nextRuntimeInstanceId++;
|
|
117
|
+
private _visualCount = 0;
|
|
118
|
+
private _unsupportedComponentCount = 0;
|
|
119
|
+
private _pendingResourceCount = 0;
|
|
120
|
+
private _failedResourceCount = 0;
|
|
121
|
+
private _textCount = 0;
|
|
122
|
+
private _particleCount = 0;
|
|
123
|
+
private _audioCount = 0;
|
|
124
|
+
private _destroyed = false;
|
|
125
|
+
protected _playing = false;
|
|
126
|
+
protected _speed = 1;
|
|
127
|
+
protected _lastAppliedTime = Number.NaN;
|
|
128
|
+
|
|
129
|
+
get stats(): Animation2DRuntimeStats {
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
nodeCount: this._nodes.length,
|
|
132
|
+
visualCount: this._visualCount,
|
|
133
|
+
unsupportedComponentCount: this._unsupportedComponentCount,
|
|
134
|
+
pendingResourceCount: this._pendingResourceCount,
|
|
135
|
+
failedResourceCount: this._failedResourceCount,
|
|
136
|
+
textCount: this._textCount,
|
|
137
|
+
particleCount: this._particleCount,
|
|
138
|
+
audioCount: this._audioCount,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
constructor(
|
|
143
|
+
owner: Entity,
|
|
144
|
+
private readonly _animation: ParsedAnimation,
|
|
145
|
+
runtimeExtensions?: Animation2DExtensionRegistry,
|
|
146
|
+
private readonly _assetManager?: AssetManager,
|
|
147
|
+
) {
|
|
148
|
+
for (const id of _animation.extensionsRequired) {
|
|
149
|
+
if (id === HYA_STATE_MACHINE_EXTENSION_ID) continue;
|
|
150
|
+
if (!runtimeExtensions?.has(id)) {
|
|
151
|
+
throw new AnimationFormatError(
|
|
152
|
+
'E_ANIMATION_MISSING_EXTENSION',
|
|
153
|
+
`Required animation runtime extension "${id}" is not registered.`,
|
|
154
|
+
'$.extensionsRequired',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
this._root = new Entity(`${_animation.name ?? 'Animation'} runtime`);
|
|
159
|
+
this._root.addComponent(new Transform2D({ x: -_animation.canvas.width / 2, y: _animation.canvas.height / 2 }));
|
|
160
|
+
owner.addChild(this._root);
|
|
161
|
+
|
|
162
|
+
const compositeSources = new Set(_animation.nodes.flatMap(node => compositeLayers(node.composite).map(layer => layer.source)));
|
|
163
|
+
const sourceGroupByNode = resolveSourceGroups(_animation.nodes, compositeSources);
|
|
164
|
+
const compositeByNode = resolveInheritedComposites(_animation.nodes, sourceGroupByNode);
|
|
165
|
+
const effectsByNode = resolveInheritedEffects(_animation.nodes, sourceGroupByNode);
|
|
166
|
+
const sourceNodesById = new Map(_animation.nodes.map(node => [node.id, node]));
|
|
167
|
+
let renderOrder = 0;
|
|
168
|
+
for (const node of _animation.nodes) {
|
|
169
|
+
const transformSource = node.transform;
|
|
170
|
+
const position = transformSource?.position ?? [0, 0];
|
|
171
|
+
const scale = transformSource?.scale ?? [1, 1];
|
|
172
|
+
const transform = new Transform2D({
|
|
173
|
+
x: position[0],
|
|
174
|
+
y: -position[1],
|
|
175
|
+
rotation: -(transformSource?.rotation ?? 0),
|
|
176
|
+
scaleX: scale[0],
|
|
177
|
+
scaleY: scale[1],
|
|
178
|
+
});
|
|
179
|
+
const entity = new Entity(node.name ?? node.id).addComponent(transform);
|
|
180
|
+
const anchor = transformSource?.anchor ?? [0, 0];
|
|
181
|
+
const content = new Entity(`${node.name ?? node.id} anchor`).addComponent(new Transform2D({
|
|
182
|
+
x: -anchor[0],
|
|
183
|
+
y: anchor[1],
|
|
184
|
+
}));
|
|
185
|
+
entity.addChild(content);
|
|
186
|
+
const runtimeNode: Animation2DRuntimeNode = {
|
|
187
|
+
source: node,
|
|
188
|
+
entity,
|
|
189
|
+
content,
|
|
190
|
+
transform,
|
|
191
|
+
visuals: [],
|
|
192
|
+
deferredVisuals: [],
|
|
193
|
+
extensions: [],
|
|
194
|
+
particles: [],
|
|
195
|
+
audio: [],
|
|
196
|
+
initialX: transform.x,
|
|
197
|
+
initialY: transform.y,
|
|
198
|
+
initialRotation: transform.rotation,
|
|
199
|
+
initialScaleX: transform.scaleX,
|
|
200
|
+
initialScaleY: transform.scaleY,
|
|
201
|
+
initialOpacity: transformSource?.opacity ?? 1,
|
|
202
|
+
opacity: transformSource?.opacity ?? 1,
|
|
203
|
+
};
|
|
204
|
+
const sourceComponents = node.components ?? [];
|
|
205
|
+
for (let componentIndex = 0; componentIndex < sourceComponents.length; componentIndex++) {
|
|
206
|
+
const component = sourceComponents[componentIndex]!;
|
|
207
|
+
if (isParticle2DComponent(component)) {
|
|
208
|
+
// Particle2D is rendered by the engine's independent pass. Do not silently
|
|
209
|
+
// bypass animation alpha-composite semantics until both renderers share a graph item protocol.
|
|
210
|
+
if (sourceGroupByNode.has(node.id) || compositeByNode.has(node.id)) {
|
|
211
|
+
this._unsupportedComponentCount++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const emitter = createParticle2DEmitter(component);
|
|
215
|
+
content.addChild(new Entity(`${entity.name} particles`).addComponent(emitter));
|
|
216
|
+
runtimeNode.particles.push(emitter);
|
|
217
|
+
this._visualCount++;
|
|
218
|
+
this._particleCount++;
|
|
219
|
+
if (component.resource) this._loadTexture(component.resource, handle => { emitter.textureSource = handle; });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (isAudioComponent(component)) {
|
|
223
|
+
const resource = this._animation.resources.find(candidate => candidate.id === component.resource);
|
|
224
|
+
if (!resource || resource.type !== 'audio') {
|
|
225
|
+
this._unsupportedComponentCount++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
runtimeNode.audio.push(new AnimationAudioClip(resource.uri, component, node, this._animation.duration));
|
|
229
|
+
this._audioCount++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!isCoreVisualComponent(component)) {
|
|
233
|
+
const handler = runtimeExtensions?.get(component.type);
|
|
234
|
+
if (!handler) {
|
|
235
|
+
this._unsupportedComponentCount++;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const instance = handler.create({ animation: _animation, node, component, parent: content });
|
|
240
|
+
if (instance) {
|
|
241
|
+
runtimeNode.extensions.push(instance);
|
|
242
|
+
this._visualCount++;
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this._abortController.abort();
|
|
246
|
+
for (const handle of this._assetHandles) handle.release();
|
|
247
|
+
this._assetHandles.length = 0;
|
|
248
|
+
for (const createdNode of [...this._nodes, runtimeNode]) {
|
|
249
|
+
for (const audio of createdNode.audio) audio.destroy();
|
|
250
|
+
for (const instance of createdNode.extensions) instance.destroy?.();
|
|
251
|
+
}
|
|
252
|
+
this._root.destroy();
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const deferred: DeferredCoreVisual = {
|
|
258
|
+
component,
|
|
259
|
+
componentIndex,
|
|
260
|
+
order: renderOrder++,
|
|
261
|
+
sourceGroup: sourceGroupByNode.get(node.id),
|
|
262
|
+
composite: compositeByNode.get(node.id),
|
|
263
|
+
effects: effectsByNode.get(node.id),
|
|
264
|
+
};
|
|
265
|
+
this._visualCount++;
|
|
266
|
+
if (component.type === 'text2d') this._textCount++;
|
|
267
|
+
if (isSourceNodeActiveAt(node, 0, sourceNodesById, _animation.duration)) {
|
|
268
|
+
this._materializeCoreVisual(runtimeNode, deferred);
|
|
269
|
+
} else {
|
|
270
|
+
runtimeNode.deferredVisuals.push(deferred);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this._nodes.push(runtimeNode);
|
|
274
|
+
this._nodesById.set(node.id, runtimeNode);
|
|
275
|
+
}
|
|
276
|
+
for (const node of this._nodes) {
|
|
277
|
+
const parent = node.source.parent ? this._nodesById.get(node.source.parent)?.content : this._root;
|
|
278
|
+
(parent ?? this._root).addChild(node.entity);
|
|
279
|
+
}
|
|
280
|
+
for (let index = 0; index < _animation.tracks.length; index++) {
|
|
281
|
+
const track = _animation.tracks[index]!;
|
|
282
|
+
let tracks = this._tracksByNode.get(track.node);
|
|
283
|
+
if (!tracks) this._tracksByNode.set(track.node, tracks = []);
|
|
284
|
+
tracks.push(track);
|
|
285
|
+
this._trackIndices.set(track, index);
|
|
286
|
+
}
|
|
287
|
+
this._trackCursors = new Int32Array(_animation.tracks.length);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
apply(time: number, playing = this._playing, speed = this._speed, forceParticleSeek = false): void {
|
|
291
|
+
const discontinuity = forceParticleSeek || (
|
|
292
|
+
!Number.isFinite(this._lastAppliedTime)
|
|
293
|
+
? time > 1e-6
|
|
294
|
+
: time + 1e-6 < this._lastAppliedTime || Math.abs(time - this._lastAppliedTime) > 0.25
|
|
295
|
+
);
|
|
296
|
+
this._playing = playing;
|
|
297
|
+
this._speed = speed;
|
|
298
|
+
this._opacityMemo.clear();
|
|
299
|
+
for (const node of this._nodes) {
|
|
300
|
+
const start = node.source.start ?? 0;
|
|
301
|
+
const end = start + (node.source.duration ?? this._animation.duration);
|
|
302
|
+
node.entity.disabled = time < start || time > end;
|
|
303
|
+
node.transform.setPosition(node.initialX, node.initialY);
|
|
304
|
+
node.transform.rotation = node.initialRotation;
|
|
305
|
+
node.transform.setScale(node.initialScaleX, node.initialScaleY);
|
|
306
|
+
node.opacity = node.initialOpacity;
|
|
307
|
+
for (const track of this._tracksByNode.get(node.source.id) ?? []) this._applyTrack(node, track, time);
|
|
308
|
+
}
|
|
309
|
+
for (const node of this._nodes) {
|
|
310
|
+
const opacity = this._resolveOpacity(node);
|
|
311
|
+
if (node.deferredVisuals.length > 0 && isRuntimeNodeActive(node, this._nodesById)) {
|
|
312
|
+
for (const deferred of node.deferredVisuals.splice(0)) this._materializeCoreVisual(node, deferred);
|
|
313
|
+
}
|
|
314
|
+
for (const particle of node.particles) {
|
|
315
|
+
if (discontinuity) particle.seek(Math.max(0, time - (node.source.start ?? 0)));
|
|
316
|
+
particle.opacity = opacity;
|
|
317
|
+
particle.playing = playing && !node.entity.disabled;
|
|
318
|
+
}
|
|
319
|
+
for (const audio of node.audio) audio.sync(time, playing && !node.entity.disabled, speed, opacity);
|
|
320
|
+
for (const visual of node.visuals) {
|
|
321
|
+
if (visual.spriteUvRectTrack) {
|
|
322
|
+
const frame = findVectorTrackFrame(visual.spriteUvRectTrack, time);
|
|
323
|
+
if (frame !== visual.spriteUvRectFrame) {
|
|
324
|
+
const offset = frame * 4;
|
|
325
|
+
visual.component.setUvRect([
|
|
326
|
+
visual.spriteUvRectTrack.values[offset]!,
|
|
327
|
+
visual.spriteUvRectTrack.values[offset + 1]!,
|
|
328
|
+
visual.spriteUvRectTrack.values[offset + 2]!,
|
|
329
|
+
visual.spriteUvRectTrack.values[offset + 3]!,
|
|
330
|
+
]);
|
|
331
|
+
visual.spriteUvRectFrame = frame;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
visual.textRasterizer?.setTime(time);
|
|
335
|
+
if (visual.pathMorph && visual.lastMorphTime !== time) {
|
|
336
|
+
const next = tessellateAnimationPath(sampleVectorPathMorph(visual.pathMorph, time));
|
|
337
|
+
visual.component.geometry.setData(next.positions, next.indices);
|
|
338
|
+
visual.component.revision++;
|
|
339
|
+
visual.lastMorphTime = time;
|
|
340
|
+
}
|
|
341
|
+
if (visual.vectorShape && visual.lastVectorTime !== undefined && visual.lastVectorTime !== time) {
|
|
342
|
+
updateVectorVisual(visual, time);
|
|
343
|
+
visual.lastVectorTime = time;
|
|
344
|
+
}
|
|
345
|
+
if (visual.sourceEffects && visual.lastEffectTime !== time) {
|
|
346
|
+
updateVisualEffects(visual.component, visual.sourceEffects, time);
|
|
347
|
+
visual.component.revision++;
|
|
348
|
+
visual.lastEffectTime = time;
|
|
349
|
+
}
|
|
350
|
+
if (visual.compositeExpansionTracks && visual.lastCompositeTime !== time) {
|
|
351
|
+
for (let index = 0; index < visual.compositeExpansionTracks.length; index++) {
|
|
352
|
+
const track = visual.compositeExpansionTracks[index];
|
|
353
|
+
if (track) visual.component.setCompositeExpansion(index, sampleVectorTrack(track, time)[0] ?? 0);
|
|
354
|
+
}
|
|
355
|
+
visual.lastCompositeTime = time;
|
|
356
|
+
}
|
|
357
|
+
const alpha = visual.styleAlpha * opacity;
|
|
358
|
+
if (Math.abs(alpha - visual.lastAlpha) < 1e-6) continue;
|
|
359
|
+
visual.color[3] = alpha;
|
|
360
|
+
visual.component.revision++;
|
|
361
|
+
visual.lastAlpha = alpha;
|
|
362
|
+
}
|
|
363
|
+
for (const extension of node.extensions) {
|
|
364
|
+
if (extension.apply) extension.apply(time, opacity);
|
|
365
|
+
else extension.setOpacity?.(opacity);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
this._lastAppliedTime = time;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
destroy(): void {
|
|
372
|
+
if (this._destroyed) return;
|
|
373
|
+
this._destroyed = true;
|
|
374
|
+
this._abortController.abort();
|
|
375
|
+
for (const handle of this._assetHandles) handle.release();
|
|
376
|
+
this._assetHandles.length = 0;
|
|
377
|
+
if (typeof document !== 'undefined' && document.fonts) {
|
|
378
|
+
for (const face of this._fontFaces) document.fonts.delete(face);
|
|
379
|
+
}
|
|
380
|
+
this._fontFaces.length = 0;
|
|
381
|
+
this._fontLoads.clear();
|
|
382
|
+
for (const node of this._nodes) {
|
|
383
|
+
for (const visual of node.visuals) visual.textRasterizer?.destroy();
|
|
384
|
+
for (const audio of node.audio) audio.destroy();
|
|
385
|
+
node.audio.length = 0;
|
|
386
|
+
for (const extension of node.extensions) extension.destroy?.();
|
|
387
|
+
node.extensions.length = 0;
|
|
388
|
+
}
|
|
389
|
+
this._root.destroy();
|
|
390
|
+
this._nodes.length = 0;
|
|
391
|
+
this._nodesById.clear();
|
|
392
|
+
this._tracksByNode.clear();
|
|
393
|
+
this._trackIndices.clear();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
setPlaying(playing: boolean): void {
|
|
397
|
+
this._playing = playing;
|
|
398
|
+
if (playing) return;
|
|
399
|
+
for (const node of this._nodes) {
|
|
400
|
+
for (const particle of node.particles) particle.playing = false;
|
|
401
|
+
for (const audio of node.audio) audio.pause();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private _loadTexture(resourceId: string, apply: (handle: AssetHandle<GPUTexture>) => void): void {
|
|
406
|
+
const resource = this._animation.resources.find(candidate => candidate.id === resourceId);
|
|
407
|
+
if (!resource || resource.type !== 'image' || !this._assetManager) {
|
|
408
|
+
this._failedResourceCount++;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this._pendingResourceCount++;
|
|
412
|
+
let request: Promise<AssetHandle<GPUTexture>>;
|
|
413
|
+
try {
|
|
414
|
+
request = this._assetManager.loadTexture(resource.uri, {
|
|
415
|
+
label: `Animation2D:${resource.id}`,
|
|
416
|
+
format: resource.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb',
|
|
417
|
+
cacheKey: resource.integrity ?? resource.uri,
|
|
418
|
+
signal: this._abortController.signal,
|
|
419
|
+
});
|
|
420
|
+
} catch {
|
|
421
|
+
this._pendingResourceCount--;
|
|
422
|
+
this._failedResourceCount++;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
void request.then(handle => {
|
|
426
|
+
if (this._destroyed) { handle.release(); return; }
|
|
427
|
+
this._assetHandles.push(handle);
|
|
428
|
+
apply(handle);
|
|
429
|
+
}).catch(() => {
|
|
430
|
+
if (!this._destroyed) this._failedResourceCount++;
|
|
431
|
+
}).finally(() => {
|
|
432
|
+
this._pendingResourceCount = Math.max(0, this._pendingResourceCount - 1);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
protected _materializeCoreVisual(node: Animation2DRuntimeNode, deferred: DeferredCoreVisual): void {
|
|
437
|
+
let visual: ReturnType<typeof createCoreVisual>;
|
|
438
|
+
try {
|
|
439
|
+
visual = createCoreVisual(
|
|
440
|
+
deferred.component,
|
|
441
|
+
deferred.componentIndex,
|
|
442
|
+
this._instanceId,
|
|
443
|
+
node.source,
|
|
444
|
+
deferred.order,
|
|
445
|
+
deferred.sourceGroup,
|
|
446
|
+
deferred.composite,
|
|
447
|
+
deferred.effects,
|
|
448
|
+
);
|
|
449
|
+
} catch {
|
|
450
|
+
this._unsupportedComponentCount++;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
node.content.addChild(new Entity(`${node.entity.name} visual`).addComponent(visual.component));
|
|
454
|
+
node.visuals.push(visual.runtime);
|
|
455
|
+
if (deferred.component.type === 'sprite2d') {
|
|
456
|
+
this._loadTexture(deferred.component.resource, handle => visual.component.setTextureHandle(handle));
|
|
457
|
+
} else if (deferred.component.type === 'text2d' && visual.runtime.textRasterizer) {
|
|
458
|
+
this._loadTextFonts(deferred.component, visual.runtime.textRasterizer);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private _loadTextFonts(component: Readonly<AnimationText2DComponent>, rasterizer: AnimationTextRasterizer): void {
|
|
463
|
+
const descriptors = [
|
|
464
|
+
component.fontResource ? {
|
|
465
|
+
resource: component.fontResource,
|
|
466
|
+
family: component.fontFamily ?? 'sans-serif',
|
|
467
|
+
style: component.fontStyle ?? 'normal',
|
|
468
|
+
weight: component.fontWeight ?? 400,
|
|
469
|
+
} : null,
|
|
470
|
+
...(component.documents ?? []).map(document => document.fontResource ? {
|
|
471
|
+
resource: document.fontResource,
|
|
472
|
+
family: document.fontFamily ?? component.fontFamily ?? 'sans-serif',
|
|
473
|
+
style: document.fontStyle ?? component.fontStyle ?? 'normal',
|
|
474
|
+
weight: document.fontWeight ?? component.fontWeight ?? 400,
|
|
475
|
+
} : null),
|
|
476
|
+
].filter((value): value is NonNullable<typeof value> => value !== null);
|
|
477
|
+
for (const descriptor of descriptors) this._loadFont(descriptor, rasterizer);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private _loadFont(
|
|
481
|
+
descriptor: { resource: string; family: string; style: 'normal' | 'italic'; weight: string | number },
|
|
482
|
+
rasterizer: AnimationTextRasterizer,
|
|
483
|
+
): void {
|
|
484
|
+
const key = `${descriptor.resource}:${descriptor.family}:${descriptor.style}:${descriptor.weight}`;
|
|
485
|
+
const existing = this._fontLoads.get(key);
|
|
486
|
+
if (existing) {
|
|
487
|
+
if (existing.state === 'loaded') rasterizer.invalidateFont();
|
|
488
|
+
else if (existing.state === 'loading') existing.rasterizers.add(rasterizer);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const load = { state: 'loading' as const, rasterizers: new Set([rasterizer]) };
|
|
492
|
+
this._fontLoads.set(key, load);
|
|
493
|
+
const resource = this._animation.resources.find(candidate => candidate.id === descriptor.resource);
|
|
494
|
+
if (!resource || resource.type !== 'binary' || !this._assetManager || typeof FontFace === 'undefined'
|
|
495
|
+
|| typeof document === 'undefined' || !document.fonts) {
|
|
496
|
+
this._fontLoads.set(key, { ...load, state: 'failed' });
|
|
497
|
+
this._failedResourceCount++;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this._pendingResourceCount++;
|
|
501
|
+
let retainedHandle: AssetHandle<ArrayBuffer> | null = null;
|
|
502
|
+
void this._assetManager.load<ArrayBuffer>(
|
|
503
|
+
`Animation2D.font:${resource.integrity ?? resource.uri}`,
|
|
504
|
+
async signal => {
|
|
505
|
+
const response = await fetch(resource.uri, signal ? { signal } : {});
|
|
506
|
+
if (!response.ok) throw new Error(`Font request failed with HTTP ${response.status}.`);
|
|
507
|
+
return response.arrayBuffer();
|
|
508
|
+
},
|
|
509
|
+
() => {},
|
|
510
|
+
{ signal: this._abortController.signal },
|
|
511
|
+
).then(async handle => {
|
|
512
|
+
retainedHandle = handle;
|
|
513
|
+
if (this._destroyed) { handle.release(); return; }
|
|
514
|
+
const face = new FontFace(descriptor.family, handle.value, {
|
|
515
|
+
style: descriptor.style,
|
|
516
|
+
weight: String(descriptor.weight),
|
|
517
|
+
});
|
|
518
|
+
await face.load();
|
|
519
|
+
if (this._destroyed) { handle.release(); return; }
|
|
520
|
+
document.fonts.add(face);
|
|
521
|
+
this._fontFaces.push(face);
|
|
522
|
+
this._assetHandles.push(handle);
|
|
523
|
+
retainedHandle = null;
|
|
524
|
+
const current = this._fontLoads.get(key);
|
|
525
|
+
if (current) {
|
|
526
|
+
current.state = 'loaded';
|
|
527
|
+
for (const target of current.rasterizers) target.invalidateFont();
|
|
528
|
+
current.rasterizers.clear();
|
|
529
|
+
}
|
|
530
|
+
}).catch(() => {
|
|
531
|
+
retainedHandle?.release();
|
|
532
|
+
if (!this._destroyed) {
|
|
533
|
+
const current = this._fontLoads.get(key);
|
|
534
|
+
if (current) {
|
|
535
|
+
current.state = 'failed';
|
|
536
|
+
current.rasterizers.clear();
|
|
537
|
+
}
|
|
538
|
+
this._failedResourceCount++;
|
|
539
|
+
}
|
|
540
|
+
}).finally(() => {
|
|
541
|
+
this._pendingResourceCount = Math.max(0, this._pendingResourceCount - 1);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private _applyTrack(node: Animation2DRuntimeNode, track: ParsedAnimationTrack, time: number): void {
|
|
546
|
+
const trackIndex = this._trackIndices.get(track)!;
|
|
547
|
+
const cursor = findFrame(track.times, time, this._trackCursors[trackIndex] ?? 0);
|
|
548
|
+
this._trackCursors[trackIndex] = cursor;
|
|
549
|
+
const progress = sampleProgress(track, cursor, time);
|
|
550
|
+
const offset = cursor * track.valueSize;
|
|
551
|
+
const nextOffset = Math.min(cursor + 1, track.times.length - 1) * track.valueSize;
|
|
552
|
+
let x = mix(track.values[offset]!, track.values[nextOffset]!, progress);
|
|
553
|
+
let y = track.valueSize === 2 ? mix(track.values[offset + 1]!, track.values[nextOffset + 1]!, progress) : 0;
|
|
554
|
+
if (track.property === 'position' && track.spatialTangents && cursor < track.times.length - 1) {
|
|
555
|
+
const tangentOffset = cursor * 4;
|
|
556
|
+
x = spatialBezier(
|
|
557
|
+
track.values[offset]!,
|
|
558
|
+
track.values[offset]! + track.spatialTangents[tangentOffset]!,
|
|
559
|
+
track.values[nextOffset]! + track.spatialTangents[tangentOffset + 2]!,
|
|
560
|
+
track.values[nextOffset]!,
|
|
561
|
+
progress,
|
|
562
|
+
);
|
|
563
|
+
y = spatialBezier(
|
|
564
|
+
track.values[offset + 1]!,
|
|
565
|
+
track.values[offset + 1]! + track.spatialTangents[tangentOffset + 1]!,
|
|
566
|
+
track.values[nextOffset + 1]! + track.spatialTangents[tangentOffset + 3]!,
|
|
567
|
+
track.values[nextOffset + 1]!,
|
|
568
|
+
progress,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
switch (track.property) {
|
|
572
|
+
case 'position': node.transform.setPosition(x, -y); break;
|
|
573
|
+
case 'rotation': node.transform.rotation = -x; break;
|
|
574
|
+
case 'scale': node.transform.setScale(x, y); break;
|
|
575
|
+
case 'opacity': node.opacity = clamp(x, 0, 1); break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
protected _resolveOpacity(node: Animation2DRuntimeNode): number {
|
|
580
|
+
const cached = this._opacityMemo.get(node.source.id);
|
|
581
|
+
if (cached !== undefined) return cached;
|
|
582
|
+
const parent = node.source.parent ? this._nodesById.get(node.source.parent) : undefined;
|
|
583
|
+
const result = clamp(node.opacity * (parent ? this._resolveOpacity(parent) : 1), 0, 1);
|
|
584
|
+
this._opacityMemo.set(node.source.id, result);
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
protected _isNodeActive(node: Animation2DRuntimeNode): boolean {
|
|
589
|
+
return isRuntimeNodeActive(node, this._nodesById);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** @internal Applies state-machine-only component channels to the shared visual instance. */
|
|
593
|
+
protected _applyStateMachineVisualChannel(
|
|
594
|
+
node: Animation2DRuntimeNode,
|
|
595
|
+
path: string,
|
|
596
|
+
value: unknown,
|
|
597
|
+
): boolean {
|
|
598
|
+
const match = /^components\.(\d+)\.(sprite\.uv-rect|vector\.morph|path\.morph)$/u.exec(path);
|
|
599
|
+
if (!match) return false;
|
|
600
|
+
const componentIndex = Number(match[1]);
|
|
601
|
+
if (node.deferredVisuals.length > 0 && this._isNodeActive(node)) {
|
|
602
|
+
const deferred = node.deferredVisuals.filter(item => item.componentIndex === componentIndex);
|
|
603
|
+
node.deferredVisuals.splice(0, node.deferredVisuals.length, ...node.deferredVisuals.filter(
|
|
604
|
+
item => item.componentIndex !== componentIndex,
|
|
605
|
+
));
|
|
606
|
+
for (const item of deferred) this._materializeCoreVisual(node, item);
|
|
607
|
+
}
|
|
608
|
+
const visuals = node.visuals.filter(visual => visual.sourceComponentIndex === componentIndex);
|
|
609
|
+
if (match[2] === 'sprite.uv-rect') {
|
|
610
|
+
const uv = numericStateMachineValue(value, path, 4);
|
|
611
|
+
for (const visual of visuals) visual.component.setUvRect([uv[0]!, uv[1]!, uv[2]!, uv[3]!]);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
const morph = numericStateMachineValue(value, path, 1);
|
|
615
|
+
for (const visual of visuals) {
|
|
616
|
+
if (match[2] === 'path.morph' && visual.pathMorph) {
|
|
617
|
+
const next = tessellateAnimationPath({
|
|
618
|
+
type: 'path2d',
|
|
619
|
+
commands: visual.pathMorph.commands,
|
|
620
|
+
values: morph,
|
|
621
|
+
fill: visual.pathMorph.fill,
|
|
622
|
+
fillRule: visual.pathMorph.fillRule,
|
|
623
|
+
...(visual.pathMorph.tolerance === undefined ? {} : { tolerance: visual.pathMorph.tolerance }),
|
|
624
|
+
});
|
|
625
|
+
visual.component.geometry.setData(next.positions, next.indices);
|
|
626
|
+
visual.component.revision++;
|
|
627
|
+
} else if (match[2] === 'vector.morph' && visual.vectorShape) {
|
|
628
|
+
const shape = visual.vectorShape;
|
|
629
|
+
let pathValues = morph;
|
|
630
|
+
if (shape.morphRelative) {
|
|
631
|
+
pathValues = new Float32Array(morph.length);
|
|
632
|
+
for (let index = 0; index < morph.length; index++) {
|
|
633
|
+
pathValues[index] = morph[index]! + (shape.values[index] ?? 0);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
updateVectorVisual(visual, 0, pathValues);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function numericStateMachineValue(value: unknown, bindingPath: string, minimumSize: number): Float32Array {
|
|
644
|
+
if (!(value instanceof Float32Array) || value.length < minimumSize) {
|
|
645
|
+
throw new TypeError(
|
|
646
|
+
`Animation2D state-machine binding "${bindingPath}" requires at least ${minimumSize} Float32 values.`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return value;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function createEllipseGeometry(width: number, height: number, centerX: number, centerY: number): Geometry2D {
|
|
653
|
+
const segments = 48;
|
|
654
|
+
const positions = new Float32Array((segments + 1) * 2);
|
|
655
|
+
positions[0] = centerX;
|
|
656
|
+
positions[1] = centerY;
|
|
657
|
+
for (let index = 0; index < segments; index++) {
|
|
658
|
+
const angle = index / segments * Math.PI * 2;
|
|
659
|
+
positions[(index + 1) * 2] = centerX + Math.cos(angle) * width / 2;
|
|
660
|
+
positions[(index + 1) * 2 + 1] = centerY + Math.sin(angle) * height / 2;
|
|
661
|
+
}
|
|
662
|
+
const indices = new Uint16Array(segments * 3);
|
|
663
|
+
for (let index = 0; index < segments; index++) {
|
|
664
|
+
indices[index * 3] = 0;
|
|
665
|
+
indices[index * 3 + 1] = index + 1;
|
|
666
|
+
indices[index * 3 + 2] = index + 2 > segments ? 1 : index + 2;
|
|
667
|
+
}
|
|
668
|
+
return new Geometry2D(positions, indices);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function isShape2DComponent(component: AnimationComponent): component is AnimationShape2DComponent {
|
|
672
|
+
return component.type === 'shape2d'
|
|
673
|
+
&& (component as Partial<AnimationShape2DComponent>).size !== undefined
|
|
674
|
+
&& (component as Partial<AnimationShape2DComponent>).fill !== undefined;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function isPath2DComponent(component: AnimationComponent): component is AnimationPath2DComponent {
|
|
678
|
+
return component.type === 'path2d'
|
|
679
|
+
&& typeof (component as Partial<AnimationPath2DComponent>).commands === 'string'
|
|
680
|
+
&& (component as Partial<AnimationPath2DComponent>).values !== undefined;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isSprite2DComponent(component: AnimationComponent): component is AnimationSprite2DComponent {
|
|
684
|
+
return component.type === 'sprite2d'
|
|
685
|
+
&& typeof (component as Partial<AnimationSprite2DComponent>).resource === 'string'
|
|
686
|
+
&& (component as Partial<AnimationSprite2DComponent>).size !== undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function isText2DComponent(component: AnimationComponent): component is AnimationText2DComponent {
|
|
690
|
+
return component.type === 'text2d'
|
|
691
|
+
&& typeof (component as Partial<AnimationText2DComponent>).text === 'string'
|
|
692
|
+
&& (component as Partial<AnimationText2DComponent>).size !== undefined;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function isParticle2DComponent(component: AnimationComponent): component is AnimationParticle2DComponent {
|
|
696
|
+
return component.type === 'particle2d'
|
|
697
|
+
&& typeof (component as Partial<AnimationParticle2DComponent>).maxParticles === 'number';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function isAudioComponent(component: AnimationComponent): component is AnimationAudioComponent {
|
|
701
|
+
return component.type === 'audio' && typeof (component as Partial<AnimationAudioComponent>).resource === 'string';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function isVectorStrokePathComponent(component: AnimationComponent): component is VectorStrokePathComponent {
|
|
705
|
+
const candidate = component as Partial<VectorStrokePathComponent>;
|
|
706
|
+
const inlinePath = typeof candidate.commands === 'string'
|
|
707
|
+
&& /^[MLQCZ]+$/.test(candidate.commands)
|
|
708
|
+
&& candidate.commands[0] === 'M'
|
|
709
|
+
&& Array.isArray(candidate.values)
|
|
710
|
+
&& candidate.values.length === strokePathValueCount(candidate.commands)
|
|
711
|
+
&& candidate.values.every(value => typeof value === 'number' && Number.isFinite(value));
|
|
712
|
+
const sourcePath = typeof candidate.sourceComponent === 'number'
|
|
713
|
+
&& Number.isSafeInteger(candidate.sourceComponent)
|
|
714
|
+
&& candidate.sourceComponent >= 0;
|
|
715
|
+
return component.type === 'org.haiyue.vector-stroke@1'
|
|
716
|
+
&& (inlinePath || sourcePath)
|
|
717
|
+
&& Array.isArray(candidate.color)
|
|
718
|
+
&& candidate.color.length === 4
|
|
719
|
+
&& candidate.color.every(value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
|
|
720
|
+
&& typeof candidate.width === 'number' && Number.isFinite(candidate.width) && candidate.width > 0
|
|
721
|
+
&& (candidate.lineCap === 'butt' || candidate.lineCap === 'round' || candidate.lineCap === 'square')
|
|
722
|
+
&& (candidate.lineJoin === 'miter' || candidate.lineJoin === 'round' || candidate.lineJoin === 'bevel')
|
|
723
|
+
&& typeof candidate.miterLimit === 'number' && Number.isFinite(candidate.miterLimit) && candidate.miterLimit >= 1
|
|
724
|
+
&& (candidate.tolerance === undefined
|
|
725
|
+
|| (typeof candidate.tolerance === 'number' && Number.isFinite(candidate.tolerance) && candidate.tolerance > 0));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function isVectorPathMorphComponent(component: AnimationComponent): component is VectorPathMorphComponent {
|
|
729
|
+
const candidate = component as Partial<VectorPathMorphComponent>;
|
|
730
|
+
return component.type === 'org.haiyue.vector-path-morph@1'
|
|
731
|
+
&& typeof candidate.commands === 'string'
|
|
732
|
+
&& /^[MLQCZ]+$/.test(candidate.commands)
|
|
733
|
+
&& (Array.isArray(candidate.times) || candidate.times instanceof Float32Array) && candidate.times.length > 0
|
|
734
|
+
&& Array.from(candidate.times).every(value => typeof value === 'number' && Number.isFinite(value))
|
|
735
|
+
&& (Array.isArray(candidate.values) || candidate.values instanceof Float32Array)
|
|
736
|
+
&& typeof candidate.valueSize === 'number' && Number.isSafeInteger(candidate.valueSize) && candidate.valueSize > 0
|
|
737
|
+
&& candidate.values.length === candidate.times.length * candidate.valueSize
|
|
738
|
+
&& Array.from(candidate.values).every(value => typeof value === 'number' && Number.isFinite(value))
|
|
739
|
+
&& (candidate.interpolation === 'step' || candidate.interpolation === 'linear' || candidate.interpolation === 'cubic-bezier')
|
|
740
|
+
&& Array.isArray(candidate.fill) && candidate.fill.length === 4;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function strokePathValueCount(commands: string): number {
|
|
744
|
+
let count = 0;
|
|
745
|
+
for (const command of commands) count += command === 'M' || command === 'L' ? 2 : command === 'Q' ? 4 : command === 'C' ? 6 : 0;
|
|
746
|
+
return count;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function isCoreVisualComponent(component: AnimationComponent): component is CoreVisualComponent {
|
|
750
|
+
return isShape2DComponent(component) || isPath2DComponent(component) || isSprite2DComponent(component)
|
|
751
|
+
|| isText2DComponent(component) || isVectorShapeComponent(component)
|
|
752
|
+
|| isVectorStrokePathComponent(component) || isVectorPathMorphComponent(component);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function isVectorShapeComponent(component: AnimationComponent): component is AnimationVectorShapeComponent {
|
|
756
|
+
return component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID
|
|
757
|
+
&& typeof (component as Partial<AnimationVectorShapeComponent>).commands === 'string';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function createCoreVisual(
|
|
761
|
+
component: CoreVisualComponent,
|
|
762
|
+
componentIndex: number,
|
|
763
|
+
instanceId: number,
|
|
764
|
+
node: Readonly<AnimationNode>,
|
|
765
|
+
order: number,
|
|
766
|
+
sourceGroup: string | undefined,
|
|
767
|
+
composite: Readonly<AnimationComposite> | undefined,
|
|
768
|
+
effects: readonly Readonly<AnimationLayerEffect>[] | undefined,
|
|
769
|
+
): { component: AnimationVisual2D; runtime: Animation2DRuntimeVisual } {
|
|
770
|
+
let geometry: Geometry2D;
|
|
771
|
+
let color: [number, number, number, number];
|
|
772
|
+
let textMaterial: AnimationTextRasterizer | null = null;
|
|
773
|
+
let gradient: import('./AnimationVisual2D').AnimationVisualGradient | null = null;
|
|
774
|
+
let vectorPaint: 'fill' | 'stroke' | undefined;
|
|
775
|
+
if (component.type === 'path2d') {
|
|
776
|
+
geometry = tessellateAnimationPath(component);
|
|
777
|
+
color = [...component.fill];
|
|
778
|
+
} else if (component.type === 'org.haiyue.vector-path-morph@1') {
|
|
779
|
+
geometry = tessellateAnimationPath(sampleVectorPathMorph(component, component.times[0] ?? 0));
|
|
780
|
+
color = [...component.fill];
|
|
781
|
+
} else if (component.type === 'org.haiyue.vector-stroke@1') {
|
|
782
|
+
geometry = tessellateVectorStrokePath(resolveVectorStrokePath(component, node));
|
|
783
|
+
color = [...component.color];
|
|
784
|
+
} else if (component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID) {
|
|
785
|
+
const initialTime = component.morph?.times[0] ?? firstVectorModifierTime(component) ?? 0;
|
|
786
|
+
const pathValues = sampleVectorShapePath(component, initialTime);
|
|
787
|
+
const resolvedPath = resolveVectorPath(component, pathValues, initialTime, component.fill !== undefined);
|
|
788
|
+
if (component.fill) {
|
|
789
|
+
geometry = resolvedPath.commands.length === 0 ? emptyGeometry2D() : tessellateRuntimeFillPath({
|
|
790
|
+
type: 'path2d', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
791
|
+
fill: [1, 1, 1, 1], fillRule: component.fillRule ?? 'nonzero',
|
|
792
|
+
...(component.tolerance === undefined ? {} : { tolerance: component.tolerance }),
|
|
793
|
+
});
|
|
794
|
+
vectorPaint = 'fill';
|
|
795
|
+
if (component.fill.kind === 'solid') color = [...component.fill.color];
|
|
796
|
+
else {
|
|
797
|
+
color = [1, 1, 1, 1];
|
|
798
|
+
gradient = createRuntimeGradient(component.fill);
|
|
799
|
+
}
|
|
800
|
+
} else {
|
|
801
|
+
const stroke = component.stroke!;
|
|
802
|
+
geometry = resolvedPath.commands.length === 0 ? emptyGeometry2D() : tessellateVectorStrokePath({
|
|
803
|
+
type: 'org.haiyue.vector-stroke@1', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
804
|
+
color: stroke.color, width: stroke.width, lineCap: stroke.lineCap, lineJoin: stroke.lineJoin,
|
|
805
|
+
miterLimit: stroke.miterLimit,
|
|
806
|
+
...(component.tolerance === undefined ? {} : { tolerance: component.tolerance }),
|
|
807
|
+
...(stroke.dash ? { dash: stroke.dash } : {}),
|
|
808
|
+
...(stroke.dashOffset === undefined ? {} : { dashOffset: stroke.dashOffset }),
|
|
809
|
+
}, true);
|
|
810
|
+
vectorPaint = 'stroke';
|
|
811
|
+
color = stroke.gradient ? [1, 1, 1, 1] : [...stroke.color];
|
|
812
|
+
if (stroke.gradient) gradient = createRuntimeGradient(stroke.gradient);
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
const position = component.position ?? [0, 0];
|
|
816
|
+
const centerX = position[0];
|
|
817
|
+
const centerY = -position[1];
|
|
818
|
+
geometry = component.type === 'shape2d' && component.shape === 'ellipse'
|
|
819
|
+
? createEllipseGeometry(component.size[0], component.size[1], centerX, centerY)
|
|
820
|
+
: createRect2D({ width: component.size[0], height: component.size[1], x: centerX, y: centerY });
|
|
821
|
+
color = component.type === 'shape2d' ? [...component.fill]
|
|
822
|
+
: component.type === 'sprite2d' ? [...(component.tint ?? [1, 1, 1, 1])] : [1, 1, 1, 1];
|
|
823
|
+
if (component.type === 'text2d') {
|
|
824
|
+
textMaterial = new AnimationTextRasterizer(component);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const visual = new AnimationVisual2D({
|
|
828
|
+
geometry,
|
|
829
|
+
color,
|
|
830
|
+
instanceId,
|
|
831
|
+
nodeId: sourceGroup ?? node.id,
|
|
832
|
+
order,
|
|
833
|
+
sourceOnly: sourceGroup !== undefined,
|
|
834
|
+
...(composite ? { composite } : {}),
|
|
835
|
+
...(component.type === 'sprite2d' ? { requiresTexture: true, uvRect: component.uvRect ?? [0, 0, 1, 1] } : {}),
|
|
836
|
+
...(component.type === 'text2d' ? { requiresTexture: true, textMaterial } : {}),
|
|
837
|
+
...(gradient ? { gradient } : {}),
|
|
838
|
+
...(effects?.length ? { effects: createVisualEffects(effects, 0) } : {}),
|
|
839
|
+
});
|
|
840
|
+
const initialStyleAlpha = component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID
|
|
841
|
+
? initialVectorStyleAlpha(component)
|
|
842
|
+
: visual.color[3];
|
|
843
|
+
const compositeExpansionTracks = composite
|
|
844
|
+
? compositeLayers(composite).map(layer => layer.expansionTrack)
|
|
845
|
+
: [];
|
|
846
|
+
return {
|
|
847
|
+
component: visual,
|
|
848
|
+
runtime: {
|
|
849
|
+
component: visual,
|
|
850
|
+
sourceComponentIndex: componentIndex,
|
|
851
|
+
color: visual.color,
|
|
852
|
+
baseAlpha: visual.color[3],
|
|
853
|
+
styleAlpha: initialStyleAlpha,
|
|
854
|
+
lastAlpha: Number.NaN,
|
|
855
|
+
...(component.type === 'org.haiyue.vector-path-morph@1' ? { pathMorph: component, lastMorphTime: component.times[0] ?? 0 } : {}),
|
|
856
|
+
...(component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID ? {
|
|
857
|
+
vectorShape: component,
|
|
858
|
+
vectorPaint: vectorPaint!,
|
|
859
|
+
...(isVectorShapeDynamic(component) ? { lastVectorTime: Number.NaN } : {}),
|
|
860
|
+
} : {}),
|
|
861
|
+
...(textMaterial ? { textRasterizer: textMaterial } : {}),
|
|
862
|
+
...(effects?.length ? { sourceEffects: effects, lastEffectTime: 0 } : {}),
|
|
863
|
+
...(compositeExpansionTracks.some(track => track !== undefined) ? {
|
|
864
|
+
compositeExpansionTracks,
|
|
865
|
+
lastCompositeTime: Number.NaN,
|
|
866
|
+
} : {}),
|
|
867
|
+
...(component.type === 'sprite2d' && component.uvRectTrack ? {
|
|
868
|
+
spriteUvRectTrack: component.uvRectTrack,
|
|
869
|
+
spriteUvRectFrame: -1,
|
|
870
|
+
} : {}),
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function createVisualEffects(
|
|
876
|
+
effects: readonly Readonly<AnimationLayerEffect>[],
|
|
877
|
+
time: number,
|
|
878
|
+
): import('./AnimationVisual2D').AnimationVisualEffect[] {
|
|
879
|
+
return effects.map(effect => ({ kind: effect.kind, values: sampleEffectValues(effect, time) }));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function updateVisualEffects(
|
|
883
|
+
visual: AnimationVisual2D,
|
|
884
|
+
effects: readonly Readonly<AnimationLayerEffect>[],
|
|
885
|
+
time: number,
|
|
886
|
+
): void {
|
|
887
|
+
const count = Math.min(visual.effects.length, effects.length);
|
|
888
|
+
for (let index = 0; index < count; index++) {
|
|
889
|
+
const sampled = sampleEffectValues(effects[index]!, time);
|
|
890
|
+
const target = visual.effects[index]!.values;
|
|
891
|
+
if (target.length === sampled.length) target.set(sampled);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function sampleEffectValues(effect: Readonly<AnimationLayerEffect>, time: number): Float32Array {
|
|
896
|
+
switch (effect.kind) {
|
|
897
|
+
case 'tint': {
|
|
898
|
+
const black = effect.blackTrack ? sampleVectorTrack(effect.blackTrack, time) : effect.black;
|
|
899
|
+
const white = effect.whiteTrack ? sampleVectorTrack(effect.whiteTrack, time) : effect.white;
|
|
900
|
+
const amount = effect.amountTrack ? sampleVectorTrack(effect.amountTrack, time)[0] ?? effect.amount : effect.amount;
|
|
901
|
+
return new Float32Array([
|
|
902
|
+
black[0] ?? effect.black[0], black[1] ?? effect.black[1], black[2] ?? effect.black[2],
|
|
903
|
+
white[0] ?? effect.white[0], white[1] ?? effect.white[1], white[2] ?? effect.white[2],
|
|
904
|
+
clamp(amount, 0, 1),
|
|
905
|
+
]);
|
|
906
|
+
}
|
|
907
|
+
case 'fill': {
|
|
908
|
+
const color = effect.colorTrack ? sampleVectorTrack(effect.colorTrack, time) : effect.color;
|
|
909
|
+
const opacity = effect.opacityTrack ? sampleVectorTrack(effect.opacityTrack, time)[0] ?? effect.opacity ?? 1 : effect.opacity ?? 1;
|
|
910
|
+
return new Float32Array([
|
|
911
|
+
color[0] ?? effect.color[0], color[1] ?? effect.color[1], color[2] ?? effect.color[2], color[3] ?? effect.color[3],
|
|
912
|
+
clamp(opacity, 0, 1),
|
|
913
|
+
]);
|
|
914
|
+
}
|
|
915
|
+
case 'opacity': {
|
|
916
|
+
const opacity = effect.opacityTrack ? sampleVectorTrack(effect.opacityTrack, time)[0] ?? effect.opacity : effect.opacity;
|
|
917
|
+
return new Float32Array([clamp(opacity, 0, 1)]);
|
|
918
|
+
}
|
|
919
|
+
case 'color-matrix':
|
|
920
|
+
return new Float32Array(effect.matrixTrack ? sampleVectorTrack(effect.matrixTrack, time) : effect.matrix);
|
|
921
|
+
case 'blur': {
|
|
922
|
+
const radius = effect.radiusTrack ? sampleVectorTrack(effect.radiusTrack, time) : effect.radius;
|
|
923
|
+
return new Float32Array([Math.max(0, radius[0] ?? effect.radius[0]), Math.max(0, radius[1] ?? effect.radius[1])]);
|
|
924
|
+
}
|
|
925
|
+
case 'drop-shadow': {
|
|
926
|
+
const color = effect.colorTrack ? sampleVectorTrack(effect.colorTrack, time) : effect.color;
|
|
927
|
+
const opacity = effect.opacityTrack ? sampleVectorTrack(effect.opacityTrack, time)[0] ?? effect.opacity : effect.opacity;
|
|
928
|
+
const offset = effect.offsetTrack ? sampleVectorTrack(effect.offsetTrack, time) : effect.offset;
|
|
929
|
+
const blur = effect.blurTrack ? sampleVectorTrack(effect.blurTrack, time)[0] ?? effect.blur : effect.blur;
|
|
930
|
+
return new Float32Array([
|
|
931
|
+
color[0] ?? effect.color[0], color[1] ?? effect.color[1], color[2] ?? effect.color[2], color[3] ?? effect.color[3],
|
|
932
|
+
clamp(opacity, 0, 1), offset[0] ?? effect.offset[0], offset[1] ?? effect.offset[1], Math.max(0, blur),
|
|
933
|
+
]);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function updateVectorVisual(
|
|
939
|
+
visual: Animation2DRuntimeVisual,
|
|
940
|
+
time: number,
|
|
941
|
+
pathOverride?: Readonly<Float32Array>,
|
|
942
|
+
): void {
|
|
943
|
+
const shape = visual.vectorShape!;
|
|
944
|
+
const paint = visual.vectorPaint!;
|
|
945
|
+
const pathValues = pathOverride ?? sampleVectorShapePath(shape, time);
|
|
946
|
+
let geometryChanged = shape.morph !== undefined || hasAnimatedVectorModifier(shape);
|
|
947
|
+
if (paint === 'fill') {
|
|
948
|
+
const fill = shape.fill!;
|
|
949
|
+
if (geometryChanged) {
|
|
950
|
+
const resolvedPath = resolveVectorPath(shape, pathValues, time, true);
|
|
951
|
+
const next = !resolvedPath.commands.length ? emptyGeometry2D() : tessellateRuntimeFillPath({
|
|
952
|
+
type: 'path2d', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
953
|
+
fill: [1, 1, 1, 1], fillRule: shape.fillRule ?? 'nonzero',
|
|
954
|
+
...(shape.tolerance === undefined ? {} : { tolerance: shape.tolerance }),
|
|
955
|
+
});
|
|
956
|
+
visual.component.geometry.setData(next.positions, next.indices);
|
|
957
|
+
}
|
|
958
|
+
if (fill.kind === 'solid') {
|
|
959
|
+
const sampledColor = fill.colorTrack ? sampleVectorTrack(fill.colorTrack, time) : fill.color;
|
|
960
|
+
visual.color[0] = sampledColor[0] ?? fill.color[0];
|
|
961
|
+
visual.color[1] = sampledColor[1] ?? fill.color[1];
|
|
962
|
+
visual.color[2] = sampledColor[2] ?? fill.color[2];
|
|
963
|
+
const sampledOpacity = fill.opacityTrack
|
|
964
|
+
? sampleVectorTrack(fill.opacityTrack, time)[0] ?? fill.opacity ?? 1
|
|
965
|
+
: fill.opacity ?? 1;
|
|
966
|
+
visual.styleAlpha = clamp((sampledColor[3] ?? fill.color[3]) * sampledOpacity, 0, 1);
|
|
967
|
+
} else {
|
|
968
|
+
updateRuntimeGradient(visual, fill, time);
|
|
969
|
+
visual.styleAlpha = 1;
|
|
970
|
+
}
|
|
971
|
+
} else {
|
|
972
|
+
const stroke = shape.stroke!;
|
|
973
|
+
const width = stroke.widthTrack ? sampleVectorTrack(stroke.widthTrack, time)[0] ?? stroke.width : stroke.width;
|
|
974
|
+
const dashOffset = stroke.dashOffsetTrack
|
|
975
|
+
? sampleVectorTrack(stroke.dashOffsetTrack, time)[0] ?? stroke.dashOffset ?? 0
|
|
976
|
+
: stroke.dashOffset;
|
|
977
|
+
geometryChanged ||= stroke.widthTrack !== undefined || stroke.dashOffsetTrack !== undefined;
|
|
978
|
+
if (geometryChanged) {
|
|
979
|
+
const resolvedPath = resolveVectorPath(shape, pathValues, time, false);
|
|
980
|
+
const next = !resolvedPath.commands.length ? emptyGeometry2D() : tessellateVectorStrokePath({
|
|
981
|
+
type: 'org.haiyue.vector-stroke@1', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
982
|
+
color: stroke.color, width, lineCap: stroke.lineCap, lineJoin: stroke.lineJoin,
|
|
983
|
+
miterLimit: stroke.miterLimit,
|
|
984
|
+
...(shape.tolerance === undefined ? {} : { tolerance: shape.tolerance }),
|
|
985
|
+
...(stroke.dash ? { dash: stroke.dash } : {}),
|
|
986
|
+
...(dashOffset === undefined ? {} : { dashOffset }),
|
|
987
|
+
}, true);
|
|
988
|
+
visual.component.geometry.setData(next.positions, next.indices);
|
|
989
|
+
}
|
|
990
|
+
const sampledColor = stroke.colorTrack ? sampleVectorTrack(stroke.colorTrack, time) : stroke.color;
|
|
991
|
+
visual.color[0] = stroke.gradient ? 1 : sampledColor[0] ?? stroke.color[0];
|
|
992
|
+
visual.color[1] = stroke.gradient ? 1 : sampledColor[1] ?? stroke.color[1];
|
|
993
|
+
visual.color[2] = stroke.gradient ? 1 : sampledColor[2] ?? stroke.color[2];
|
|
994
|
+
const sampledOpacity = stroke.opacityTrack
|
|
995
|
+
? sampleVectorTrack(stroke.opacityTrack, time)[0] ?? stroke.opacity ?? 1
|
|
996
|
+
: stroke.opacity ?? 1;
|
|
997
|
+
visual.styleAlpha = stroke.gradient ? 1 : clamp((sampledColor[3] ?? stroke.color[3]) * sampledOpacity, 0, 1);
|
|
998
|
+
if (stroke.gradient) updateRuntimeGradient(visual, stroke.gradient, time);
|
|
999
|
+
}
|
|
1000
|
+
visual.component.revision++;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function sampleVectorShapePath(
|
|
1004
|
+
shape: Readonly<AnimationVectorShapeComponent>,
|
|
1005
|
+
time: number,
|
|
1006
|
+
): Float32Array {
|
|
1007
|
+
if (!shape.morph) return shape.values instanceof Float32Array
|
|
1008
|
+
? shape.values
|
|
1009
|
+
: new Float32Array(shape.values);
|
|
1010
|
+
const sampled = sampleVectorTrack(shape.morph, time);
|
|
1011
|
+
if (!shape.morphRelative) return sampled;
|
|
1012
|
+
for (let index = 0; index < sampled.length; index++) {
|
|
1013
|
+
sampled[index] = (sampled[index] ?? 0) + (shape.values[index] ?? 0);
|
|
1014
|
+
}
|
|
1015
|
+
return sampled;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function createRuntimeGradient(paint: AnimationVectorGradientPaint): import('./AnimationVisual2D').AnimationVisualGradient {
|
|
1019
|
+
return {
|
|
1020
|
+
kind: paint.kind === 'linear-gradient' ? 'linear' : 'radial',
|
|
1021
|
+
start: [paint.start[0], -paint.start[1]],
|
|
1022
|
+
end: [paint.end[0], -paint.end[1]],
|
|
1023
|
+
stops: new Float32Array(paint.stops),
|
|
1024
|
+
opacity: paint.opacity ?? 1,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function updateRuntimeGradient(visual: Animation2DRuntimeVisual, paint: AnimationVectorGradientPaint, time: number): void {
|
|
1029
|
+
const gradient = visual.component.gradient ??= createRuntimeGradient(paint);
|
|
1030
|
+
const start = paint.startTrack ? sampleVectorTrack(paint.startTrack, time) : paint.start;
|
|
1031
|
+
const end = paint.endTrack ? sampleVectorTrack(paint.endTrack, time) : paint.end;
|
|
1032
|
+
const stops = paint.stopsTrack ? sampleVectorTrack(paint.stopsTrack, time) : paint.stops;
|
|
1033
|
+
gradient.start[0] = start[0] ?? paint.start[0];
|
|
1034
|
+
gradient.start[1] = -(start[1] ?? paint.start[1]);
|
|
1035
|
+
gradient.end[0] = end[0] ?? paint.end[0];
|
|
1036
|
+
gradient.end[1] = -(end[1] ?? paint.end[1]);
|
|
1037
|
+
if (gradient.stops.length !== stops.length) gradient.stops = new Float32Array(stops);
|
|
1038
|
+
else gradient.stops.set(stops);
|
|
1039
|
+
gradient.opacity = paint.opacityTrack
|
|
1040
|
+
? sampleVectorTrack(paint.opacityTrack, time)[0] ?? paint.opacity ?? 1
|
|
1041
|
+
: paint.opacity ?? 1;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function initialVectorStyleAlpha(component: AnimationVectorShapeComponent): number {
|
|
1045
|
+
if (component.fill) return component.fill.kind === 'solid'
|
|
1046
|
+
? component.fill.color[3] * (component.fill.opacity ?? 1)
|
|
1047
|
+
: 1;
|
|
1048
|
+
const stroke = component.stroke!;
|
|
1049
|
+
return stroke.gradient ? 1 : stroke.color[3] * (stroke.opacity ?? 1);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function isVectorShapeDynamic(component: AnimationVectorShapeComponent): boolean {
|
|
1053
|
+
if (component.morph || hasAnimatedVectorModifier(component)) return true;
|
|
1054
|
+
if (component.fill) return component.fill.kind === 'solid'
|
|
1055
|
+
? component.fill.colorTrack !== undefined || component.fill.opacityTrack !== undefined
|
|
1056
|
+
: component.fill.startTrack !== undefined || component.fill.endTrack !== undefined
|
|
1057
|
+
|| component.fill.stopsTrack !== undefined || component.fill.opacityTrack !== undefined;
|
|
1058
|
+
const stroke = component.stroke!;
|
|
1059
|
+
return stroke.colorTrack !== undefined || stroke.opacityTrack !== undefined || stroke.widthTrack !== undefined
|
|
1060
|
+
|| stroke.dashOffsetTrack !== undefined || (stroke.gradient !== undefined && (
|
|
1061
|
+
stroke.gradient.startTrack !== undefined || stroke.gradient.endTrack !== undefined
|
|
1062
|
+
|| stroke.gradient.stopsTrack !== undefined || stroke.gradient.opacityTrack !== undefined
|
|
1063
|
+
));
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function resolveVectorPath(
|
|
1067
|
+
component: AnimationVectorShapeComponent,
|
|
1068
|
+
values: readonly number[] | Float32Array,
|
|
1069
|
+
time: number,
|
|
1070
|
+
closeOpen: boolean,
|
|
1071
|
+
) {
|
|
1072
|
+
if (!component.modifiers?.length) return { commands: component.commands, values: new Float32Array(values) };
|
|
1073
|
+
return applyVectorPathModifiers(
|
|
1074
|
+
component.commands,
|
|
1075
|
+
values,
|
|
1076
|
+
component.tolerance ?? 0.35,
|
|
1077
|
+
component.modifiers.map(modifier => modifier.kind === 'round-corners'
|
|
1078
|
+
? {
|
|
1079
|
+
kind: modifier.kind,
|
|
1080
|
+
radius: modifier.radiusTrack ? sampleVectorTrack(modifier.radiusTrack, time)[0] ?? modifier.radius : modifier.radius,
|
|
1081
|
+
}
|
|
1082
|
+
: {
|
|
1083
|
+
kind: modifier.kind,
|
|
1084
|
+
start: modifier.startTrack ? sampleVectorTrack(modifier.startTrack, time)[0] ?? modifier.start : modifier.start,
|
|
1085
|
+
end: modifier.endTrack ? sampleVectorTrack(modifier.endTrack, time)[0] ?? modifier.end : modifier.end,
|
|
1086
|
+
offset: modifier.offsetTrack ? sampleVectorTrack(modifier.offsetTrack, time)[0] ?? modifier.offset : modifier.offset,
|
|
1087
|
+
mode: modifier.mode,
|
|
1088
|
+
}),
|
|
1089
|
+
closeOpen,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function hasAnimatedVectorModifier(component: AnimationVectorShapeComponent): boolean {
|
|
1094
|
+
return component.modifiers?.some(modifier => modifier.kind === 'round-corners'
|
|
1095
|
+
? modifier.radiusTrack !== undefined
|
|
1096
|
+
: modifier.startTrack !== undefined || modifier.endTrack !== undefined || modifier.offsetTrack !== undefined) ?? false;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function firstVectorModifierTime(component: AnimationVectorShapeComponent): number | undefined {
|
|
1100
|
+
for (const modifier of component.modifiers ?? []) {
|
|
1101
|
+
const track = modifier.kind === 'round-corners'
|
|
1102
|
+
? modifier.radiusTrack
|
|
1103
|
+
: modifier.startTrack ?? modifier.endTrack ?? modifier.offsetTrack;
|
|
1104
|
+
if (track) return track.times[0];
|
|
1105
|
+
}
|
|
1106
|
+
return undefined;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function emptyGeometry2D(): Geometry2D {
|
|
1110
|
+
// Geometry2D deliberately rejects empty buffers; a zero-area triangle keeps
|
|
1111
|
+
// the renderer contract valid while producing no fragments for an empty trim.
|
|
1112
|
+
return new Geometry2D(new Float32Array(6), new Uint16Array([0, 1, 2]));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function tessellateRuntimeFillPath(component: AnimationPath2DComponent): Geometry2D {
|
|
1116
|
+
try {
|
|
1117
|
+
return tessellateAnimationPath(component);
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
if (error instanceof RangeError && (
|
|
1120
|
+
error.message === 'Animation path contains no drawable closed contour.'
|
|
1121
|
+
|| error.message === 'Animation path tessellation produced no triangles.'
|
|
1122
|
+
)) return emptyGeometry2D();
|
|
1123
|
+
throw error;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function sampleVectorTrack(track: AnimationVectorValueTrack, time: number): Float32Array {
|
|
1128
|
+
const frameCount = track.times.length;
|
|
1129
|
+
if (frameCount === 0 || track.valueSize <= 0) return new Float32Array();
|
|
1130
|
+
const frame = findVectorTrackFrame(track, time);
|
|
1131
|
+
const next = Math.min(frame + 1, frameCount - 1);
|
|
1132
|
+
let progress = 0;
|
|
1133
|
+
if (next !== frame && track.interpolation !== 'step') {
|
|
1134
|
+
const start = track.times[frame]!;
|
|
1135
|
+
const end = track.times[next]!;
|
|
1136
|
+
progress = clamp((time - start) / Math.max(end - start, 1e-8), 0, 1);
|
|
1137
|
+
if (track.interpolation === 'cubic-bezier' && track.easings) {
|
|
1138
|
+
const offset = frame * 4;
|
|
1139
|
+
progress = cubicBezierYForX(
|
|
1140
|
+
progress,
|
|
1141
|
+
track.easings[offset] ?? 0.333,
|
|
1142
|
+
track.easings[offset + 1] ?? 0.333,
|
|
1143
|
+
track.easings[offset + 2] ?? 0.667,
|
|
1144
|
+
track.easings[offset + 3] ?? 0.667,
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const result = new Float32Array(track.valueSize);
|
|
1149
|
+
const fromOffset = frame * track.valueSize;
|
|
1150
|
+
const toOffset = next * track.valueSize;
|
|
1151
|
+
for (let index = 0; index < track.valueSize; index++) {
|
|
1152
|
+
const from = track.values[fromOffset + index] ?? 0;
|
|
1153
|
+
result[index] = mix(from, track.values[toOffset + index] ?? from, progress);
|
|
1154
|
+
}
|
|
1155
|
+
return result;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function findVectorTrackFrame(track: AnimationVectorValueTrack, time: number): number {
|
|
1159
|
+
let low = 0;
|
|
1160
|
+
let high = track.times.length - 1;
|
|
1161
|
+
while (low < high) {
|
|
1162
|
+
const middle = Math.ceil((low + high) / 2);
|
|
1163
|
+
if (track.times[middle]! <= time) low = middle;
|
|
1164
|
+
else high = middle - 1;
|
|
1165
|
+
}
|
|
1166
|
+
return low;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function isSourceNodeActiveAt(
|
|
1170
|
+
node: Readonly<AnimationNode>,
|
|
1171
|
+
time: number,
|
|
1172
|
+
nodesById: ReadonlyMap<string, Readonly<AnimationNode>>,
|
|
1173
|
+
animationDuration: number,
|
|
1174
|
+
): boolean {
|
|
1175
|
+
let cursor: Readonly<AnimationNode> | undefined = node;
|
|
1176
|
+
while (cursor) {
|
|
1177
|
+
const start = cursor.start ?? 0;
|
|
1178
|
+
const end = start + (cursor.duration ?? animationDuration);
|
|
1179
|
+
if (time < start || time > end) return false;
|
|
1180
|
+
cursor = cursor.parent ? nodesById.get(cursor.parent) : undefined;
|
|
1181
|
+
}
|
|
1182
|
+
return true;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function isRuntimeNodeActive(
|
|
1186
|
+
node: Animation2DRuntimeNode,
|
|
1187
|
+
nodesById: ReadonlyMap<string, Animation2DRuntimeNode>,
|
|
1188
|
+
): boolean {
|
|
1189
|
+
let cursor: Animation2DRuntimeNode | undefined = node;
|
|
1190
|
+
while (cursor) {
|
|
1191
|
+
if (cursor.entity.disabled) return false;
|
|
1192
|
+
cursor = cursor.source.parent ? nodesById.get(cursor.source.parent) : undefined;
|
|
1193
|
+
}
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function resolveVectorStrokePath(
|
|
1198
|
+
component: VectorStrokePathComponent,
|
|
1199
|
+
node: Readonly<AnimationNode>,
|
|
1200
|
+
): VectorStrokePathComponent {
|
|
1201
|
+
if (typeof component.commands === 'string' && component.values !== undefined) return component;
|
|
1202
|
+
const sourceIndex = component.sourceComponent;
|
|
1203
|
+
const source = typeof sourceIndex === 'number' ? node.components?.[sourceIndex] : undefined;
|
|
1204
|
+
if (!source || !isPath2DComponent(source)) throw new TypeError('Lottie stroke extension references a missing path2d source component.');
|
|
1205
|
+
return { ...component, commands: source.commands, values: source.values };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function resolveSourceGroups(nodes: readonly Readonly<AnimationNode>[], sources: ReadonlySet<string>): Map<string, string> {
|
|
1209
|
+
const byId = new Map(nodes.map(node => [node.id, node]));
|
|
1210
|
+
const result = new Map<string, string>();
|
|
1211
|
+
for (const node of nodes) {
|
|
1212
|
+
let cursor: Readonly<AnimationNode> | undefined = node;
|
|
1213
|
+
while (cursor) {
|
|
1214
|
+
if (sources.has(cursor.id)) { result.set(node.id, cursor.id); break; }
|
|
1215
|
+
cursor = cursor.parent ? byId.get(cursor.parent) : undefined;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return result;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function resolveInheritedEffects(
|
|
1222
|
+
nodes: readonly Readonly<AnimationNode>[],
|
|
1223
|
+
sourceGroups: ReadonlyMap<string, string>,
|
|
1224
|
+
): Map<string, readonly Readonly<AnimationLayerEffect>[]> {
|
|
1225
|
+
const byId = new Map(nodes.map(node => [node.id, node]));
|
|
1226
|
+
const result = new Map<string, readonly Readonly<AnimationLayerEffect>[]>();
|
|
1227
|
+
for (const node of nodes) {
|
|
1228
|
+
const sourceBoundary = sourceGroups.get(node.id);
|
|
1229
|
+
let cursor: Readonly<AnimationNode> | undefined = node;
|
|
1230
|
+
while (cursor) {
|
|
1231
|
+
if (cursor.effects?.length) { result.set(node.id, cursor.effects); break; }
|
|
1232
|
+
if (cursor.id === sourceBoundary) break;
|
|
1233
|
+
cursor = cursor.parent ? byId.get(cursor.parent) : undefined;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return result;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function resolveInheritedComposites(
|
|
1240
|
+
nodes: readonly Readonly<AnimationNode>[],
|
|
1241
|
+
sourceGroups: ReadonlyMap<string, string>,
|
|
1242
|
+
): Map<string, Readonly<AnimationComposite>> {
|
|
1243
|
+
const byId = new Map(nodes.map(node => [node.id, node]));
|
|
1244
|
+
const result = new Map<string, Readonly<AnimationComposite>>();
|
|
1245
|
+
for (const node of nodes) {
|
|
1246
|
+
const sourceBoundary = sourceGroups.get(node.id);
|
|
1247
|
+
let cursor: Readonly<AnimationNode> | undefined = node;
|
|
1248
|
+
while (cursor) {
|
|
1249
|
+
if (cursor.composite) { result.set(node.id, cursor.composite); break; }
|
|
1250
|
+
if (cursor.id === sourceBoundary) break;
|
|
1251
|
+
cursor = cursor.parent ? byId.get(cursor.parent) : undefined;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return result;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function compositeLayers(composite: Readonly<AnimationComposite> | undefined): readonly Readonly<AnimationCompositeLayer>[] {
|
|
1258
|
+
if (!composite) return [];
|
|
1259
|
+
return 'layers' in composite ? composite.layers : [composite];
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function findFrame(times: Float32Array, time: number, previous: number): number {
|
|
1263
|
+
if (times.length <= 1 || time <= times[0]!) return 0;
|
|
1264
|
+
if (time >= times[times.length - 1]!) return times.length - 1;
|
|
1265
|
+
let cursor = Math.min(previous, times.length - 2);
|
|
1266
|
+
if (time >= times[cursor]! && time < times[cursor + 1]!) return cursor;
|
|
1267
|
+
let low = 0;
|
|
1268
|
+
let high = times.length - 1;
|
|
1269
|
+
while (low + 1 < high) {
|
|
1270
|
+
const middle = (low + high) >>> 1;
|
|
1271
|
+
if (times[middle]! <= time) low = middle;
|
|
1272
|
+
else high = middle;
|
|
1273
|
+
}
|
|
1274
|
+
return low;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function sampleProgress(track: ParsedAnimationTrack, frame: number, time: number): number {
|
|
1278
|
+
if (frame >= track.times.length - 1 || track.interpolation === 'step') return 0;
|
|
1279
|
+
const start = track.times[frame]!;
|
|
1280
|
+
const end = track.times[frame + 1]!;
|
|
1281
|
+
const linear = clamp((time - start) / Math.max(end - start, 1e-8), 0, 1);
|
|
1282
|
+
if (track.interpolation !== 'cubic-bezier' || !track.easings) return linear;
|
|
1283
|
+
const offset = frame * 4;
|
|
1284
|
+
return cubicBezierYForX(
|
|
1285
|
+
linear,
|
|
1286
|
+
track.easings[offset]!, track.easings[offset + 1]!,
|
|
1287
|
+
track.easings[offset + 2]!, track.easings[offset + 3]!,
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function cubicBezierYForX(x: number, x1: number, y1: number, x2: number, y2: number): number {
|
|
1292
|
+
let t = x;
|
|
1293
|
+
for (let iteration = 0; iteration < 5; iteration++) {
|
|
1294
|
+
const estimate = bezier(t, x1, x2) - x;
|
|
1295
|
+
const derivative = bezierDerivative(t, x1, x2);
|
|
1296
|
+
if (Math.abs(derivative) < 1e-5) break;
|
|
1297
|
+
t = clamp(t - estimate / derivative, 0, 1);
|
|
1298
|
+
}
|
|
1299
|
+
return bezier(t, y1, y2);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function bezier(t: number, a: number, b: number): number {
|
|
1303
|
+
const inverse = 1 - t;
|
|
1304
|
+
return 3 * inverse * inverse * t * a + 3 * inverse * t * t * b + t * t * t;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function bezierDerivative(t: number, a: number, b: number): number {
|
|
1308
|
+
const inverse = 1 - t;
|
|
1309
|
+
return 3 * inverse * inverse * a + 6 * inverse * t * (b - a) + 3 * t * t * (1 - b);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function mix(a: number, b: number, t: number): number {
|
|
1313
|
+
return a + (b - a) * t;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function spatialBezier(start: number, control1: number, control2: number, end: number, t: number): number {
|
|
1317
|
+
const inverse = 1 - t;
|
|
1318
|
+
return inverse * inverse * inverse * start
|
|
1319
|
+
+ 3 * inverse * inverse * t * control1
|
|
1320
|
+
+ 3 * inverse * t * t * control2
|
|
1321
|
+
+ t * t * t * end;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function clamp(value: number, min: number, max: number): number {
|
|
1325
|
+
return Math.min(max, Math.max(min, value));
|
|
1326
|
+
}
|