@haiyue/extensions 0.1.0 → 0.2.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/README.md +25 -0
- package/dist/animation/Animation2DComponent.d.ts +18 -0
- package/dist/animation/Animation2DExtensionRegistry.d.ts +7 -0
- package/dist/animation/Animation2DRenderSystem.d.ts +6 -0
- package/dist/animation/Animation2DRuntime.d.ts +6 -2
- package/dist/animation/AnimationTextRasterizer.d.ts +4 -0
- package/dist/animation/AnimationVisual2D.d.ts +58 -2
- package/dist/animation/interaction/InteractionRuntime.d.ts +83 -0
- package/dist/animation/interaction/runtime-types.d.ts +257 -0
- package/dist/animation.d.ts +3 -1
- package/dist/animation.js +551 -59
- package/dist/animation3d.js +1 -1
- package/dist/canvas-text.js +1 -1
- package/dist/chunks/2d-ui-animation-2d.generated-FpziAeGJ.js +4 -0
- package/dist/chunks/2d-ui-indexed-sprite.generated-DmKR0CC3.js +4 -0
- package/dist/chunks/Animation2DStateMachine-BHcA6Cu3.js +18212 -0
- package/dist/chunks/{AnimationStateMachineController-BIcAh-sT.js → AnimationStateMachineController-4sKaqh9E.js} +2 -2
- package/dist/chunks/AnimationVisual2D-8y3qp0_U.js +230 -0
- package/dist/chunks/{RenderSystem2DBase-DDT61dAu.js → RenderSystem2DBase-CF2s00vg.js} +2 -2
- package/dist/controls/VirtualJoystickControls.d.ts +46 -0
- package/dist/controls/contracts.d.ts +91 -0
- package/dist/controls.d.ts +2 -0
- package/dist/controls.js +358 -0
- package/dist/deformable-animation/runtime/DeformableMesh2DRuntimeExtension.d.ts +11 -0
- package/dist/deformable-animation/runtime/DeformableMesh2DSampler.d.ts +11 -0
- package/dist/deformable-animation.d.ts +5 -0
- package/dist/deformable-animation.js +316 -0
- package/dist/experimental-indexed-sprite.d.ts +5 -0
- package/dist/experimental-indexed-sprite.js +482 -0
- package/dist/gltf-worker-runtime.js +9 -9
- package/dist/hya-state-machine.js +3 -2
- package/dist/index.js +1 -1
- package/dist/indexed-sprite/AtlasLayout.d.ts +2 -0
- package/dist/indexed-sprite/IndexedSpriteRenderer.d.ts +23 -0
- package/dist/indexed-sprite/contracts.d.ts +89 -0
- package/dist/internal/2d-ui-shader-artifact.js +232 -23
- package/dist/ray-tracing/acceleration/bvh.d.ts +37 -0
- package/dist/ray-tracing/acceleration/index.d.ts +6 -0
- package/dist/ray-tracing/acceleration/pack.d.ts +152 -0
- package/dist/ray-tracing/acceleration/query.d.ts +4 -0
- package/dist/ray-tracing/acceleration/runtime.d.ts +30 -0
- package/dist/ray-tracing/acceleration/tlas.d.ts +4 -0
- package/dist/ray-tracing/acceleration/types.d.ts +165 -0
- package/dist/ray-tracing/denoise/index.d.ts +3 -0
- package/dist/ray-tracing/denoise/layout.d.ts +9 -0
- package/dist/ray-tracing/denoise/runtime.d.ts +23 -0
- package/dist/ray-tracing/denoise/types.d.ts +28 -0
- package/dist/ray-tracing/hybrid/contract.d.ts +2 -0
- package/dist/ray-tracing/hybrid/index.d.ts +5 -0
- package/dist/ray-tracing/hybrid/layout.d.ts +17 -0
- package/dist/ray-tracing/hybrid/plan.d.ts +2 -0
- package/dist/ray-tracing/hybrid/runtime.d.ts +31 -0
- package/dist/ray-tracing/hybrid/shaders/hybrid-artifact.generated.d.ts +625 -0
- package/dist/ray-tracing/hybrid/types.d.ts +144 -0
- package/dist/ray-tracing/lifecycle/device-recovery.d.ts +31 -0
- package/dist/ray-tracing/lifecycle/index.d.ts +2 -0
- package/dist/ray-tracing/lifecycle/types.d.ts +37 -0
- package/dist/ray-tracing/material/index.d.ts +2 -0
- package/dist/ray-tracing/material/pack.d.ts +5 -0
- package/dist/ray-tracing/material/types.d.ts +63 -0
- package/dist/ray-tracing/reference/index.d.ts +122 -0
- package/dist/ray-tracing/renderer/index.d.ts +7 -0
- package/dist/ray-tracing/renderer/layout.d.ts +20 -0
- package/dist/ray-tracing/renderer/plan.d.ts +2 -0
- package/dist/ray-tracing/renderer/reference.d.ts +17 -0
- package/dist/ray-tracing/renderer/runtime.d.ts +39 -0
- package/dist/ray-tracing/renderer/scene.d.ts +4 -0
- package/dist/ray-tracing/renderer/types.d.ts +115 -0
- package/dist/ray-tracing/sampling/index.d.ts +8 -0
- package/dist/ray-tracing/sampling/layout.d.ts +12 -0
- package/dist/ray-tracing/sampling/plan.d.ts +5 -0
- package/dist/ray-tracing/sampling/revision.d.ts +15 -0
- package/dist/ray-tracing/sampling/runtime.d.ts +44 -0
- package/dist/ray-tracing/sampling/sequence.d.ts +2 -0
- package/dist/ray-tracing/sampling/types.d.ts +96 -0
- package/dist/ray-tracing/scene/index.d.ts +41 -0
- package/dist/ray-tracing/traversal/index.d.ts +4 -0
- package/dist/ray-tracing/traversal/layout.d.ts +16 -0
- package/dist/ray-tracing/traversal/plan.d.ts +2 -0
- package/dist/ray-tracing/traversal/runtime.d.ts +25 -0
- package/dist/ray-tracing/traversal/types.d.ts +79 -0
- package/dist/ray-tracing/worker/client.d.ts +25 -0
- package/dist/ray-tracing/worker/index.d.ts +5 -0
- package/dist/ray-tracing/worker/runtime.d.ts +11 -0
- package/dist/ray-tracing/worker/serialization.d.ts +16 -0
- package/dist/ray-tracing/worker/source.d.ts +1 -0
- package/dist/ray-tracing/worker/types.d.ts +91 -0
- package/dist/ray-tracing.d.ts +17 -0
- package/dist/ray-tracing.js +7666 -0
- package/dist/spine.js +1 -1
- package/dist/tilemap.js +1 -1
- package/package.json +26 -23
- package/dist/animation/Animation2DStateMachineVisualRuntime.d.ts +0 -27
- package/dist/animation/Particle2DDescriptorAdapter.d.ts +0 -5
- package/dist/animation3d/Animation3DResource.d.ts +0 -28
- package/dist/animation3d/hya/HyaAnimation3DStateMachineRuntime.d.ts +0 -65
- package/dist/animation3d/hya/index.d.ts +0 -4
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.d.ts +0 -81
- package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.d.ts +0 -39
- package/dist/animation3d/runtime/integration/index.d.ts +0 -2
- package/dist/animation3d/runtime/mixer/Action.d.ts +0 -150
- package/dist/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.d.ts +0 -6
- package/dist/animation3d/runtime/mixer/Mixer.d.ts +0 -59
- package/dist/animation3d/runtime/mixer/PoseBuffer.d.ts +0 -1
- package/dist/animation3d/runtime/mixer/TrackSampler.d.ts +0 -24
- package/dist/animation3d/runtime/mixer/index.d.ts +0 -4
- package/dist/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.d.ts +0 -1
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.d.ts +0 -1
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineController.d.ts +0 -1
- package/dist/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.d.ts +0 -1
- package/dist/animation3d/runtime/state-machine/index.d.ts +0 -6
- package/dist/benchmark.d.ts +0 -30
- package/dist/chunks/2d-ui-animation-2d.generated-D4aHzFU2.js +0 -4
- package/dist/chunks/Animation2DStateMachine-9IonguNa.js +0 -3245
- package/dist/gltf/GltfAccessorReader.d.ts +0 -7
- package/dist/gltf/GltfDracoDecoder.d.ts +0 -4
- package/dist/gltf/GltfLoaderErrors.d.ts +0 -4
- package/dist/gltf/GltfMaterialDescriptor.d.ts +0 -23
- package/dist/gltf/GltfMaterialLoader.d.ts +0 -6
- package/dist/gltf-worker-runtime.d.ts +0 -7
- package/dist/plugins/pluginUtils.d.ts +0 -20
- package/dist/plugins.d.ts +0 -6
- package/dist/shaders/generated/2d-ui-artifact.generated.d.ts +0 -658
- package/dist/spine/Spine2DGpuRenderer.d.ts +0 -65
- package/dist/spine/Spine2DRuntime.d.ts +0 -103
- package/dist/spine/SpineAnimationBoneTimelines.d.ts +0 -8
- package/dist/spine/SpineFloatBuilder.d.ts +0 -15
- package/dist/spine/SpinePathConstraintSolver.d.ts +0 -32
- package/dist/spine/SpineSkeletonRuntime.d.ts +0 -185
- package/dist/spine/SpineSlotAnimation.d.ts +0 -17
- package/dist/spine/SpineTimelineRuntime.d.ts +0 -25
- package/dist/spine/SpineVertexBuilder.d.ts +0 -61
- package/dist/tilemap/Tilemap2DRenderer.d.ts +0 -24
- package/dist/utils/arrayAccess.d.ts +0 -4
- package/dist/utils/camera2d.d.ts +0 -12
- package/src/animation/Animation2DComponent.ts +0 -135
- package/src/animation/Animation2DExtensionRegistry.ts +0 -52
- package/src/animation/Animation2DRenderSystem.ts +0 -1026
- package/src/animation/Animation2DRuntime.ts +0 -1326
- package/src/animation/Animation2DStateMachine.ts +0 -39
- package/src/animation/Animation2DStateMachineComponent.ts +0 -228
- package/src/animation/Animation2DStateMachineRuntime.ts +0 -141
- package/src/animation/Animation2DStateMachineSystem.ts +0 -60
- package/src/animation/Animation2DStateMachineVisualRuntime.ts +0 -282
- package/src/animation/Animation2DSystem.ts +0 -72
- package/src/animation/AnimationAssetLoader.ts +0 -84
- package/src/animation/AnimationAudioClip.ts +0 -140
- package/src/animation/AnimationPathTessellator.ts +0 -691
- package/src/animation/AnimationTextRasterizer.ts +0 -340
- package/src/animation/AnimationVisual2D.ts +0 -153
- package/src/animation/HyaAnimation2DClipAdapter.ts +0 -845
- package/src/animation/Particle2DDescriptorAdapter.ts +0 -35
- package/src/animation/runtime/mixer/Action.ts +0 -876
- package/src/animation/runtime/mixer/Mixer.ts +0 -505
- package/src/animation/runtime/mixer/PoseBuffer.ts +0 -105
- package/src/animation/runtime/mixer/Sampler.ts +0 -252
- package/src/animation/runtime/mixer/Types.ts +0 -138
- package/src/animation/runtime/mixer/index.ts +0 -31
- package/src/animation-state-machine/AnimationStateMachine.ts +0 -146
- package/src/animation-state-machine/AnimationStateMachineChannels.ts +0 -116
- package/src/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.ts +0 -197
- package/src/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.ts +0 -503
- package/src/animation-state-machine/runtime/AnimationStateMachineCompiler.ts +0 -763
- package/src/animation-state-machine/runtime/AnimationStateMachineController.ts +0 -987
- package/src/animation-state-machine/runtime/AnimationStateMachineMixerPort.ts +0 -54
- package/src/animation-state-machine/runtime/index.ts +0 -47
- package/src/animation.ts +0 -33
- package/src/animation3d/Animation3DAction.ts +0 -73
- package/src/animation3d/Animation3DBinding.ts +0 -116
- package/src/animation3d/Animation3DClip.ts +0 -22
- package/src/animation3d/Animation3DError.ts +0 -34
- package/src/animation3d/Animation3DMixer.ts +0 -82
- package/src/animation3d/Animation3DPose.ts +0 -100
- package/src/animation3d/Animation3DPoseBuffer.ts +0 -85
- package/src/animation3d/Animation3DResource.ts +0 -49
- package/src/animation3d/Animation3DStateMachine.ts +0 -126
- package/src/animation3d/Animation3DStateMachineRuntime.ts +0 -191
- package/src/animation3d/Animation3DTrack.ts +0 -29
- package/src/animation3d/hya/HyaAnimation3DRuntime.ts +0 -756
- package/src/animation3d/hya/HyaAnimation3DStateMachineRuntime.ts +0 -618
- package/src/animation3d/hya/HyaAnimation3DTypes.ts +0 -168
- package/src/animation3d/hya/index.ts +0 -10
- package/src/animation3d/index.ts +0 -76
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.ts +0 -325
- package/src/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.ts +0 -158
- package/src/animation3d/runtime/integration/index.ts +0 -9
- package/src/animation3d/runtime/mixer/Action.ts +0 -833
- package/src/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.ts +0 -26
- package/src/animation3d/runtime/mixer/Mixer.ts +0 -562
- package/src/animation3d/runtime/mixer/PoseBuffer.ts +0 -1
- package/src/animation3d/runtime/mixer/TrackSampler.ts +0 -399
- package/src/animation3d/runtime/mixer/index.ts +0 -8
- package/src/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.ts +0 -4
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.ts +0 -1
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineController.ts +0 -1
- package/src/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.ts +0 -1
- package/src/animation3d/runtime/state-machine/index.ts +0 -45
- package/src/animation3d.ts +0 -5
- package/src/benchmark.ts +0 -168
- package/src/canvas-text/CanvasText2DRenderSystem.ts +0 -292
- package/src/canvas-text/CanvasTextComponent.ts +0 -47
- package/src/canvas-text.ts +0 -4
- package/src/experimental-gltf-worker.ts +0 -18
- package/src/experimental-spine-worker.ts +0 -9
- package/src/gltf/GltfAccessorReader.ts +0 -271
- package/src/gltf/GltfAnimation3DAdapter.ts +0 -680
- package/src/gltf/GltfAnimationRuntime.ts +0 -392
- package/src/gltf/GltfAssetWorkerClient.ts +0 -136
- package/src/gltf/GltfCompatibilityReport.ts +0 -214
- package/src/gltf/GltfConservativeBounds.ts +0 -219
- package/src/gltf/GltfDracoDecoder.ts +0 -251
- package/src/gltf/GltfExtensionAdapter.ts +0 -340
- package/src/gltf/GltfLoaderContract.ts +0 -333
- package/src/gltf/GltfLoaderErrors.ts +0 -59
- package/src/gltf/GltfMaterialDescriptor.ts +0 -134
- package/src/gltf/GltfMaterialEncoder.ts +0 -173
- package/src/gltf/GltfMaterialLoader.ts +0 -293
- package/src/gltf/GltfModelComponent.ts +0 -115
- package/src/gltf/GltfModelSystem.ts +0 -208
- package/src/gltf/GltfPlugin.ts +0 -84
- package/src/gltf/GltfSchema.ts +0 -229
- package/src/gltf/GltfUvSemanticPlanner.ts +0 -166
- package/src/gltf/gltfLoader.ts +0 -910
- package/src/gltf-animation3d.ts +0 -10
- package/src/gltf-worker-runtime.ts +0 -10
- package/src/gltf.ts +0 -56
- package/src/grid/Grid2DComponent.ts +0 -69
- package/src/grid.ts +0 -2
- package/src/hya-state-machine.ts +0 -19
- package/src/index.ts +0 -2
- package/src/plugins/pluginUtils.ts +0 -50
- package/src/plugins.ts +0 -6
- package/src/shaders/generated/2d-ui-animation-2d.generated.wgsl +0 -242
- package/src/shaders/generated/2d-ui-artifact.generated.ts +0 -867
- package/src/shaders/generated/2d-ui-canvas-text-2d.generated.wgsl +0 -38
- package/src/shaders/generated/2d-ui-spine2d.generated.wgsl +0 -41
- package/src/shaders/generated/2d-ui-tilemap2d.generated.wgsl +0 -36
- package/src/spine/Spine2DComponent.ts +0 -168
- package/src/spine/Spine2DGpuRenderer.ts +0 -449
- package/src/spine/Spine2DRenderSystem.ts +0 -199
- package/src/spine/Spine2DRuntime.ts +0 -1529
- package/src/spine/SpineAnimationBoneTimelines.ts +0 -30
- package/src/spine/SpineAssetParser.ts +0 -20
- package/src/spine/SpineAssetWorkerClient.ts +0 -112
- package/src/spine/SpineAssetWorkerContract.ts +0 -4
- package/src/spine/SpineAtlasParser.ts +0 -127
- package/src/spine/SpineFloatBuilder.ts +0 -66
- package/src/spine/SpinePathConstraintSolver.ts +0 -582
- package/src/spine/SpinePlugin.ts +0 -76
- package/src/spine/SpineSkeletonRuntime.ts +0 -521
- package/src/spine/SpineSlotAnimation.ts +0 -195
- package/src/spine/SpineTimelineRuntime.ts +0 -453
- package/src/spine/SpineVertexBuilder.ts +0 -686
- package/src/spine.ts +0 -7
- package/src/tilemap/Tilemap2DComponent.ts +0 -232
- package/src/tilemap/Tilemap2DRenderSystem.ts +0 -58
- package/src/tilemap/Tilemap2DRenderer.ts +0 -289
- package/src/tilemap/TilemapPlugin.ts +0 -75
- package/src/tilemap.ts +0 -6
- package/src/tween/Tween2DComponent.ts +0 -83
- package/src/tween/Tween2DSystem.ts +0 -78
- package/src/tween.ts +0 -4
- package/src/types/earcut.d.ts +0 -7
- package/src/types/wgsl.d.ts +0 -4
- package/src/utils/RenderSystem2DBase.ts +0 -164
- package/src/utils/arrayAccess.ts +0 -17
- package/src/utils/camera2d.ts +0 -68
- package/src/utils/render2dGpu.ts +0 -132
- package/test/fixtures/gltf/animation-characterization.gltf +0 -344
|
@@ -1,3245 +0,0 @@
|
|
|
1
|
-
import { HYA_STATE_MACHINE_EXTENSION_ID, AnimationFormatError, ANIMATION_VECTOR_SHAPE_EXTENSION_ID } from '@haiyue/animation-spec';
|
|
2
|
-
import { Geometry2D, Component, Entity, Transform2D } from '@haiyue/engine';
|
|
3
|
-
import { createRect2D } from '@haiyue/engine/geometry';
|
|
4
|
-
import { UniqueCheckType } from '@haiyue/engine/ecs';
|
|
5
|
-
import { ParticleEmitter2D } from '@haiyue/engine/components';
|
|
6
|
-
import { A as Animation3DStateMachineController, c as compileAnimationStateMachineDefinition } from './AnimationStateMachineController-BIcAh-sT.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A vertex in a circular doubly linked list representing a polygon ring.
|
|
10
|
-
* `prev`/`next` are always linked (set immediately after {@link createNode}), so they're typed
|
|
11
|
-
* non-null; `prevZ`/`nextZ` are the z-order list links and are null at the ends.
|
|
12
|
-
*
|
|
13
|
-
* @typedef {object} Node
|
|
14
|
-
* @property {number} i vertex index in the coordinates array
|
|
15
|
-
* @property {number} x vertex x coordinate
|
|
16
|
-
* @property {number} y vertex y coordinate
|
|
17
|
-
* @property {Node} prev previous vertex node in the polygon ring
|
|
18
|
-
* @property {Node} next next vertex node in the polygon ring
|
|
19
|
-
* @property {number} z z-order curve value; doubles as the owning block index during eliminateHoles
|
|
20
|
-
* @property {Node | null} prevZ previous node in z-order
|
|
21
|
-
* @property {Node | null} nextZ next node in z-order
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
// single-vertex holes to preserve through filterPoints (steiner points); kept off the Node
|
|
25
|
-
// shape since they're rare — the empty-set fast path means non-steiner inputs pay nothing
|
|
26
|
-
/** @type {Set<Node>} */
|
|
27
|
-
const steiners = new Set();
|
|
28
|
-
|
|
29
|
-
// set by filterPoints whenever it removes at least one node; read by earcutLinked's stall
|
|
30
|
-
// handler to decide whether another clip pass is worth attempting before the costlier stages
|
|
31
|
-
let filteredOut = false;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Triangulate a polygon given as a flat array of vertex coordinates.
|
|
35
|
-
*
|
|
36
|
-
* @param {ArrayLike<number>} data flat array of vertex coordinates
|
|
37
|
-
* @param {ArrayLike<number> | null} [holeIndices] indices (in vertices, not coordinates) where each hole ring starts
|
|
38
|
-
* @param {number} [dim=2] number of coordinates per vertex in `data`
|
|
39
|
-
* @returns {number[]} triangles as triplets of vertex indices into `data`
|
|
40
|
-
* @example earcut([10,0, 0,50, 60,60, 70,10]); // [1,0,3, 3,2,1]
|
|
41
|
-
*/
|
|
42
|
-
function earcut(data, holeIndices, dim = 2) {
|
|
43
|
-
|
|
44
|
-
const hasHoles = holeIndices && holeIndices.length;
|
|
45
|
-
const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
|
|
46
|
-
if (steiners.size) steiners.clear();
|
|
47
|
-
|
|
48
|
-
let outerNode = linkedList(data, 0, outerLen, dim, true);
|
|
49
|
-
/** @type {number[]} */
|
|
50
|
-
const triangles = [];
|
|
51
|
-
|
|
52
|
-
if (!outerNode || outerNode.next === outerNode.prev) return triangles;
|
|
53
|
-
|
|
54
|
-
let minX = 0, minY = 0, invSize = 0;
|
|
55
|
-
|
|
56
|
-
if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
|
|
57
|
-
|
|
58
|
-
// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
|
|
59
|
-
if (data.length > 80 * dim) {
|
|
60
|
-
minX = data[0];
|
|
61
|
-
minY = data[1];
|
|
62
|
-
let maxX = minX;
|
|
63
|
-
let maxY = minY;
|
|
64
|
-
|
|
65
|
-
for (let i = dim; i < outerLen; i += dim) {
|
|
66
|
-
const x = data[i];
|
|
67
|
-
const y = data[i + 1];
|
|
68
|
-
if (x < minX) minX = x;
|
|
69
|
-
if (y < minY) minY = y;
|
|
70
|
-
if (x > maxX) maxX = x;
|
|
71
|
-
if (y > maxY) maxY = y;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// minX, minY and invSize are later used to transform coords into integers for z-order calculation
|
|
75
|
-
invSize = Math.max(maxX - minX, maxY - minY);
|
|
76
|
-
invSize = invSize !== 0 ? 32767 / invSize : 0;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
earcutLinked(outerNode, triangles, minX, minY, invSize);
|
|
80
|
-
|
|
81
|
-
return triangles;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// create a circular doubly linked list from polygon points in the specified winding order
|
|
85
|
-
/** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @param {boolean} clockwise @returns {Node | null} */
|
|
86
|
-
function linkedList(data, start, end, dim, clockwise) {
|
|
87
|
-
/** @type {Node | null} */
|
|
88
|
-
let last = null;
|
|
89
|
-
|
|
90
|
-
if (clockwise === (signedArea$1(data, start, end, dim) > 0)) {
|
|
91
|
-
for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
|
|
92
|
-
} else {
|
|
93
|
-
for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (last && equals(last, last.next)) {
|
|
97
|
-
removeNode(last);
|
|
98
|
-
last = last.next;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return last;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Remove collinear or coincident points; removability depends only on a node's immediate
|
|
105
|
-
// neighbors, so we sweep forward and re-check the predecessor after each removal. With no `end`
|
|
106
|
-
// we sweep the whole ring, lapping until nothing is removable (the fixpoint the clipper needs).
|
|
107
|
-
// With an explicit `end` we heal only the dirty window around a bridge/diagonal cut, stopping at
|
|
108
|
-
// `end` rather than lapping — O(window) instead of O(ring).
|
|
109
|
-
/** @param {Node} start @param {Node} [end] @returns {Node} */
|
|
110
|
-
function filterPoints(start, end = start) {
|
|
111
|
-
const full = end === start;
|
|
112
|
-
|
|
113
|
-
let p = start, again;
|
|
114
|
-
do {
|
|
115
|
-
again = false;
|
|
116
|
-
if (p !== p.next && (steiners.size === 0 || !steiners.has(p)) &&
|
|
117
|
-
(equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
|
|
118
|
-
if (full || p === end) end = p.prev; // pull the stop bound back past the removal
|
|
119
|
-
filteredOut = true;
|
|
120
|
-
removeNode(p);
|
|
121
|
-
p = p.prev; // re-check the predecessor
|
|
122
|
-
again = true;
|
|
123
|
-
} else if (full || p !== end) {
|
|
124
|
-
p = p.next;
|
|
125
|
-
again = !full; // local heal: keep looping until the sweep reaches end
|
|
126
|
-
}
|
|
127
|
-
} while (again || p !== end);
|
|
128
|
-
|
|
129
|
-
return end;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// main ear slicing loop which triangulates a polygon (given as a linked list)
|
|
133
|
-
/** @param {Node} ear @param {number[]} triangles @param {number} minX @param {number} minY @param {number} invSize */
|
|
134
|
-
function earcutLinked(ear, triangles, minX, minY, invSize) {
|
|
135
|
-
// interlink polygon nodes in z-order
|
|
136
|
-
if (invSize) indexCurve(ear, minX, minY, invSize);
|
|
137
|
-
|
|
138
|
-
let stop = ear, cured = false;
|
|
139
|
-
|
|
140
|
-
// iterate through ears, slicing them one by one
|
|
141
|
-
while (ear.prev !== ear.next) {
|
|
142
|
-
const prev = ear.prev;
|
|
143
|
-
/** @type {Node} */
|
|
144
|
-
const next = ear.next;
|
|
145
|
-
|
|
146
|
-
if (area(prev, ear, next) < 0 && (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear))) {
|
|
147
|
-
triangles.push(prev.i, ear.i, next.i); // cut off the triangle
|
|
148
|
-
|
|
149
|
-
removeNode(ear);
|
|
150
|
-
ear = next;
|
|
151
|
-
stop = next;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
ear = next;
|
|
156
|
-
|
|
157
|
-
// if we looped through the whole remaining polygon and can't find any more ears
|
|
158
|
-
if (ear === stop) {
|
|
159
|
-
// try filtering collinear/coincident points and slicing again — repeat as long as
|
|
160
|
-
// filtering actually removes nodes, since each removal can expose new ears
|
|
161
|
-
filteredOut = false;
|
|
162
|
-
ear = filterPoints(ear);
|
|
163
|
-
if (filteredOut) { stop = ear; continue; }
|
|
164
|
-
|
|
165
|
-
// filtering is exhausted: cure small local self-intersections once, then retry
|
|
166
|
-
if (!cured) {
|
|
167
|
-
ear = cureLocalIntersections(ear, triangles);
|
|
168
|
-
stop = ear;
|
|
169
|
-
cured = true;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// as a last resort, try splitting the remaining polygon into two
|
|
174
|
-
splitEarcut(ear, triangles, minX, minY, invSize);
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// check whether a polygon node forms a valid ear with adjacent nodes
|
|
181
|
-
/** @param {Node} ear @returns {boolean} */
|
|
182
|
-
function isEar(ear) {
|
|
183
|
-
// reflex check (area(a, b, c) >= 0) is hoisted into the earcutLinked caller to avoid non-inlined call here
|
|
184
|
-
const a = ear.prev, b = ear, c = ear.next,
|
|
185
|
-
ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y,
|
|
186
|
-
x0 = Math.min(ax, bx, cx), // triangle bbox
|
|
187
|
-
y0 = Math.min(ay, by, cy),
|
|
188
|
-
x1 = Math.max(ax, bx, cx),
|
|
189
|
-
y1 = Math.max(ay, by, cy);
|
|
190
|
-
|
|
191
|
-
// make sure we don't have other points inside the potential ear
|
|
192
|
-
let p = c.next;
|
|
193
|
-
while (p !== a) {
|
|
194
|
-
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && !(ax === p.x && ay === p.y) &&
|
|
195
|
-
pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
|
|
196
|
-
p = p.next;
|
|
197
|
-
}
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** @param {Node} ear @param {number} minX @param {number} minY @param {number} invSize @returns {boolean} */
|
|
202
|
-
function isEarHashed(ear, minX, minY, invSize) {
|
|
203
|
-
// reflex check is hoisted into the earcutLinked caller (see isEar)
|
|
204
|
-
const a = ear.prev, b = ear, c = ear.next,
|
|
205
|
-
ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y,
|
|
206
|
-
x0 = Math.min(ax, bx, cx), // triangle bbox
|
|
207
|
-
y0 = Math.min(ay, by, cy),
|
|
208
|
-
x1 = Math.max(ax, bx, cx),
|
|
209
|
-
y1 = Math.max(ay, by, cy),
|
|
210
|
-
minZ = zOrder(x0, y0, minX, minY, invSize), // z-order range for the current triangle bbox;
|
|
211
|
-
maxZ = zOrder(x1, y1, minX, minY, invSize);
|
|
212
|
-
|
|
213
|
-
let p = ear.prevZ;
|
|
214
|
-
while (p && p.z >= minZ) { // look for points inside the triangle in decreasing z-order
|
|
215
|
-
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c && !(ax === p.x && ay === p.y) &&
|
|
216
|
-
pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
|
|
217
|
-
p = p.prevZ;
|
|
218
|
-
}
|
|
219
|
-
let n = ear.nextZ;
|
|
220
|
-
while (n && n.z <= maxZ) { // look for points in increasing z-order
|
|
221
|
-
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c && !(ax === n.x && ay === n.y) &&
|
|
222
|
-
pointInTriangle(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
|
|
223
|
-
n = n.nextZ;
|
|
224
|
-
}
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// go through all polygon nodes and cure small local self-intersections
|
|
229
|
-
/** @param {Node} start @param {number[]} triangles @returns {Node} */
|
|
230
|
-
function cureLocalIntersections(start, triangles) {
|
|
231
|
-
let p = start;
|
|
232
|
-
let cured = false;
|
|
233
|
-
do {
|
|
234
|
-
const a = p.prev,
|
|
235
|
-
b = p.next.next;
|
|
236
|
-
|
|
237
|
-
if (intersects(a, p, p.next, b, false) && locallyInside(a, b) && locallyInside(b, a)) {
|
|
238
|
-
|
|
239
|
-
triangles.push(a.i, p.i, b.i);
|
|
240
|
-
|
|
241
|
-
// remove two nodes involved
|
|
242
|
-
removeNode(p);
|
|
243
|
-
removeNode(p.next);
|
|
244
|
-
|
|
245
|
-
p = start = b;
|
|
246
|
-
cured = true;
|
|
247
|
-
}
|
|
248
|
-
p = p.next;
|
|
249
|
-
} while (p !== start);
|
|
250
|
-
|
|
251
|
-
return cured ? filterPoints(p) : p;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// try splitting polygon into two and triangulate them independently
|
|
255
|
-
/** @param {Node} start @param {number[]} triangles @param {number} minX @param {number} minY @param {number} invSize */
|
|
256
|
-
function splitEarcut(start, triangles, minX, minY, invSize) {
|
|
257
|
-
// look for a valid diagonal that divides the polygon into two
|
|
258
|
-
let a = start;
|
|
259
|
-
do {
|
|
260
|
-
let b = a.next.next;
|
|
261
|
-
while (b !== a.prev) {
|
|
262
|
-
if (a.i !== b.i && isValidDiagonal(a, b)) {
|
|
263
|
-
// split the polygon in two by the diagonal
|
|
264
|
-
let c = splitPolygon(a, b);
|
|
265
|
-
|
|
266
|
-
// filter colinear points around the cuts
|
|
267
|
-
a = filterPoints(a, a.next);
|
|
268
|
-
c = filterPoints(c, c.next);
|
|
269
|
-
|
|
270
|
-
// run earcut on each half
|
|
271
|
-
earcutLinked(a, triangles, minX, minY, invSize);
|
|
272
|
-
earcutLinked(c, triangles, minX, minY, invSize);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
b = b.next;
|
|
276
|
-
}
|
|
277
|
-
a = a.next;
|
|
278
|
-
} while (a !== start);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// true only while eliminateHoles merges holes, so removeNode keeps the block index live (growBlock)
|
|
282
|
-
let indexActive = false;
|
|
283
|
-
|
|
284
|
-
// link every hole into the outer loop, producing a single-ring polygon without holes
|
|
285
|
-
/** @param {ArrayLike<number>} data @param {ArrayLike<number>} holeIndices @param {Node} outerNode @param {number} dim @returns {Node} */
|
|
286
|
-
function eliminateHoles(data, holeIndices, outerNode, dim) {
|
|
287
|
-
const queue = [];
|
|
288
|
-
|
|
289
|
-
for (let i = 0, len = holeIndices.length; i < len; i++) {
|
|
290
|
-
const start = holeIndices[i] * dim;
|
|
291
|
-
const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
|
|
292
|
-
const list = /** @type {Node} */ (linkedList(data, start, end, dim, false));
|
|
293
|
-
if (list === list.next) steiners.add(list);
|
|
294
|
-
queue.push(getLeftmost(list));
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
queue.sort(compareXYSlope);
|
|
298
|
-
|
|
299
|
-
// block-bbox index for findHoleBridge, grown append-only as holes merge (see notes
|
|
300
|
-
// above buildBlockIndex). Seed it with the outer ring, then append each merged hole.
|
|
301
|
-
buildBlockIndex(data.length / dim, holeIndices.length);
|
|
302
|
-
indexSegment(outerNode, outerNode);
|
|
303
|
-
|
|
304
|
-
// process holes from left to right; indexActive lets removeNode keep block bboxes live as
|
|
305
|
-
// filterPoints heals edges during merges (see growBlock)
|
|
306
|
-
indexActive = true;
|
|
307
|
-
for (let i = 0; i < queue.length; i++) {
|
|
308
|
-
outerNode = eliminateHole(queue[i], outerNode);
|
|
309
|
-
}
|
|
310
|
-
indexActive = false;
|
|
311
|
-
|
|
312
|
-
// collapse collinear/coincident points across the whole merged ring once before clipping
|
|
313
|
-
return filterPoints(outerNode);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** @param {Node} a @param {Node} b @returns {number} */
|
|
317
|
-
function compareXYSlope(a, b) {
|
|
318
|
-
// when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
|
|
319
|
-
// the bridge to the outer shell is always the point that they meet at.
|
|
320
|
-
return a.x - b.x || a.y - b.y ||
|
|
321
|
-
(a.next.y - a.y) / (a.next.x - a.x) -
|
|
322
|
-
(b.next.y - b.y) / (b.next.x - b.x);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// find a bridge between vertices that connects hole with an outer ring and link it
|
|
326
|
-
/** @param {Node} hole @param {Node} outerNode @returns {Node} */
|
|
327
|
-
function eliminateHole(hole, outerNode) {
|
|
328
|
-
const bridge = findHoleBridge(hole, outerNode);
|
|
329
|
-
if (!bridge) {
|
|
330
|
-
return outerNode;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const bridgeReverse = splitPolygon(bridge, hole);
|
|
334
|
-
|
|
335
|
-
// index the merged-in segment before filtering: in ring order the splice runs
|
|
336
|
-
// bridge -> hole -> bridgeReverse -> bridge2 -> (bridge's old next), covering the
|
|
337
|
-
// hole's edges and both new slit edges. filterPoints below only drops collinear /
|
|
338
|
-
// coincident points, so these bboxes stay valid (conservative) supersets.
|
|
339
|
-
const bridge2 = bridgeReverse.next;
|
|
340
|
-
indexSegment(bridge, bridge2.next);
|
|
341
|
-
|
|
342
|
-
// heal collinear/coincident points around the two new slit edges
|
|
343
|
-
filterPoints(bridgeReverse, bridgeReverse.next);
|
|
344
|
-
return filterPoints(bridge, bridge.next);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Block-bbox index for findHoleBridge (issue #183): one [minX,minY,maxX,maxY] bbox per K
|
|
348
|
-
// consecutive ring edges, in a flat Float64Array, so the leftward-ray scan can skip whole
|
|
349
|
-
// blocks in O(1) instead of walking the entire merged ring. Grown append-only — the outer
|
|
350
|
-
// ring seeds it, then each merged hole appends a segment (head node, stop node, K-blocks
|
|
351
|
-
// over head..stop); independent segments, not a ring tiling, since splices land mid-ring.
|
|
352
|
-
// Buffers are sized once from the input upper bound and reused across calls.
|
|
353
|
-
//
|
|
354
|
-
// filterPoints only drops collinear/coincident points, so a stale bbox stays a conservative
|
|
355
|
-
// superset of its live edges (never a false skip); the scan skips dead nodes (p.prev.next !==
|
|
356
|
-
// p) and lazily advances a dead stop. Blocks are scanned in append (not ring) order, so the
|
|
357
|
-
// chosen bridge can differ from the un-indexed code — a different but equally valid result.
|
|
358
|
-
const K = 16; // edges per block
|
|
359
|
-
|
|
360
|
-
let blockBBox = new Float64Array(0); // [minX,minY,maxX,maxY] per block
|
|
361
|
-
let numBlocks = 0;
|
|
362
|
-
/** @type {Node[]} */
|
|
363
|
-
const blockHead = []; // first node of each block's segment
|
|
364
|
-
/** @type {Node[]} */
|
|
365
|
-
const blockStop = []; // node just past each block's segment (exclusive walk bound)
|
|
366
|
-
|
|
367
|
-
/** @param {number} maxNodes @param {number} numHoles */
|
|
368
|
-
function buildBlockIndex(maxNodes, numHoles) {
|
|
369
|
-
// upper bound: every input node indexed once, +2 bridge nodes per hole, plus a partial
|
|
370
|
-
// trailing block per appended segment (outer ring + one per hole)
|
|
371
|
-
const maxBlocks = Math.ceil((maxNodes + 2 * numHoles) / K) + numHoles + 2;
|
|
372
|
-
if (blockBBox.length < maxBlocks * 4) blockBBox = new Float64Array(maxBlocks * 4);
|
|
373
|
-
numBlocks = 0;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// index the ring run head..stop (exclusive) as ceil(len / K) blocks; head === stop means
|
|
377
|
-
// the whole ring. each block's bbox covers both endpoints of every edge it owns.
|
|
378
|
-
/** @param {Node} head @param {Node} stop */
|
|
379
|
-
function indexSegment(head, stop) {
|
|
380
|
-
let p = head;
|
|
381
|
-
do {
|
|
382
|
-
const b = numBlocks++;
|
|
383
|
-
blockHead[b] = p;
|
|
384
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
385
|
-
let k = 0;
|
|
386
|
-
do {
|
|
387
|
-
const c = p.next; // edge p->c; bbox must bound both endpoints
|
|
388
|
-
p.z = b; // reuse z as the owning block during eliminateHoles (see growBlock)
|
|
389
|
-
if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
|
|
390
|
-
if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
|
|
391
|
-
if (c.x < minX) minX = c.x; if (c.x > maxX) maxX = c.x;
|
|
392
|
-
if (c.y < minY) minY = c.y; if (c.y > maxY) maxY = c.y;
|
|
393
|
-
p = c;
|
|
394
|
-
} while (++k < K && p !== stop);
|
|
395
|
-
blockStop[b] = p;
|
|
396
|
-
const g = b * 4;
|
|
397
|
-
blockBBox[g] = minX; blockBBox[g + 1] = minY; blockBBox[g + 2] = maxX; blockBBox[g + 3] = maxY;
|
|
398
|
-
} while (p !== stop);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// when filterPoints heals an edge head->tail (removing the collinear node between them), the
|
|
402
|
-
// healed edge can extend past head's frozen block bbox if its old far endpoint lived in another
|
|
403
|
-
// block; grow head's block bbox to cover tail so the leftward-ray prune can't false-skip it.
|
|
404
|
-
/** @param {Node} head @param {Node} tail */
|
|
405
|
-
function growBlock(head, tail) {
|
|
406
|
-
const g = head.z * 4;
|
|
407
|
-
if (tail.x < blockBBox[g]) blockBBox[g] = tail.x;
|
|
408
|
-
if (tail.y < blockBBox[g + 1]) blockBBox[g + 1] = tail.y;
|
|
409
|
-
if (tail.x > blockBBox[g + 2]) blockBBox[g + 2] = tail.x;
|
|
410
|
-
if (tail.y > blockBBox[g + 3]) blockBBox[g + 3] = tail.y;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** @param {number} b @returns {Node} */
|
|
414
|
-
function liveBlockStop(b) {
|
|
415
|
-
let stop = blockStop[b];
|
|
416
|
-
while (stop.prev.next !== stop) stop = stop.next;
|
|
417
|
-
blockStop[b] = stop;
|
|
418
|
-
return stop;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// the block's head node can be removed by filterPoints during merges; advance it to the next
|
|
422
|
-
// live node so the walk doesn't start on (and immediately terminate at) a dead node. For the
|
|
423
|
-
// single full-ring seed block (head === stop) the same forward advance keeps them equal, so the
|
|
424
|
-
// do-while still laps the whole ring instead of collapsing to an empty walk.
|
|
425
|
-
/** @param {number} b @returns {Node} */
|
|
426
|
-
function liveBlockHead(b) {
|
|
427
|
-
let head = blockHead[b];
|
|
428
|
-
while (head.prev.next !== head) head = head.next;
|
|
429
|
-
blockHead[b] = head;
|
|
430
|
-
return head;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// David Eberly's algorithm for finding a bridge between hole and outer polygon
|
|
434
|
-
/** @param {Node} hole @param {Node} outerNode @returns {Node | null} */
|
|
435
|
-
function findHoleBridge(hole, outerNode) {
|
|
436
|
-
let p = outerNode;
|
|
437
|
-
const hx = hole.x;
|
|
438
|
-
const hy = hole.y;
|
|
439
|
-
let qx = -Infinity;
|
|
440
|
-
/** @type {Node | undefined} */
|
|
441
|
-
let m;
|
|
442
|
-
|
|
443
|
-
// find a segment intersected by a ray from the hole's leftmost point to the left;
|
|
444
|
-
// segment's endpoint with lesser x will be potential connection point
|
|
445
|
-
// unless they intersect at a vertex, then choose the vertex
|
|
446
|
-
if (equals(hole, p)) return p;
|
|
447
|
-
|
|
448
|
-
// scan blocks; skip any whose bbox can't hold a crossing that beats qx and lies left
|
|
449
|
-
// of hx (the prune Morton order can't express — explicit per-axis [minY,maxY]/[minX,maxX])
|
|
450
|
-
for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
|
|
451
|
-
if (hy < blockBBox[g + 1] || hy > blockBBox[g + 3] || blockBBox[g] > hx || blockBBox[g + 2] <= qx) continue;
|
|
452
|
-
|
|
453
|
-
// ensure the walk's exclusive bound is live so we don't overrun into other blocks
|
|
454
|
-
const stop = liveBlockStop(b);
|
|
455
|
-
|
|
456
|
-
p = liveBlockHead(b);
|
|
457
|
-
do {
|
|
458
|
-
if (p.prev.next === p) { // skip nodes removed by filterPoints (stale in the index)
|
|
459
|
-
if (equals(hole, p.next)) return p.next;
|
|
460
|
-
else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
|
|
461
|
-
const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
|
|
462
|
-
if (x <= hx && x > qx) {
|
|
463
|
-
qx = x;
|
|
464
|
-
m = p.x < p.next.x ? p : p.next;
|
|
465
|
-
if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
p = p.next;
|
|
470
|
-
} while (p !== stop);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (!m) return null;
|
|
474
|
-
|
|
475
|
-
// look for points inside the triangle of hole point, segment intersection and endpoint;
|
|
476
|
-
// if there are no points found, we have a valid connection;
|
|
477
|
-
// otherwise choose the point of the minimum angle with the ray as connection point
|
|
478
|
-
|
|
479
|
-
const mx = m.x;
|
|
480
|
-
const my = m.y;
|
|
481
|
-
const tminY = Math.min(hy, my); // the triangle's y span; x span is [mx, hx]
|
|
482
|
-
const tmaxY = Math.max(hy, my);
|
|
483
|
-
let tanMin = Infinity;
|
|
484
|
-
|
|
485
|
-
// scan the same blocks; skip any whose bbox can't overlap the triangle's [mx,hx]×[tminY,tmaxY] box
|
|
486
|
-
for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
|
|
487
|
-
if (blockBBox[g + 2] < mx || blockBBox[g] > hx || blockBBox[g + 3] < tminY || blockBBox[g + 1] > tmaxY) continue;
|
|
488
|
-
|
|
489
|
-
const stop = liveBlockStop(b);
|
|
490
|
-
|
|
491
|
-
p = liveBlockHead(b);
|
|
492
|
-
do {
|
|
493
|
-
if (p.prev.next === p && hx >= p.x && p.x >= mx && hx !== p.x && // skip dead nodes
|
|
494
|
-
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
|
|
495
|
-
|
|
496
|
-
const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
|
|
497
|
-
|
|
498
|
-
// if hole point sits on p's horizontal edge (T-junction touch): the bridge runs
|
|
499
|
-
// along that edge — locallyInside rejects it as collinear, but it's valid
|
|
500
|
-
if ((locallyInside(p, hole) || (p.y === hy && p.next.y === hy && p.next.x > hx)) &&
|
|
501
|
-
(tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
|
|
502
|
-
m = p;
|
|
503
|
-
tanMin = tan;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
p = p.next;
|
|
508
|
-
} while (p !== stop);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return m;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// whether sector in vertex m contains sector in vertex p in the same coordinates
|
|
515
|
-
/** @param {Node} m @param {Node} p @returns {boolean} */
|
|
516
|
-
function sectorContainsSector(m, p) {
|
|
517
|
-
return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// scratch buffers reused across calls and grown on demand: two node-ref arrays that
|
|
521
|
-
// ping-pong during the radix passes, plus parallel z-value arrays so the passes read
|
|
522
|
-
// z from contiguous memory instead of dereferencing each node. 256-entry histogram for
|
|
523
|
-
// 8-bit digits; the small histogram keeps per-call setup cheap (most rings are short)
|
|
524
|
-
/** @type {Node[]} */
|
|
525
|
-
const sortArr = [];
|
|
526
|
-
/** @type {Node[]} */
|
|
527
|
-
let sortBuf = [];
|
|
528
|
-
let zArr = new Uint32Array(0);
|
|
529
|
-
let zBuf = new Uint32Array(0);
|
|
530
|
-
const counts = new Uint32Array(256);
|
|
531
|
-
|
|
532
|
-
// interlink polygon nodes in z-order: collect into an array, sort by z, relink
|
|
533
|
-
/** @param {Node} start @param {number} minX @param {number} minY @param {number} invSize */
|
|
534
|
-
function indexCurve(start, minX, minY, invSize) {
|
|
535
|
-
let p = start;
|
|
536
|
-
let n = 0;
|
|
537
|
-
do {
|
|
538
|
-
// always (re)compute: z may still hold a block index left over from eliminateHoles
|
|
539
|
-
p.z = zOrder(p.x, p.y, minX, minY, invSize);
|
|
540
|
-
sortArr[n++] = p;
|
|
541
|
-
p = p.next;
|
|
542
|
-
} while (p !== start);
|
|
543
|
-
|
|
544
|
-
sortNodes(n);
|
|
545
|
-
|
|
546
|
-
/** @type {Node | null} */
|
|
547
|
-
let prev = null;
|
|
548
|
-
for (let i = 0; i < n; i++) {
|
|
549
|
-
const node = sortArr[i];
|
|
550
|
-
node.prevZ = prev;
|
|
551
|
-
if (prev) prev.nextZ = node;
|
|
552
|
-
prev = node;
|
|
553
|
-
}
|
|
554
|
-
/** @type {Node} */ (prev).nextZ = null;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// sort the first n nodes of sortArr by z, in place: insertion sort for small n (cheaper
|
|
558
|
-
// than histogram setup), else LSD radix in four 8-bit passes (covering z's 30 bits)
|
|
559
|
-
/** @param {number} n */
|
|
560
|
-
function sortNodes(n) {
|
|
561
|
-
if (n <= 32) {
|
|
562
|
-
for (let i = 1; i < n; i++) {
|
|
563
|
-
const node = sortArr[i], z = node.z;
|
|
564
|
-
let j = i - 1;
|
|
565
|
-
while (j >= 0 && sortArr[j].z > z) { sortArr[j + 1] = sortArr[j]; j--; }
|
|
566
|
-
sortArr[j + 1] = node;
|
|
567
|
-
}
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (zArr.length < n) {
|
|
572
|
-
zArr = new Uint32Array(n);
|
|
573
|
-
zBuf = new Uint32Array(n);
|
|
574
|
-
sortBuf = new Array(n);
|
|
575
|
-
}
|
|
576
|
-
for (let i = 0; i < n; i++) zArr[i] = sortArr[i].z;
|
|
577
|
-
|
|
578
|
-
// even pass count lands the sorted result back in sortArr
|
|
579
|
-
radixPass(n, sortArr, zArr, sortBuf, zBuf, 0);
|
|
580
|
-
radixPass(n, sortBuf, zBuf, sortArr, zArr, 8);
|
|
581
|
-
radixPass(n, sortArr, zArr, sortBuf, zBuf, 16);
|
|
582
|
-
radixPass(n, sortBuf, zBuf, sortArr, zArr, 24);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// one LSD radix pass: stably scatter the first n nodes (and their z) from src to dst,
|
|
586
|
-
// bucketed by the 8-bit digit of z at the given bit shift
|
|
587
|
-
/** @param {number} n @param {Node[]} src @param {Uint32Array} srcZ @param {Node[]} dst @param {Uint32Array} dstZ @param {number} shift */
|
|
588
|
-
function radixPass(n, src, srcZ, dst, dstZ, shift) {
|
|
589
|
-
counts.fill(0);
|
|
590
|
-
for (let i = 0; i < n; i++) counts[(srcZ[i] >>> shift) & 0xff]++;
|
|
591
|
-
// turn per-bucket counts into start offsets (prefix sum)
|
|
592
|
-
let sum = 0;
|
|
593
|
-
for (let b = 0; b < 256; b++) { const c = counts[b]; counts[b] = sum; sum += c; }
|
|
594
|
-
for (let i = 0; i < n; i++) {
|
|
595
|
-
const z = srcZ[i];
|
|
596
|
-
const pos = counts[(z >>> shift) & 0xff]++;
|
|
597
|
-
dst[pos] = src[i];
|
|
598
|
-
dstZ[pos] = z;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// z-order of a point given coords and inverse of the longer side of data bbox
|
|
603
|
-
/** @param {number} x @param {number} y @param {number} minX @param {number} minY @param {number} invSize @returns {number} */
|
|
604
|
-
function zOrder(x, y, minX, minY, invSize) {
|
|
605
|
-
// coords are transformed into non-negative 15-bit integer range
|
|
606
|
-
x = (x - minX) * invSize | 0;
|
|
607
|
-
y = (y - minY) * invSize | 0;
|
|
608
|
-
|
|
609
|
-
x = (x | (x << 8)) & 0x00FF00FF;
|
|
610
|
-
x = (x | (x << 4)) & 0x0F0F0F0F;
|
|
611
|
-
x = (x | (x << 2)) & 0x33333333;
|
|
612
|
-
x = (x | (x << 1)) & 0x55555555;
|
|
613
|
-
|
|
614
|
-
y = (y | (y << 8)) & 0x00FF00FF;
|
|
615
|
-
y = (y | (y << 4)) & 0x0F0F0F0F;
|
|
616
|
-
y = (y | (y << 2)) & 0x33333333;
|
|
617
|
-
y = (y | (y << 1)) & 0x55555555;
|
|
618
|
-
|
|
619
|
-
return x | (y << 1);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// find the leftmost node of a polygon ring
|
|
623
|
-
/** @param {Node} start @returns {Node} */
|
|
624
|
-
function getLeftmost(start) {
|
|
625
|
-
let p = start,
|
|
626
|
-
leftmost = start;
|
|
627
|
-
do {
|
|
628
|
-
if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
|
|
629
|
-
p = p.next;
|
|
630
|
-
} while (p !== start);
|
|
631
|
-
|
|
632
|
-
return leftmost;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// check if a point lies within a convex triangle
|
|
636
|
-
/** @param {number} ax @param {number} ay @param {number} bx @param {number} by @param {number} cx @param {number} cy @param {number} px @param {number} py @returns {boolean} */
|
|
637
|
-
function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
|
|
638
|
-
return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
|
|
639
|
-
(ax - px) * (by - py) >= (bx - px) * (ay - py) &&
|
|
640
|
-
(bx - px) * (cy - py) >= (cx - px) * (by - py);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
|
|
644
|
-
/** @param {Node} a @param {Node} b @returns {boolean} true when the diagonal is valid */
|
|
645
|
-
function isValidDiagonal(a, b) {
|
|
646
|
-
const zeroLength = equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0; // degenerate case
|
|
647
|
-
return a.next.i !== b.i && (zeroLength || locallyInside(a, b) && locallyInside(b, a) && // // locally visible
|
|
648
|
-
(area(a.prev, a, b.prev) !== 0 || area(a, b.prev, b) !== 0)) && // no opposite-facing sectors
|
|
649
|
-
!intersectsPolygon(a, b) && (zeroLength || middleInside(a, b)); // doesn't intersect other edges, diagonal inside polygon
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// signed area of a triangle
|
|
653
|
-
/** @param {Node} p @param {Node} q @param {Node} r @returns {number} */
|
|
654
|
-
function area(p, q, r) {
|
|
655
|
-
return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// check if two points are equal
|
|
659
|
-
/** @param {Node} p1 @param {Node} p2 @returns {boolean} */
|
|
660
|
-
function equals(p1, p2) {
|
|
661
|
-
return p1.x === p2.x && p1.y === p2.y;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// check if two segments intersect; by default includes collinear boundary touches
|
|
665
|
-
/** @param {Node} p1 @param {Node} q1 @param {Node} p2 @param {Node} q2 @param {boolean} [includeBoundary] @returns {boolean} */
|
|
666
|
-
function intersects(p1, q1, p2, q2, includeBoundary = true) {
|
|
667
|
-
const o1 = area(p1, q1, p2);
|
|
668
|
-
const o2 = area(p1, q1, q2);
|
|
669
|
-
const o3 = area(p2, q2, p1);
|
|
670
|
-
const o4 = area(p2, q2, q1);
|
|
671
|
-
|
|
672
|
-
if (((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) && ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))) return true;
|
|
673
|
-
|
|
674
|
-
if (!includeBoundary) return false;
|
|
675
|
-
|
|
676
|
-
if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
|
|
677
|
-
if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
|
|
678
|
-
if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
|
|
679
|
-
if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
|
|
680
|
-
|
|
681
|
-
return false;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// for collinear points p, q, r, check if point q lies on segment pr
|
|
685
|
-
/** @param {Node} p @param {Node} q @param {Node} r @returns {boolean} */
|
|
686
|
-
function onSegment(p, q, r) {
|
|
687
|
-
return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// check if a polygon diagonal intersects any polygon segments
|
|
691
|
-
/** @param {Node} a @param {Node} b @returns {boolean} */
|
|
692
|
-
function intersectsPolygon(a, b) {
|
|
693
|
-
// diagonal bbox; an edge whose bbox can't overlap it can't intersect it, so
|
|
694
|
-
// skip the orientation test for those (the common case — the diagonal is short)
|
|
695
|
-
const minX = Math.min(a.x, b.x);
|
|
696
|
-
const maxX = Math.max(a.x, b.x);
|
|
697
|
-
const minY = Math.min(a.y, b.y);
|
|
698
|
-
const maxY = Math.max(a.y, b.y);
|
|
699
|
-
|
|
700
|
-
let p = a;
|
|
701
|
-
do {
|
|
702
|
-
const n = p.next;
|
|
703
|
-
if ((p.x > maxX && n.x > maxX) || (p.x < minX && n.x < minX) ||
|
|
704
|
-
(p.y > maxY && n.y > maxY) || (p.y < minY && n.y < minY)) {
|
|
705
|
-
p = n;
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
708
|
-
if (p.i !== a.i && n.i !== a.i && p.i !== b.i && n.i !== b.i &&
|
|
709
|
-
intersects(p, n, a, b)) return true;
|
|
710
|
-
p = n;
|
|
711
|
-
} while (p !== a);
|
|
712
|
-
|
|
713
|
-
return false;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// check if a polygon diagonal is locally inside the polygon
|
|
717
|
-
/** @param {Node} a @param {Node} b @returns {boolean} */
|
|
718
|
-
function locallyInside(a, b) {
|
|
719
|
-
return area(a.prev, a, a.next) < 0 ?
|
|
720
|
-
area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
|
|
721
|
-
area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// check if the middle point of a polygon diagonal is inside the polygon
|
|
725
|
-
/** @param {Node} a @param {Node} b @returns {boolean} */
|
|
726
|
-
function middleInside(a, b) {
|
|
727
|
-
let p = a;
|
|
728
|
-
let inside = false;
|
|
729
|
-
const px = (a.x + b.x) / 2;
|
|
730
|
-
const py = (a.y + b.y) / 2;
|
|
731
|
-
do {
|
|
732
|
-
const n = p.next;
|
|
733
|
-
if (((p.y > py) !== (n.y > py)) && (px < (n.x - p.x) * (py - p.y) / (n.y - p.y) + p.x))
|
|
734
|
-
inside = !inside;
|
|
735
|
-
p = n;
|
|
736
|
-
} while (p !== a);
|
|
737
|
-
|
|
738
|
-
return inside;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
|
|
742
|
-
// if one belongs to the outer ring and another to a hole, it merges it into a single ring
|
|
743
|
-
/** @param {Node} a @param {Node} b @returns {Node} */
|
|
744
|
-
function splitPolygon(a, b) {
|
|
745
|
-
const a2 = createNode(a.i, a.x, a.y),
|
|
746
|
-
b2 = createNode(b.i, b.x, b.y),
|
|
747
|
-
an = a.next,
|
|
748
|
-
bp = b.prev;
|
|
749
|
-
|
|
750
|
-
a.next = b;
|
|
751
|
-
b.prev = a;
|
|
752
|
-
|
|
753
|
-
a2.next = an;
|
|
754
|
-
an.prev = a2;
|
|
755
|
-
|
|
756
|
-
b2.next = a2;
|
|
757
|
-
a2.prev = b2;
|
|
758
|
-
|
|
759
|
-
bp.next = b2;
|
|
760
|
-
b2.prev = bp;
|
|
761
|
-
|
|
762
|
-
return b2;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// create a node and optionally link it with previous one (in a circular doubly linked list)
|
|
766
|
-
/** @param {number} i @param {number} x @param {number} y @param {Node | null} last @returns {Node} */
|
|
767
|
-
function insertNode(i, x, y, last) {
|
|
768
|
-
const p = createNode(i, x, y);
|
|
769
|
-
|
|
770
|
-
if (!last) {
|
|
771
|
-
p.prev = p;
|
|
772
|
-
p.next = p;
|
|
773
|
-
|
|
774
|
-
} else {
|
|
775
|
-
p.next = last.next;
|
|
776
|
-
p.prev = last;
|
|
777
|
-
last.next.prev = p;
|
|
778
|
-
last.next = p;
|
|
779
|
-
}
|
|
780
|
-
return p;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/** @param {Node} p */
|
|
784
|
-
function removeNode(p) {
|
|
785
|
-
p.next.prev = p.prev;
|
|
786
|
-
p.prev.next = p.next;
|
|
787
|
-
|
|
788
|
-
if (p.prevZ) p.prevZ.nextZ = p.nextZ;
|
|
789
|
-
if (p.nextZ) p.nextZ.prevZ = p.prevZ;
|
|
790
|
-
|
|
791
|
-
// keep the hole-bridge index's block bboxes covering the healed prev->next edge
|
|
792
|
-
if (indexActive) growBlock(p.prev, p.next);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/** @param {number} i @param {number} x @param {number} y @returns {Node} */
|
|
796
|
-
function createNode(i, x, y) {
|
|
797
|
-
// prev/next are assigned by the caller before any read, so the null init is cast away here
|
|
798
|
-
return /** @type {Node} */ (/** @type {unknown} */ ({
|
|
799
|
-
i, // vertex index in coordinates array
|
|
800
|
-
x, y, // vertex coordinates
|
|
801
|
-
prev: null, // previous and next vertex nodes in a polygon ring
|
|
802
|
-
next: null,
|
|
803
|
-
z: 0, // z-order curve value; doubles as owning block in the hole-bridge index during eliminateHoles
|
|
804
|
-
prevZ: null, // previous and next nodes in z-order
|
|
805
|
-
nextZ: null
|
|
806
|
-
}));
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @returns {number} */
|
|
810
|
-
function signedArea$1(data, start, end, dim) {
|
|
811
|
-
let sum = 0;
|
|
812
|
-
for (let i = start, j = end - dim; i < end; i += dim) {
|
|
813
|
-
sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
|
|
814
|
-
j = i;
|
|
815
|
-
}
|
|
816
|
-
return sum;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const geometryCache = new WeakMap();
|
|
820
|
-
const strokeGeometryCache = new WeakMap();
|
|
821
|
-
const MAX_FLATTENED_VERTICES = 262_144;
|
|
822
|
-
/** Applies ordered source-neutral modifiers after curve flattening. Empty trim ranges return an empty command stream. */
|
|
823
|
-
function applyVectorPathModifiers(commands, values, tolerance, modifiers, closeOpen) {
|
|
824
|
-
if (modifiers.length === 0)
|
|
825
|
-
return { commands, values: new Float32Array(values) };
|
|
826
|
-
let paths = flattenStrokePaths({
|
|
827
|
-
commands, values,
|
|
828
|
-
tolerance,
|
|
829
|
-
});
|
|
830
|
-
for (const modifier of modifiers) {
|
|
831
|
-
paths = modifier.kind === 'round-corners'
|
|
832
|
-
? paths.map(path => roundStrokeSubpath(path, modifier.radius, tolerance))
|
|
833
|
-
: trimStrokeSubpaths(paths, modifier.start, modifier.end, modifier.offset, modifier.mode);
|
|
834
|
-
}
|
|
835
|
-
let resolvedCommands = '';
|
|
836
|
-
const resolvedValues = [];
|
|
837
|
-
for (const path of paths) {
|
|
838
|
-
if (path.points.length < 2 || subpathLength(path) <= 1e-6)
|
|
839
|
-
continue;
|
|
840
|
-
resolvedCommands += `M${'L'.repeat(path.points.length - 1)}${path.closed || closeOpen ? 'Z' : ''}`;
|
|
841
|
-
for (const point of path.points)
|
|
842
|
-
resolvedValues.push(point.x, point.y);
|
|
843
|
-
}
|
|
844
|
-
return { commands: resolvedCommands, values: new Float32Array(resolvedValues) };
|
|
845
|
-
}
|
|
846
|
-
function roundStrokeSubpath(path, radius, tolerance) {
|
|
847
|
-
if (radius <= 1e-6 || path.points.length < 3)
|
|
848
|
-
return path;
|
|
849
|
-
const result = [];
|
|
850
|
-
const last = path.points.length - 1;
|
|
851
|
-
for (let index = 0; index <= last; index++) {
|
|
852
|
-
const current = path.points[index];
|
|
853
|
-
if (!path.closed && (index === 0 || index === last)) {
|
|
854
|
-
result.push(current);
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
857
|
-
const previous = path.points[(index - 1 + path.points.length) % path.points.length];
|
|
858
|
-
const next = path.points[(index + 1) % path.points.length];
|
|
859
|
-
const previousLength = distance(previous, current);
|
|
860
|
-
const nextLength = distance(current, next);
|
|
861
|
-
if (previousLength <= 1e-6 || nextLength <= 1e-6) {
|
|
862
|
-
result.push(current);
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
const incomingX = (current.x - previous.x) / previousLength;
|
|
866
|
-
const incomingY = (current.y - previous.y) / previousLength;
|
|
867
|
-
const outgoingX = (next.x - current.x) / nextLength;
|
|
868
|
-
const outgoingY = (next.y - current.y) / nextLength;
|
|
869
|
-
if (incomingX * outgoingX + incomingY * outgoingY > 0.995) {
|
|
870
|
-
result.push(current);
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
const inset = Math.min(radius, previousLength * 0.5, nextLength * 0.5);
|
|
874
|
-
const entry = interpolatePoint(current, previous, inset / previousLength);
|
|
875
|
-
const exit = interpolatePoint(current, next, inset / nextLength);
|
|
876
|
-
result.push(entry);
|
|
877
|
-
const steps = Math.max(2, Math.min(16, Math.ceil(Math.sqrt(inset / Math.max(tolerance, 0.05)))));
|
|
878
|
-
for (let step = 1; step <= steps; step++) {
|
|
879
|
-
const t = step / steps;
|
|
880
|
-
const inverse = 1 - t;
|
|
881
|
-
result.push({
|
|
882
|
-
x: inverse * inverse * entry.x + 2 * inverse * t * current.x + t * t * exit.x,
|
|
883
|
-
y: inverse * inverse * entry.y + 2 * inverse * t * current.y + t * t * exit.y,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
return { points: result, closed: path.closed };
|
|
888
|
-
}
|
|
889
|
-
function trimStrokeSubpaths(paths, start, end, offset, mode) {
|
|
890
|
-
const span = end - start;
|
|
891
|
-
if (Math.abs(span) >= 1 - 1e-6)
|
|
892
|
-
return paths.slice();
|
|
893
|
-
if (Math.abs(span) <= 1e-6)
|
|
894
|
-
return [];
|
|
895
|
-
const normalizedStart = wrapUnit(start + offset);
|
|
896
|
-
const normalizedEnd = wrapUnit(end + offset);
|
|
897
|
-
const intervals = normalizedEnd > normalizedStart
|
|
898
|
-
? [[normalizedStart, normalizedEnd]]
|
|
899
|
-
: [[normalizedStart, 1], [0, normalizedEnd]];
|
|
900
|
-
if (mode === 'individual')
|
|
901
|
-
return paths.flatMap(path => {
|
|
902
|
-
const length = subpathLength(path);
|
|
903
|
-
return intervals.flatMap(([from, to]) => extractSubpathRange(path, from * length, to * length));
|
|
904
|
-
});
|
|
905
|
-
const lengths = paths.map(subpathLength);
|
|
906
|
-
const total = lengths.reduce((sum, length) => sum + length, 0);
|
|
907
|
-
if (total <= 1e-6)
|
|
908
|
-
return [];
|
|
909
|
-
const result = [];
|
|
910
|
-
let pathStart = 0;
|
|
911
|
-
for (let index = 0; index < paths.length; index++) {
|
|
912
|
-
const length = lengths[index];
|
|
913
|
-
const pathEnd = pathStart + length;
|
|
914
|
-
for (const [from, to] of intervals) {
|
|
915
|
-
const globalStart = from * total;
|
|
916
|
-
const globalEnd = to * total;
|
|
917
|
-
const overlapStart = Math.max(pathStart, globalStart);
|
|
918
|
-
const overlapEnd = Math.min(pathEnd, globalEnd);
|
|
919
|
-
if (overlapEnd > overlapStart + 1e-6) {
|
|
920
|
-
result.push(...extractSubpathRange(paths[index], overlapStart - pathStart, overlapEnd - pathStart));
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
pathStart = pathEnd;
|
|
924
|
-
}
|
|
925
|
-
return result;
|
|
926
|
-
}
|
|
927
|
-
function extractSubpathRange(path, from, to) {
|
|
928
|
-
const length = subpathLength(path);
|
|
929
|
-
if (length <= 1e-6 || to <= from + 1e-6)
|
|
930
|
-
return [];
|
|
931
|
-
if (from <= 1e-6 && to >= length - 1e-6)
|
|
932
|
-
return [path];
|
|
933
|
-
const segmentCount = path.closed ? path.points.length : path.points.length - 1;
|
|
934
|
-
const result = [pointAtSubpathDistance(path, from)];
|
|
935
|
-
let cursor = 0;
|
|
936
|
-
for (let segment = 0; segment < segmentCount; segment++) {
|
|
937
|
-
const next = (segment + 1) % path.points.length;
|
|
938
|
-
cursor += distance(path.points[segment], path.points[next]);
|
|
939
|
-
if (cursor > from + 1e-6 && cursor < to - 1e-6)
|
|
940
|
-
result.push(path.points[next]);
|
|
941
|
-
}
|
|
942
|
-
result.push(pointAtSubpathDistance(path, to));
|
|
943
|
-
return [{ points: result, closed: false }];
|
|
944
|
-
}
|
|
945
|
-
function pointAtSubpathDistance(path, target) {
|
|
946
|
-
const segmentCount = path.closed ? path.points.length : path.points.length - 1;
|
|
947
|
-
let cursor = 0;
|
|
948
|
-
for (let segment = 0; segment < segmentCount; segment++) {
|
|
949
|
-
const next = (segment + 1) % path.points.length;
|
|
950
|
-
const segmentLength = distance(path.points[segment], path.points[next]);
|
|
951
|
-
if (target <= cursor + segmentLength || segment === segmentCount - 1) {
|
|
952
|
-
return interpolatePoint(path.points[segment], path.points[next], segmentLength <= 1e-8 ? 0 : (target - cursor) / segmentLength);
|
|
953
|
-
}
|
|
954
|
-
cursor += segmentLength;
|
|
955
|
-
}
|
|
956
|
-
return path.points.at(-1);
|
|
957
|
-
}
|
|
958
|
-
function subpathLength(path) {
|
|
959
|
-
const segmentCount = path.closed ? path.points.length : path.points.length - 1;
|
|
960
|
-
let result = 0;
|
|
961
|
-
for (let index = 0; index < segmentCount; index++) {
|
|
962
|
-
result += distance(path.points[index], path.points[(index + 1) % path.points.length]);
|
|
963
|
-
}
|
|
964
|
-
return result;
|
|
965
|
-
}
|
|
966
|
-
function distance(left, right) {
|
|
967
|
-
return Math.hypot(right.x - left.x, right.y - left.y);
|
|
968
|
-
}
|
|
969
|
-
function interpolatePoint(from, to, progress) {
|
|
970
|
-
const t = Math.min(1, Math.max(0, progress));
|
|
971
|
-
return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
|
|
972
|
-
}
|
|
973
|
-
function wrapUnit(value) {
|
|
974
|
-
return ((value % 1) + 1) % 1;
|
|
975
|
-
}
|
|
976
|
-
/** Flattens packed quadratic/cubic commands and triangulates contours with hole nesting. */
|
|
977
|
-
function tessellateAnimationPath(component) {
|
|
978
|
-
const cached = geometryCache.get(component);
|
|
979
|
-
if (cached)
|
|
980
|
-
return cached;
|
|
981
|
-
const contours = flattenPath(component);
|
|
982
|
-
if (contours.length === 0)
|
|
983
|
-
throw new RangeError('Animation path contains no drawable closed contour.');
|
|
984
|
-
classifyContours(contours, component.fillRule ?? 'nonzero');
|
|
985
|
-
const positions = [];
|
|
986
|
-
const indices = [];
|
|
987
|
-
for (let contourIndex = 0; contourIndex < contours.length; contourIndex++) {
|
|
988
|
-
const outer = contours[contourIndex];
|
|
989
|
-
if (outer.role !== 'outer')
|
|
990
|
-
continue;
|
|
991
|
-
const flat = [];
|
|
992
|
-
const holes = [];
|
|
993
|
-
appendContour(flat, outer.points);
|
|
994
|
-
for (let childIndex = 0; childIndex < contours.length; childIndex++) {
|
|
995
|
-
const hole = contours[childIndex];
|
|
996
|
-
if (hole.role !== 'hole' || nearestOuterAncestor(contours, childIndex) !== contourIndex)
|
|
997
|
-
continue;
|
|
998
|
-
holes.push(flat.length / 2);
|
|
999
|
-
appendContour(flat, hole.points);
|
|
1000
|
-
}
|
|
1001
|
-
const vertexOffset = positions.length / 2;
|
|
1002
|
-
positions.push(...flat);
|
|
1003
|
-
for (const index of earcut(flat, holes, 2))
|
|
1004
|
-
indices.push(vertexOffset + index);
|
|
1005
|
-
}
|
|
1006
|
-
if (indices.length === 0)
|
|
1007
|
-
throw new RangeError('Animation path tessellation produced no triangles.');
|
|
1008
|
-
const indexArray = positions.length / 2 > 0xffff ? new Uint32Array(indices) : new Uint16Array(indices);
|
|
1009
|
-
const geometry = new Geometry2D(new Float32Array(positions), indexArray);
|
|
1010
|
-
geometryCache.set(component, geometry);
|
|
1011
|
-
return geometry;
|
|
1012
|
-
}
|
|
1013
|
-
/** Tessellates the compact Lottie stroke extension without expanding it in the HYA payload. */
|
|
1014
|
-
function tessellateVectorStrokePath(component, allowEmpty = false) {
|
|
1015
|
-
const cached = strokeGeometryCache.get(component);
|
|
1016
|
-
if (cached)
|
|
1017
|
-
return cached;
|
|
1018
|
-
const flattened = flattenStrokePaths(component);
|
|
1019
|
-
const subpaths = component.dash?.length
|
|
1020
|
-
? flattened.flatMap(subpath => dashStrokeSubpath(subpath, component.dash, component.dashOffset ?? 0))
|
|
1021
|
-
: flattened;
|
|
1022
|
-
const contours = subpaths.flatMap(subpath => strokeContours(subpath, component));
|
|
1023
|
-
const positions = [];
|
|
1024
|
-
const indices = [];
|
|
1025
|
-
for (const contour of contours) {
|
|
1026
|
-
if (contour.length < 3)
|
|
1027
|
-
continue;
|
|
1028
|
-
const flat = contour.flatMap(point => [point.x, point.y]);
|
|
1029
|
-
const vertexOffset = positions.length / 2;
|
|
1030
|
-
positions.push(...flat);
|
|
1031
|
-
for (const index of earcut(flat, undefined, 2))
|
|
1032
|
-
indices.push(vertexOffset + index);
|
|
1033
|
-
}
|
|
1034
|
-
if (indices.length === 0 && !allowEmpty)
|
|
1035
|
-
throw new RangeError('Animation stroke tessellation produced no triangles.');
|
|
1036
|
-
const indexArray = indices.length === 0
|
|
1037
|
-
? new Uint16Array([0, 1, 2])
|
|
1038
|
-
: positions.length / 2 > 0xffff ? new Uint32Array(indices) : new Uint16Array(indices);
|
|
1039
|
-
const geometry = new Geometry2D(indices.length === 0 ? new Float32Array(6) : new Float32Array(positions), indexArray);
|
|
1040
|
-
strokeGeometryCache.set(component, geometry);
|
|
1041
|
-
return geometry;
|
|
1042
|
-
}
|
|
1043
|
-
function dashStrokeSubpath(subpath, sourcePattern, sourceOffset) {
|
|
1044
|
-
const positive = Array.from(sourcePattern, value => Math.max(0, value)).filter(value => value > 1e-6);
|
|
1045
|
-
if (positive.length === 0)
|
|
1046
|
-
return [subpath];
|
|
1047
|
-
const pattern = positive.length % 2 === 0 ? positive : [...positive, ...positive];
|
|
1048
|
-
const total = pattern.reduce((sum, value) => sum + value, 0);
|
|
1049
|
-
let offset = ((sourceOffset % total) + total) % total;
|
|
1050
|
-
let patternIndex = 0;
|
|
1051
|
-
while (offset >= pattern[patternIndex]) {
|
|
1052
|
-
offset -= pattern[patternIndex];
|
|
1053
|
-
patternIndex = (patternIndex + 1) % pattern.length;
|
|
1054
|
-
}
|
|
1055
|
-
let remaining = pattern[patternIndex] - offset;
|
|
1056
|
-
let drawing = patternIndex % 2 === 0;
|
|
1057
|
-
const result = [];
|
|
1058
|
-
let active = drawing ? [subpath.points[0]] : null;
|
|
1059
|
-
const segmentCount = subpath.closed ? subpath.points.length : subpath.points.length - 1;
|
|
1060
|
-
for (let segment = 0; segment < segmentCount; segment++) {
|
|
1061
|
-
const from = subpath.points[segment];
|
|
1062
|
-
const to = subpath.points[(segment + 1) % subpath.points.length];
|
|
1063
|
-
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
1064
|
-
if (length <= 1e-8)
|
|
1065
|
-
continue;
|
|
1066
|
-
let distance = 0;
|
|
1067
|
-
while (distance < length - 1e-8) {
|
|
1068
|
-
const advance = Math.min(remaining, length - distance);
|
|
1069
|
-
const endDistance = distance + advance;
|
|
1070
|
-
const point = {
|
|
1071
|
-
x: from.x + (to.x - from.x) * endDistance / length,
|
|
1072
|
-
y: from.y + (to.y - from.y) * endDistance / length,
|
|
1073
|
-
};
|
|
1074
|
-
if (drawing) {
|
|
1075
|
-
active ??= [{
|
|
1076
|
-
x: from.x + (to.x - from.x) * distance / length,
|
|
1077
|
-
y: from.y + (to.y - from.y) * distance / length,
|
|
1078
|
-
}];
|
|
1079
|
-
active.push(point);
|
|
1080
|
-
}
|
|
1081
|
-
distance = endDistance;
|
|
1082
|
-
remaining -= advance;
|
|
1083
|
-
if (remaining <= 1e-8) {
|
|
1084
|
-
if (drawing && active && active.length >= 2)
|
|
1085
|
-
result.push({ points: active, closed: false });
|
|
1086
|
-
patternIndex = (patternIndex + 1) % pattern.length;
|
|
1087
|
-
drawing = patternIndex % 2 === 0;
|
|
1088
|
-
remaining = pattern[patternIndex];
|
|
1089
|
-
active = drawing ? [point] : null;
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
if (drawing && active && active.length >= 2)
|
|
1094
|
-
result.push({ points: active, closed: false });
|
|
1095
|
-
return result;
|
|
1096
|
-
}
|
|
1097
|
-
/** Samples a topology-stable Lottie path morph into the ordinary path tessellator ABI. */
|
|
1098
|
-
function sampleVectorPathMorph(component, time) {
|
|
1099
|
-
const values = samplePackedPathMorph(component, time);
|
|
1100
|
-
return {
|
|
1101
|
-
type: 'path2d',
|
|
1102
|
-
commands: component.commands,
|
|
1103
|
-
values,
|
|
1104
|
-
fill: component.fill,
|
|
1105
|
-
fillRule: component.fillRule,
|
|
1106
|
-
...(component.tolerance === undefined ? {} : { tolerance: component.tolerance }),
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
function samplePackedPathMorph(component, time) {
|
|
1110
|
-
const frameCount = component.times.length;
|
|
1111
|
-
if (frameCount === 0 || component.valueSize <= 0)
|
|
1112
|
-
throw new RangeError('Lottie path morph contains no keyframes.');
|
|
1113
|
-
let frame = 0;
|
|
1114
|
-
while (frame + 1 < frameCount && component.times[frame + 1] <= time)
|
|
1115
|
-
frame++;
|
|
1116
|
-
const nextFrame = Math.min(frame + 1, frameCount - 1);
|
|
1117
|
-
let progress = 0;
|
|
1118
|
-
if (nextFrame !== frame && component.interpolation !== 'step') {
|
|
1119
|
-
const start = component.times[frame];
|
|
1120
|
-
const end = component.times[nextFrame];
|
|
1121
|
-
progress = Math.min(1, Math.max(0, (time - start) / Math.max(1e-8, end - start)));
|
|
1122
|
-
if (component.interpolation === 'cubic-bezier' && component.easings) {
|
|
1123
|
-
const offset = frame * 4;
|
|
1124
|
-
progress = cubicBezierYForX$2(progress, component.easings[offset] ?? 0.333, component.easings[offset + 1] ?? 0.333, component.easings[offset + 2] ?? 0.667, component.easings[offset + 3] ?? 0.667);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
const fromOffset = frame * component.valueSize;
|
|
1128
|
-
const toOffset = nextFrame * component.valueSize;
|
|
1129
|
-
const values = new Float32Array(component.valueSize);
|
|
1130
|
-
for (let offset = 0; offset < values.length; offset++) {
|
|
1131
|
-
const from = component.values[fromOffset + offset] ?? 0;
|
|
1132
|
-
const to = component.values[toOffset + offset] ?? from;
|
|
1133
|
-
values[offset] = from + (to - from) * progress;
|
|
1134
|
-
}
|
|
1135
|
-
return values;
|
|
1136
|
-
}
|
|
1137
|
-
function cubicBezierYForX$2(x, x1, y1, x2, y2) {
|
|
1138
|
-
let parameter = x;
|
|
1139
|
-
for (let iteration = 0; iteration < 5; iteration++) {
|
|
1140
|
-
const estimate = cubicBezierCoordinate(parameter, x1, x2) - x;
|
|
1141
|
-
const derivative = cubicBezierDerivative(parameter, x1, x2);
|
|
1142
|
-
if (Math.abs(derivative) < 1e-5)
|
|
1143
|
-
break;
|
|
1144
|
-
parameter = Math.min(1, Math.max(0, parameter - estimate / derivative));
|
|
1145
|
-
}
|
|
1146
|
-
return cubicBezierCoordinate(parameter, y1, y2);
|
|
1147
|
-
}
|
|
1148
|
-
function cubicBezierCoordinate(value, first, second) {
|
|
1149
|
-
const inverse = 1 - value;
|
|
1150
|
-
return 3 * inverse * inverse * value * first + 3 * inverse * value * value * second + value * value * value;
|
|
1151
|
-
}
|
|
1152
|
-
function cubicBezierDerivative(value, first, second) {
|
|
1153
|
-
const inverse = 1 - value;
|
|
1154
|
-
return 3 * inverse * inverse * first + 6 * inverse * value * (second - first) + 3 * value * value * (1 - second);
|
|
1155
|
-
}
|
|
1156
|
-
function flattenStrokePaths(component) {
|
|
1157
|
-
const subpaths = [];
|
|
1158
|
-
const values = component.values;
|
|
1159
|
-
if (typeof component.commands !== 'string' || values === undefined) {
|
|
1160
|
-
throw new TypeError('Resolved Lottie stroke extension requires commands and values.');
|
|
1161
|
-
}
|
|
1162
|
-
const toleranceSquared = (component.tolerance ?? 0.35) ** 2;
|
|
1163
|
-
let valueOffset = 0;
|
|
1164
|
-
let current = null;
|
|
1165
|
-
let cursor = { x: 0, y: 0 };
|
|
1166
|
-
let start = cursor;
|
|
1167
|
-
const budget = { count: 0 };
|
|
1168
|
-
const finish = (closed) => {
|
|
1169
|
-
if (!current)
|
|
1170
|
-
return;
|
|
1171
|
-
const compact = [];
|
|
1172
|
-
for (const point of current)
|
|
1173
|
-
if (!compact.length || !samePoint(compact.at(-1), point))
|
|
1174
|
-
compact.push(point);
|
|
1175
|
-
if (closed && compact.length > 1 && samePoint(compact[0], compact.at(-1)))
|
|
1176
|
-
compact.pop();
|
|
1177
|
-
if (compact.length >= 2)
|
|
1178
|
-
subpaths.push({ points: compact, closed });
|
|
1179
|
-
current = null;
|
|
1180
|
-
};
|
|
1181
|
-
for (const command of component.commands) {
|
|
1182
|
-
if (command === 'M') {
|
|
1183
|
-
finish(false);
|
|
1184
|
-
cursor = readPoint(values, valueOffset);
|
|
1185
|
-
valueOffset += 2;
|
|
1186
|
-
start = cursor;
|
|
1187
|
-
current = [];
|
|
1188
|
-
appendPoint(current, cursor, budget);
|
|
1189
|
-
}
|
|
1190
|
-
else if (command === 'L') {
|
|
1191
|
-
cursor = readPoint(values, valueOffset);
|
|
1192
|
-
valueOffset += 2;
|
|
1193
|
-
if (current)
|
|
1194
|
-
appendPoint(current, cursor, budget);
|
|
1195
|
-
}
|
|
1196
|
-
else if (command === 'Q') {
|
|
1197
|
-
const control = readPoint(values, valueOffset);
|
|
1198
|
-
const end = readPoint(values, valueOffset + 2);
|
|
1199
|
-
valueOffset += 4;
|
|
1200
|
-
if (current)
|
|
1201
|
-
flattenQuadratic(cursor, control, end, toleranceSquared, current, budget, 0);
|
|
1202
|
-
cursor = end;
|
|
1203
|
-
}
|
|
1204
|
-
else if (command === 'C') {
|
|
1205
|
-
const control1 = readPoint(values, valueOffset);
|
|
1206
|
-
const control2 = readPoint(values, valueOffset + 2);
|
|
1207
|
-
const end = readPoint(values, valueOffset + 4);
|
|
1208
|
-
valueOffset += 6;
|
|
1209
|
-
if (current)
|
|
1210
|
-
flattenCubic(cursor, control1, control2, end, toleranceSquared, current, budget, 0);
|
|
1211
|
-
cursor = end;
|
|
1212
|
-
}
|
|
1213
|
-
else if (command === 'Z') {
|
|
1214
|
-
cursor = start;
|
|
1215
|
-
finish(true);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
finish(false);
|
|
1219
|
-
return subpaths;
|
|
1220
|
-
}
|
|
1221
|
-
function strokeContours(subpath, style) {
|
|
1222
|
-
const points = subpath.points;
|
|
1223
|
-
const closed = subpath.closed;
|
|
1224
|
-
const halfWidth = style.width / 2;
|
|
1225
|
-
const contours = [];
|
|
1226
|
-
const segmentCount = closed ? points.length : points.length - 1;
|
|
1227
|
-
for (let segment = 0; segment < segmentCount; segment++) {
|
|
1228
|
-
const from = points[segment];
|
|
1229
|
-
const to = points[(segment + 1) % points.length];
|
|
1230
|
-
const direction = normalizePoint({ x: to.x - from.x, y: to.y - from.y });
|
|
1231
|
-
if (!direction)
|
|
1232
|
-
continue;
|
|
1233
|
-
const normal = { x: -direction.y, y: direction.x };
|
|
1234
|
-
const extendStart = !closed && style.lineCap === 'square' && segment === 0 ? halfWidth : 0;
|
|
1235
|
-
const extendEnd = !closed && style.lineCap === 'square' && segment === segmentCount - 1 ? halfWidth : 0;
|
|
1236
|
-
const start = { x: from.x - direction.x * extendStart, y: from.y - direction.y * extendStart };
|
|
1237
|
-
const end = { x: to.x + direction.x * extendEnd, y: to.y + direction.y * extendEnd };
|
|
1238
|
-
contours.push([
|
|
1239
|
-
{ x: start.x + normal.x * halfWidth, y: start.y + normal.y * halfWidth },
|
|
1240
|
-
{ x: end.x + normal.x * halfWidth, y: end.y + normal.y * halfWidth },
|
|
1241
|
-
{ x: end.x - normal.x * halfWidth, y: end.y - normal.y * halfWidth },
|
|
1242
|
-
{ x: start.x - normal.x * halfWidth, y: start.y - normal.y * halfWidth },
|
|
1243
|
-
]);
|
|
1244
|
-
}
|
|
1245
|
-
const joinStart = closed ? 0 : 1;
|
|
1246
|
-
const joinEnd = closed ? points.length : points.length - 1;
|
|
1247
|
-
for (let index = joinStart; index < joinEnd; index++) {
|
|
1248
|
-
const previous = points[(index - 1 + points.length) % points.length];
|
|
1249
|
-
const vertex = points[index];
|
|
1250
|
-
const next = points[(index + 1) % points.length];
|
|
1251
|
-
const incoming = normalizePoint({ x: vertex.x - previous.x, y: vertex.y - previous.y });
|
|
1252
|
-
const outgoing = normalizePoint({ x: next.x - vertex.x, y: next.y - vertex.y });
|
|
1253
|
-
if (!incoming || !outgoing)
|
|
1254
|
-
continue;
|
|
1255
|
-
if (style.lineJoin === 'round') {
|
|
1256
|
-
contours.push(strokeCircle(vertex, halfWidth));
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
const cross = incoming.x * outgoing.y - incoming.y * outgoing.x;
|
|
1260
|
-
if (Math.abs(cross) < 1e-6)
|
|
1261
|
-
continue;
|
|
1262
|
-
const side = cross > 0 ? 1 : -1;
|
|
1263
|
-
const incomingNormal = { x: -incoming.y * side, y: incoming.x * side };
|
|
1264
|
-
const outgoingNormal = { x: -outgoing.y * side, y: outgoing.x * side };
|
|
1265
|
-
const outerIncoming = { x: vertex.x + incomingNormal.x * halfWidth, y: vertex.y + incomingNormal.y * halfWidth };
|
|
1266
|
-
const outerOutgoing = { x: vertex.x + outgoingNormal.x * halfWidth, y: vertex.y + outgoingNormal.y * halfWidth };
|
|
1267
|
-
if (style.lineJoin === 'miter') {
|
|
1268
|
-
const bisector = normalizePoint({ x: incomingNormal.x + outgoingNormal.x, y: incomingNormal.y + outgoingNormal.y });
|
|
1269
|
-
const denominator = bisector ? bisector.x * outgoingNormal.x + bisector.y * outgoingNormal.y : 0;
|
|
1270
|
-
const miterLength = Math.abs(denominator) > 1e-6 ? halfWidth / denominator : Infinity;
|
|
1271
|
-
if (bisector && miterLength > 0 && miterLength <= halfWidth * style.miterLimit) {
|
|
1272
|
-
contours.push([outerIncoming, { x: vertex.x + bisector.x * miterLength, y: vertex.y + bisector.y * miterLength }, outerOutgoing]);
|
|
1273
|
-
continue;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
contours.push([outerIncoming, vertex, outerOutgoing]);
|
|
1277
|
-
}
|
|
1278
|
-
if (!closed && style.lineCap === 'round' && points.length > 1) {
|
|
1279
|
-
contours.push(strokeCircle(points[0], halfWidth), strokeCircle(points.at(-1), halfWidth));
|
|
1280
|
-
}
|
|
1281
|
-
return contours;
|
|
1282
|
-
}
|
|
1283
|
-
function strokeCircle(center, radius) {
|
|
1284
|
-
return Array.from({ length: 16 }, (_, index) => {
|
|
1285
|
-
const angle = index * Math.PI * 2 / 16;
|
|
1286
|
-
return { x: center.x + Math.cos(angle) * radius, y: center.y + Math.sin(angle) * radius };
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
function normalizePoint(point) {
|
|
1290
|
-
const length = Math.hypot(point.x, point.y);
|
|
1291
|
-
return length > 1e-8 ? { x: point.x / length, y: point.y / length } : null;
|
|
1292
|
-
}
|
|
1293
|
-
function flattenPath(component) {
|
|
1294
|
-
const contours = [];
|
|
1295
|
-
const values = component.values;
|
|
1296
|
-
const toleranceSquared = (component.tolerance ?? 0.35) ** 2;
|
|
1297
|
-
let valueOffset = 0;
|
|
1298
|
-
let current = null;
|
|
1299
|
-
let cursor = { x: 0, y: 0 };
|
|
1300
|
-
let start = cursor;
|
|
1301
|
-
const budget = { count: 0 };
|
|
1302
|
-
for (const command of component.commands) {
|
|
1303
|
-
if (command === 'M') {
|
|
1304
|
-
if (current && current.length >= 3)
|
|
1305
|
-
closeContour(contours, current);
|
|
1306
|
-
cursor = readPoint(values, valueOffset);
|
|
1307
|
-
valueOffset += 2;
|
|
1308
|
-
start = cursor;
|
|
1309
|
-
current = [];
|
|
1310
|
-
appendPoint(current, cursor, budget);
|
|
1311
|
-
}
|
|
1312
|
-
else if (command === 'L') {
|
|
1313
|
-
cursor = readPoint(values, valueOffset);
|
|
1314
|
-
valueOffset += 2;
|
|
1315
|
-
if (current)
|
|
1316
|
-
appendPoint(current, cursor, budget);
|
|
1317
|
-
}
|
|
1318
|
-
else if (command === 'Q') {
|
|
1319
|
-
const control = readPoint(values, valueOffset);
|
|
1320
|
-
const end = readPoint(values, valueOffset + 2);
|
|
1321
|
-
valueOffset += 4;
|
|
1322
|
-
if (current)
|
|
1323
|
-
flattenQuadratic(cursor, control, end, toleranceSquared, current, budget, 0);
|
|
1324
|
-
cursor = end;
|
|
1325
|
-
}
|
|
1326
|
-
else if (command === 'C') {
|
|
1327
|
-
const control1 = readPoint(values, valueOffset);
|
|
1328
|
-
const control2 = readPoint(values, valueOffset + 2);
|
|
1329
|
-
const end = readPoint(values, valueOffset + 4);
|
|
1330
|
-
valueOffset += 6;
|
|
1331
|
-
if (current)
|
|
1332
|
-
flattenCubic(cursor, control1, control2, end, toleranceSquared, current, budget, 0);
|
|
1333
|
-
cursor = end;
|
|
1334
|
-
}
|
|
1335
|
-
else if (command === 'Z' && current) {
|
|
1336
|
-
if (!samePoint(cursor, start))
|
|
1337
|
-
appendPoint(current, start, budget);
|
|
1338
|
-
closeContour(contours, current);
|
|
1339
|
-
current = null;
|
|
1340
|
-
cursor = start;
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
return contours;
|
|
1344
|
-
}
|
|
1345
|
-
function readPoint(values, offset) {
|
|
1346
|
-
return { x: values[offset] ?? 0, y: -(values[offset + 1] ?? 0) };
|
|
1347
|
-
}
|
|
1348
|
-
function flattenQuadratic(a, b, c, toleranceSquared, out, budget, depth) {
|
|
1349
|
-
if (depth >= 12 || pointLineDistanceSquared(b, a, c) <= toleranceSquared) {
|
|
1350
|
-
appendPoint(out, c, budget);
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
const ab = midpoint(a, b), bc = midpoint(b, c), middle = midpoint(ab, bc);
|
|
1354
|
-
flattenQuadratic(a, ab, middle, toleranceSquared, out, budget, depth + 1);
|
|
1355
|
-
flattenQuadratic(middle, bc, c, toleranceSquared, out, budget, depth + 1);
|
|
1356
|
-
}
|
|
1357
|
-
function flattenCubic(a, b, c, d, toleranceSquared, out, budget, depth) {
|
|
1358
|
-
if (depth >= 12 || Math.max(pointLineDistanceSquared(b, a, d), pointLineDistanceSquared(c, a, d)) <= toleranceSquared) {
|
|
1359
|
-
appendPoint(out, d, budget);
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
const ab = midpoint(a, b), bc = midpoint(b, c), cd = midpoint(c, d);
|
|
1363
|
-
const abc = midpoint(ab, bc), bcd = midpoint(bc, cd), middle = midpoint(abc, bcd);
|
|
1364
|
-
flattenCubic(a, ab, abc, middle, toleranceSquared, out, budget, depth + 1);
|
|
1365
|
-
flattenCubic(middle, bcd, cd, d, toleranceSquared, out, budget, depth + 1);
|
|
1366
|
-
}
|
|
1367
|
-
function appendPoint(out, point, budget) {
|
|
1368
|
-
if (++budget.count > MAX_FLATTENED_VERTICES) {
|
|
1369
|
-
throw new RangeError(`Animation path exceeds the ${MAX_FLATTENED_VERTICES} flattened-vertex runtime budget.`);
|
|
1370
|
-
}
|
|
1371
|
-
out.push(point);
|
|
1372
|
-
}
|
|
1373
|
-
function closeContour(out, points) {
|
|
1374
|
-
const compact = [];
|
|
1375
|
-
for (const point of points)
|
|
1376
|
-
if (!compact.length || !samePoint(compact[compact.length - 1], point))
|
|
1377
|
-
compact.push(point);
|
|
1378
|
-
if (compact.length > 1 && samePoint(compact[0], compact[compact.length - 1]))
|
|
1379
|
-
compact.pop();
|
|
1380
|
-
if (compact.length < 3)
|
|
1381
|
-
return;
|
|
1382
|
-
out.push({ points: compact, area: signedArea(compact), parent: -1, depth: 0, role: 'ignore' });
|
|
1383
|
-
}
|
|
1384
|
-
function classifyContours(contours, fillRule) {
|
|
1385
|
-
for (let i = 0; i < contours.length; i++) {
|
|
1386
|
-
const contour = contours[i];
|
|
1387
|
-
let parent = -1;
|
|
1388
|
-
let parentArea = Infinity;
|
|
1389
|
-
const point = contour.points[0];
|
|
1390
|
-
for (let candidateIndex = 0; candidateIndex < contours.length; candidateIndex++) {
|
|
1391
|
-
if (candidateIndex === i)
|
|
1392
|
-
continue;
|
|
1393
|
-
const candidate = contours[candidateIndex];
|
|
1394
|
-
const area = Math.abs(candidate.area);
|
|
1395
|
-
if (area <= Math.abs(contour.area) || area >= parentArea || !pointInPolygon(point, candidate.points))
|
|
1396
|
-
continue;
|
|
1397
|
-
parent = candidateIndex;
|
|
1398
|
-
parentArea = area;
|
|
1399
|
-
}
|
|
1400
|
-
contour.parent = parent;
|
|
1401
|
-
}
|
|
1402
|
-
for (const contour of contours) {
|
|
1403
|
-
let depth = 0;
|
|
1404
|
-
let parent = contour.parent;
|
|
1405
|
-
while (parent >= 0) {
|
|
1406
|
-
depth++;
|
|
1407
|
-
parent = contours[parent].parent;
|
|
1408
|
-
}
|
|
1409
|
-
contour.depth = depth;
|
|
1410
|
-
if (fillRule === 'evenodd') {
|
|
1411
|
-
contour.role = (depth & 1) === 0 ? 'outer' : 'hole';
|
|
1412
|
-
continue;
|
|
1413
|
-
}
|
|
1414
|
-
let windingBefore = 0;
|
|
1415
|
-
parent = contour.parent;
|
|
1416
|
-
while (parent >= 0) {
|
|
1417
|
-
windingBefore += Math.sign(contours[parent].area);
|
|
1418
|
-
parent = contours[parent].parent;
|
|
1419
|
-
}
|
|
1420
|
-
const windingAfter = windingBefore + Math.sign(contour.area);
|
|
1421
|
-
contour.role = windingBefore === 0 && windingAfter !== 0
|
|
1422
|
-
? 'outer'
|
|
1423
|
-
: windingBefore !== 0 && windingAfter === 0 ? 'hole' : 'ignore';
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
function nearestOuterAncestor(contours, contourIndex) {
|
|
1427
|
-
let parent = contours[contourIndex].parent;
|
|
1428
|
-
while (parent >= 0) {
|
|
1429
|
-
if (contours[parent].role === 'outer')
|
|
1430
|
-
return parent;
|
|
1431
|
-
parent = contours[parent].parent;
|
|
1432
|
-
}
|
|
1433
|
-
return -1;
|
|
1434
|
-
}
|
|
1435
|
-
function appendContour(out, points) {
|
|
1436
|
-
for (const point of points)
|
|
1437
|
-
out.push(point.x, point.y);
|
|
1438
|
-
}
|
|
1439
|
-
function signedArea(points) {
|
|
1440
|
-
let area = 0;
|
|
1441
|
-
for (let i = 0; i < points.length; i++) {
|
|
1442
|
-
const a = points[i], b = points[(i + 1) % points.length];
|
|
1443
|
-
area += a.x * b.y - b.x * a.y;
|
|
1444
|
-
}
|
|
1445
|
-
return area * 0.5;
|
|
1446
|
-
}
|
|
1447
|
-
function pointInPolygon(point, polygon) {
|
|
1448
|
-
let inside = false;
|
|
1449
|
-
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
1450
|
-
const a = polygon[i], b = polygon[j];
|
|
1451
|
-
if ((a.y > point.y) !== (b.y > point.y) && point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x)
|
|
1452
|
-
inside = !inside;
|
|
1453
|
-
}
|
|
1454
|
-
return inside;
|
|
1455
|
-
}
|
|
1456
|
-
function midpoint(a, b) { return { x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5 }; }
|
|
1457
|
-
function samePoint(a, b) { return Math.abs(a.x - b.x) < 1e-6 && Math.abs(a.y - b.y) < 1e-6; }
|
|
1458
|
-
function pointLineDistanceSquared(point, a, b) {
|
|
1459
|
-
const dx = b.x - a.x, dy = b.y - a.y;
|
|
1460
|
-
const lengthSquared = dx * dx + dy * dy;
|
|
1461
|
-
if (lengthSquared <= 1e-12)
|
|
1462
|
-
return (point.x - a.x) ** 2 + (point.y - a.y) ** 2;
|
|
1463
|
-
const t = ((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSquared;
|
|
1464
|
-
const x = a.x - point.x + dx * t, y = a.y - point.y + dy * t;
|
|
1465
|
-
return x * x + y * y;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
/** Runtime-private render item consumed by Animation2DRenderSystem. */
|
|
1469
|
-
class AnimationVisual2D extends Component {
|
|
1470
|
-
static UniqueCheckType = UniqueCheckType.SYMBOL | UniqueCheckType.REPLACE;
|
|
1471
|
-
static UniqueSymbol = Symbol.for('AnimationVisual2D');
|
|
1472
|
-
geometry;
|
|
1473
|
-
instanceId;
|
|
1474
|
-
nodeId;
|
|
1475
|
-
nodeKey;
|
|
1476
|
-
order;
|
|
1477
|
-
sourceOnly;
|
|
1478
|
-
compositeLayers;
|
|
1479
|
-
compositeKeys;
|
|
1480
|
-
/** Compatibility accessors for single-layer renderer diagnostics. */
|
|
1481
|
-
compositeSource;
|
|
1482
|
-
compositeKey;
|
|
1483
|
-
compositeMode;
|
|
1484
|
-
uvRect;
|
|
1485
|
-
color;
|
|
1486
|
-
texture;
|
|
1487
|
-
textureHandle;
|
|
1488
|
-
textMaterial;
|
|
1489
|
-
requiresTexture;
|
|
1490
|
-
gradient;
|
|
1491
|
-
effects;
|
|
1492
|
-
revision = 0;
|
|
1493
|
-
constructor(options) {
|
|
1494
|
-
super('AnimationVisual2D');
|
|
1495
|
-
this.geometry = options.geometry;
|
|
1496
|
-
this.instanceId = options.instanceId;
|
|
1497
|
-
this.nodeId = options.nodeId;
|
|
1498
|
-
this.nodeKey = `${options.instanceId}:${options.nodeId}`;
|
|
1499
|
-
this.order = options.order;
|
|
1500
|
-
this.sourceOnly = options.sourceOnly ?? false;
|
|
1501
|
-
this.compositeLayers = Object.freeze(options.composite
|
|
1502
|
-
? ('layers' in options.composite
|
|
1503
|
-
? options.composite.layers.map(layer => ({ ...layer }))
|
|
1504
|
-
: [{ ...options.composite }])
|
|
1505
|
-
: []);
|
|
1506
|
-
this.compositeKeys = Object.freeze(this.compositeLayers.map(layer => `${options.instanceId}:${layer.source}`));
|
|
1507
|
-
this.compositeSource = this.compositeLayers[0]?.source;
|
|
1508
|
-
this.compositeKey = this.compositeKeys[0];
|
|
1509
|
-
this.compositeMode = this.compositeLayers[0]?.mode;
|
|
1510
|
-
this.uvRect = [...(options.uvRect ?? [0, 0, 1, 1])];
|
|
1511
|
-
this.color = [...options.color];
|
|
1512
|
-
this.texture = options.texture ?? null;
|
|
1513
|
-
this.textureHandle = options.textureHandle ?? null;
|
|
1514
|
-
this.textMaterial = options.textMaterial ?? null;
|
|
1515
|
-
this.requiresTexture = options.requiresTexture ?? false;
|
|
1516
|
-
this.gradient = options.gradient ?? null;
|
|
1517
|
-
this.effects = Object.freeze(options.effects ? [...options.effects] : []);
|
|
1518
|
-
}
|
|
1519
|
-
setTexture(texture) {
|
|
1520
|
-
if (this.texture === texture)
|
|
1521
|
-
return;
|
|
1522
|
-
this.texture = texture;
|
|
1523
|
-
this.revision++;
|
|
1524
|
-
}
|
|
1525
|
-
setTextureHandle(handle) {
|
|
1526
|
-
if (this.textureHandle === handle)
|
|
1527
|
-
return;
|
|
1528
|
-
this.textureHandle = handle;
|
|
1529
|
-
this.revision++;
|
|
1530
|
-
}
|
|
1531
|
-
setUvRect(value) {
|
|
1532
|
-
if (this.uvRect.every((current, index) => current === value[index]))
|
|
1533
|
-
return;
|
|
1534
|
-
this.uvRect[0] = value[0];
|
|
1535
|
-
this.uvRect[1] = value[1];
|
|
1536
|
-
this.uvRect[2] = value[2];
|
|
1537
|
-
this.uvRect[3] = value[3];
|
|
1538
|
-
this.revision++;
|
|
1539
|
-
}
|
|
1540
|
-
setCompositeExpansion(index, value) {
|
|
1541
|
-
const layer = this.compositeLayers[index];
|
|
1542
|
-
if (!layer || layer.expansion === value)
|
|
1543
|
-
return;
|
|
1544
|
-
layer.expansion = value;
|
|
1545
|
-
this.revision++;
|
|
1546
|
-
}
|
|
1547
|
-
resolveTexture() {
|
|
1548
|
-
if (!this.textureHandle)
|
|
1549
|
-
return this.texture;
|
|
1550
|
-
try {
|
|
1551
|
-
return this.textureHandle.value;
|
|
1552
|
-
}
|
|
1553
|
-
catch {
|
|
1554
|
-
return null;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
clone() {
|
|
1558
|
-
return new AnimationVisual2D({
|
|
1559
|
-
geometry: this.geometry,
|
|
1560
|
-
color: this.color,
|
|
1561
|
-
instanceId: this.instanceId,
|
|
1562
|
-
nodeId: this.nodeId,
|
|
1563
|
-
order: this.order,
|
|
1564
|
-
sourceOnly: this.sourceOnly,
|
|
1565
|
-
...(this.compositeLayers.length === 0 ? {} : { composite: { layers: this.compositeLayers } }),
|
|
1566
|
-
texture: this.texture,
|
|
1567
|
-
textureHandle: this.textureHandle,
|
|
1568
|
-
textMaterial: this.textMaterial,
|
|
1569
|
-
requiresTexture: this.requiresTexture,
|
|
1570
|
-
uvRect: this.uvRect,
|
|
1571
|
-
gradient: this.gradient ? {
|
|
1572
|
-
kind: this.gradient.kind,
|
|
1573
|
-
start: [...this.gradient.start],
|
|
1574
|
-
end: [...this.gradient.end],
|
|
1575
|
-
stops: new Float32Array(this.gradient.stops),
|
|
1576
|
-
opacity: this.gradient.opacity,
|
|
1577
|
-
} : null,
|
|
1578
|
-
effects: this.effects.map(effect => ({ kind: effect.kind, values: new Float32Array(effect.values) })),
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
/** Runtime-private deterministic grapheme shaper and Canvas atlas uploader. */
|
|
1584
|
-
class AnimationTextRasterizer {
|
|
1585
|
-
component;
|
|
1586
|
-
texture = null;
|
|
1587
|
-
textureVersion = 0;
|
|
1588
|
-
canvas;
|
|
1589
|
-
time = Number.NaN;
|
|
1590
|
-
dirty = true;
|
|
1591
|
-
updating = false;
|
|
1592
|
-
updateToken = 0;
|
|
1593
|
-
animated;
|
|
1594
|
-
constructor(component) {
|
|
1595
|
-
this.component = component;
|
|
1596
|
-
this.canvas = typeof document === 'undefined' ? null : document.createElement('canvas');
|
|
1597
|
-
this.animated = (component.documents?.length ?? 0) > 1
|
|
1598
|
-
|| (component.animators ?? []).some(animator => hasAnimatedAnimator(animator));
|
|
1599
|
-
void this.updateTexture();
|
|
1600
|
-
}
|
|
1601
|
-
setTime(time) {
|
|
1602
|
-
if (!this.animated)
|
|
1603
|
-
return;
|
|
1604
|
-
if (this.time === time && !this.dirty)
|
|
1605
|
-
return;
|
|
1606
|
-
this.time = time;
|
|
1607
|
-
this.dirty = true;
|
|
1608
|
-
void this.updateTexture();
|
|
1609
|
-
}
|
|
1610
|
-
invalidateFont() {
|
|
1611
|
-
this.dirty = true;
|
|
1612
|
-
void this.updateTexture();
|
|
1613
|
-
}
|
|
1614
|
-
destroy() {
|
|
1615
|
-
this.updateToken++;
|
|
1616
|
-
closeTextTexture(this.texture, this.canvas);
|
|
1617
|
-
this.texture = null;
|
|
1618
|
-
}
|
|
1619
|
-
async updateTexture() {
|
|
1620
|
-
if (!this.dirty || this.updating || !this.canvas)
|
|
1621
|
-
return;
|
|
1622
|
-
const token = ++this.updateToken;
|
|
1623
|
-
this.dirty = false;
|
|
1624
|
-
this.updating = true;
|
|
1625
|
-
try {
|
|
1626
|
-
this.draw(this.canvas, Number.isFinite(this.time) ? this.time : 0);
|
|
1627
|
-
this.texture = this.canvas;
|
|
1628
|
-
this.textureVersion++;
|
|
1629
|
-
if (typeof createImageBitmap !== 'function')
|
|
1630
|
-
return;
|
|
1631
|
-
const bitmap = await createImageBitmap(this.canvas, { colorSpaceConversion: 'none' });
|
|
1632
|
-
if (token !== this.updateToken) {
|
|
1633
|
-
bitmap.close();
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
const retired = this.texture;
|
|
1637
|
-
this.texture = bitmap;
|
|
1638
|
-
this.textureVersion++;
|
|
1639
|
-
closeTextTexture(retired, this.canvas);
|
|
1640
|
-
}
|
|
1641
|
-
finally {
|
|
1642
|
-
this.updating = false;
|
|
1643
|
-
if (this.dirty)
|
|
1644
|
-
void this.updateTexture();
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
draw(canvas, time) {
|
|
1648
|
-
const component = this.component;
|
|
1649
|
-
const document = resolveDocument(component, time);
|
|
1650
|
-
const dpr = Math.max(1, Math.min(4, component.resolutionScale ?? 2));
|
|
1651
|
-
const width = Math.max(1, Math.ceil(component.size[0]));
|
|
1652
|
-
const height = Math.max(1, Math.ceil(component.size[1]));
|
|
1653
|
-
canvas.width = Math.max(1, Math.ceil(width * dpr));
|
|
1654
|
-
canvas.height = Math.max(1, Math.ceil(height * dpr));
|
|
1655
|
-
const context = canvas.getContext('2d');
|
|
1656
|
-
if (!context)
|
|
1657
|
-
return;
|
|
1658
|
-
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1659
|
-
context.clearRect(0, 0, width, height);
|
|
1660
|
-
const background = component.backgroundColor;
|
|
1661
|
-
if (background && background[3] > 0) {
|
|
1662
|
-
context.fillStyle = cssColor(background);
|
|
1663
|
-
context.fillRect(0, 0, width, height);
|
|
1664
|
-
}
|
|
1665
|
-
const padding = Math.max(0, component.padding ?? 0);
|
|
1666
|
-
const contentWidth = Math.max(1, width - padding * 2);
|
|
1667
|
-
const contentHeight = Math.max(1, height - padding * 2);
|
|
1668
|
-
context.font = `${document.fontStyle} ${document.fontWeight} ${document.fontSize}px ${quoteFont(document.fontFamily)}`;
|
|
1669
|
-
context.textBaseline = 'alphabetic';
|
|
1670
|
-
context.textAlign = 'left';
|
|
1671
|
-
const lines = document.text.split(/\r?\n/).map(splitGraphemes);
|
|
1672
|
-
const animators = component.animators ?? [];
|
|
1673
|
-
const selectorPlans = animators.map(animator => createSelectorIndexPlan(lines, animator));
|
|
1674
|
-
const lineHeight = Math.max(1, document.lineHeight);
|
|
1675
|
-
const textHeight = lines.length * lineHeight;
|
|
1676
|
-
let top = padding;
|
|
1677
|
-
const vertical = component.verticalAlign ?? 'middle';
|
|
1678
|
-
if (vertical === 'middle')
|
|
1679
|
-
top += Math.max(0, (contentHeight - textHeight) / 2);
|
|
1680
|
-
else if (vertical === 'bottom')
|
|
1681
|
-
top += Math.max(0, contentHeight - textHeight);
|
|
1682
|
-
let globalIndex = 0;
|
|
1683
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
1684
|
-
const line = lines[lineIndex];
|
|
1685
|
-
const advances = line.map(glyph => context.measureText(glyph).width);
|
|
1686
|
-
const baseWidth = advances.reduce((sum, advance) => sum + advance, 0) + Math.max(0, line.length - 1) * document.tracking;
|
|
1687
|
-
let x = padding;
|
|
1688
|
-
if (document.textAlign === 'center')
|
|
1689
|
-
x += Math.max(0, (contentWidth - baseWidth) / 2);
|
|
1690
|
-
else if (document.textAlign === 'right')
|
|
1691
|
-
x += Math.max(0, contentWidth - baseWidth);
|
|
1692
|
-
const baseline = top + lineIndex * lineHeight + document.fontSize;
|
|
1693
|
-
for (let glyphIndex = 0; glyphIndex < line.length; glyphIndex++, globalIndex++) {
|
|
1694
|
-
const glyph = line[glyphIndex];
|
|
1695
|
-
const advance = advances[glyphIndex] ?? 0;
|
|
1696
|
-
const state = resolveGlyphState(animators, selectorPlans, time, globalIndex, document.color);
|
|
1697
|
-
const centerX = x + advance / 2 + state.x;
|
|
1698
|
-
context.save();
|
|
1699
|
-
context.translate(centerX, baseline + state.y);
|
|
1700
|
-
context.rotate(state.rotation);
|
|
1701
|
-
context.scale(state.scaleX, state.scaleY);
|
|
1702
|
-
context.globalAlpha = state.opacity;
|
|
1703
|
-
context.fillStyle = cssColor(state.color);
|
|
1704
|
-
context.fillText(glyph, -advance / 2, 0);
|
|
1705
|
-
context.restore();
|
|
1706
|
-
x += advance + document.tracking + state.tracking;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
function resolveDocument(component, time) {
|
|
1712
|
-
let keyframe;
|
|
1713
|
-
for (const candidate of component.documents ?? []) {
|
|
1714
|
-
if (candidate.time > time)
|
|
1715
|
-
break;
|
|
1716
|
-
keyframe = candidate;
|
|
1717
|
-
}
|
|
1718
|
-
return {
|
|
1719
|
-
text: keyframe?.text ?? component.text,
|
|
1720
|
-
fontFamily: keyframe?.fontFamily ?? component.fontFamily ?? 'sans-serif',
|
|
1721
|
-
fontSize: keyframe?.fontSize ?? component.fontSize ?? 28,
|
|
1722
|
-
fontWeight: keyframe?.fontWeight ?? component.fontWeight ?? 400,
|
|
1723
|
-
fontStyle: keyframe?.fontStyle ?? component.fontStyle ?? 'normal',
|
|
1724
|
-
lineHeight: keyframe?.lineHeight ?? component.lineHeight ?? (keyframe?.fontSize ?? component.fontSize ?? 28) * 1.2,
|
|
1725
|
-
tracking: keyframe?.tracking ?? component.tracking ?? 0,
|
|
1726
|
-
textAlign: keyframe?.textAlign ?? component.textAlign ?? 'center',
|
|
1727
|
-
color: keyframe?.color ?? component.color,
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
function resolveGlyphState(animators, selectorPlans, time, index, baseColor) {
|
|
1731
|
-
const state = { x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0, opacity: 1, tracking: 0, color: [...baseColor] };
|
|
1732
|
-
for (let animatorIndex = 0; animatorIndex < animators.length; animatorIndex++) {
|
|
1733
|
-
const animator = animators[animatorIndex];
|
|
1734
|
-
const plan = selectorPlans[animatorIndex];
|
|
1735
|
-
const weight = selectorWeight(animator, time, plan.indices[index] ?? 0, plan.count);
|
|
1736
|
-
if (Math.abs(weight) < 1e-6)
|
|
1737
|
-
continue;
|
|
1738
|
-
const position = sampled(animator.positionTrack, time, animator.position ?? [0, 0]);
|
|
1739
|
-
const scale = sampled(animator.scaleTrack, time, animator.scale ?? [1, 1]);
|
|
1740
|
-
const rotation = sampled(animator.rotationTrack, time, [animator.rotation ?? 0])[0] ?? 0;
|
|
1741
|
-
const opacity = sampled(animator.opacityTrack, time, [animator.opacity ?? 1])[0] ?? 1;
|
|
1742
|
-
const tracking = sampled(animator.trackingTrack, time, [animator.tracking ?? 0])[0] ?? 0;
|
|
1743
|
-
const color = sampled(animator.fillColorTrack, time, animator.fillColor ?? state.color);
|
|
1744
|
-
state.x += (position[0] ?? 0) * weight;
|
|
1745
|
-
state.y += (position[1] ?? 0) * weight;
|
|
1746
|
-
state.scaleX *= mix$1(1, scale[0] ?? 1, weight);
|
|
1747
|
-
state.scaleY *= mix$1(1, scale[1] ?? 1, weight);
|
|
1748
|
-
state.rotation += rotation * weight;
|
|
1749
|
-
state.opacity *= mix$1(1, opacity, weight);
|
|
1750
|
-
state.tracking += tracking * weight;
|
|
1751
|
-
for (let channel = 0; channel < 4; channel++)
|
|
1752
|
-
state.color[channel] = mix$1(state.color[channel], color[channel] ?? state.color[channel], weight);
|
|
1753
|
-
}
|
|
1754
|
-
state.opacity = clamp$2(state.opacity, 0, 1);
|
|
1755
|
-
return state;
|
|
1756
|
-
}
|
|
1757
|
-
function selectorWeight(animator, time, index, count) {
|
|
1758
|
-
const selector = animator.selector;
|
|
1759
|
-
const units = selector.units ?? 'percent';
|
|
1760
|
-
const coordinate = units === 'index' ? index + 0.5 : (index + 0.5) / Math.max(1, count);
|
|
1761
|
-
const start = sampled(selector.startTrack, time, [selector.start])[0] ?? selector.start;
|
|
1762
|
-
const end = sampled(selector.endTrack, time, [selector.end])[0] ?? selector.end;
|
|
1763
|
-
const offset = sampled(selector.offsetTrack, time, [selector.offset ?? 0])[0] ?? selector.offset ?? 0;
|
|
1764
|
-
const amount = sampled(selector.amountTrack, time, [selector.amount ?? 1])[0] ?? selector.amount ?? 1;
|
|
1765
|
-
const low = Math.min(start + offset, end + offset);
|
|
1766
|
-
const high = Math.max(start + offset, end + offset);
|
|
1767
|
-
const span = Math.max(1e-6, high - low);
|
|
1768
|
-
const progress = clamp$2((coordinate - low) / span, 0, 1);
|
|
1769
|
-
let weight = coordinate >= low && coordinate <= high ? 1 : 0;
|
|
1770
|
-
switch (selector.shape ?? 'square') {
|
|
1771
|
-
case 'ramp-up':
|
|
1772
|
-
weight = progress;
|
|
1773
|
-
break;
|
|
1774
|
-
case 'ramp-down':
|
|
1775
|
-
weight = 1 - progress;
|
|
1776
|
-
break;
|
|
1777
|
-
case 'triangle':
|
|
1778
|
-
weight = 1 - Math.abs(progress * 2 - 1);
|
|
1779
|
-
break;
|
|
1780
|
-
case 'round':
|
|
1781
|
-
weight = Math.sqrt(Math.max(0, 1 - (progress * 2 - 1) ** 2));
|
|
1782
|
-
break;
|
|
1783
|
-
case 'smooth':
|
|
1784
|
-
weight = progress * progress * (3 - 2 * progress);
|
|
1785
|
-
break;
|
|
1786
|
-
}
|
|
1787
|
-
if (selector.easing)
|
|
1788
|
-
weight = cubicBezierYForX$1(weight, ...selector.easing);
|
|
1789
|
-
const smoothness = selector.smoothness ?? 1;
|
|
1790
|
-
if (smoothness < 1) {
|
|
1791
|
-
const width = Math.max(0.01, smoothness);
|
|
1792
|
-
const edge = 0.5 - width * 0.5;
|
|
1793
|
-
weight = clamp$2((weight - edge) / width, 0, 1);
|
|
1794
|
-
}
|
|
1795
|
-
return clamp$2(weight * amount, -1, 1);
|
|
1796
|
-
}
|
|
1797
|
-
function createSelectorIndexPlan(lines, animator) {
|
|
1798
|
-
const basedOn = animator.selector.basedOn ?? 'characters';
|
|
1799
|
-
const indices = [];
|
|
1800
|
-
let group = 0;
|
|
1801
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
1802
|
-
const line = lines[lineIndex];
|
|
1803
|
-
for (let glyphIndex = 0; glyphIndex < line.length; glyphIndex++) {
|
|
1804
|
-
const glyph = line[glyphIndex];
|
|
1805
|
-
indices.push(basedOn === 'lines' ? lineIndex : group);
|
|
1806
|
-
if (basedOn === 'characters')
|
|
1807
|
-
group++;
|
|
1808
|
-
else if (basedOn === 'characters-excluding-spaces' && glyph !== ' ')
|
|
1809
|
-
group++;
|
|
1810
|
-
else if (basedOn === 'words' && (glyph === ' ' || glyphIndex === line.length - 1))
|
|
1811
|
-
group++;
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
const count = Math.max(1, basedOn === 'lines' ? lines.length : group);
|
|
1815
|
-
const seed = animator.selector.randomSeed;
|
|
1816
|
-
if (seed === undefined || count < 2)
|
|
1817
|
-
return { indices, count };
|
|
1818
|
-
const permutation = Array.from({ length: count }, (_, index) => index);
|
|
1819
|
-
let randomState = seed >>> 0;
|
|
1820
|
-
for (let index = permutation.length - 1; index > 0; index--) {
|
|
1821
|
-
randomState = deterministicRandom(randomState);
|
|
1822
|
-
const swap = randomState % (index + 1);
|
|
1823
|
-
[permutation[index], permutation[swap]] = [permutation[swap], permutation[index]];
|
|
1824
|
-
}
|
|
1825
|
-
return { indices: indices.map(index => permutation[index] ?? index), count };
|
|
1826
|
-
}
|
|
1827
|
-
function deterministicRandom(state) {
|
|
1828
|
-
let value = state || 0x6d2b79f5;
|
|
1829
|
-
value ^= value << 13;
|
|
1830
|
-
value ^= value >>> 17;
|
|
1831
|
-
value ^= value << 5;
|
|
1832
|
-
return value >>> 0;
|
|
1833
|
-
}
|
|
1834
|
-
function sampled(track, time, fallback) {
|
|
1835
|
-
if (!track || track.times.length === 0)
|
|
1836
|
-
return fallback;
|
|
1837
|
-
let frame = 0;
|
|
1838
|
-
while (frame + 1 < track.times.length && track.times[frame + 1] <= time)
|
|
1839
|
-
frame++;
|
|
1840
|
-
const next = Math.min(frame + 1, track.times.length - 1);
|
|
1841
|
-
let progress = 0;
|
|
1842
|
-
if (next !== frame && track.interpolation !== 'step') {
|
|
1843
|
-
progress = clamp$2((time - track.times[frame]) / Math.max(1e-8, track.times[next] - track.times[frame]), 0, 1);
|
|
1844
|
-
if (track.interpolation === 'cubic-bezier' && track.easings) {
|
|
1845
|
-
const offset = frame * 4;
|
|
1846
|
-
progress = cubicBezierYForX$1(progress, track.easings[offset], track.easings[offset + 1], track.easings[offset + 2], track.easings[offset + 3]);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
const result = new Array(track.valueSize);
|
|
1850
|
-
for (let index = 0; index < track.valueSize; index++) {
|
|
1851
|
-
const from = track.values[frame * track.valueSize + index] ?? 0;
|
|
1852
|
-
result[index] = mix$1(from, track.values[next * track.valueSize + index] ?? from, progress);
|
|
1853
|
-
}
|
|
1854
|
-
return result;
|
|
1855
|
-
}
|
|
1856
|
-
function splitGraphemes(text) {
|
|
1857
|
-
const result = [];
|
|
1858
|
-
for (const codePoint of Array.from(text)) {
|
|
1859
|
-
if (codePoint === '\u200D' && result.length > 0) {
|
|
1860
|
-
result[result.length - 1] += codePoint;
|
|
1861
|
-
}
|
|
1862
|
-
else if (result.length > 0 && (/\p{Mark}/u.test(codePoint) || codePoint === '\uFE0F' || result[result.length - 1].endsWith('\u200D'))) {
|
|
1863
|
-
result[result.length - 1] += codePoint;
|
|
1864
|
-
}
|
|
1865
|
-
else
|
|
1866
|
-
result.push(codePoint);
|
|
1867
|
-
}
|
|
1868
|
-
return result;
|
|
1869
|
-
}
|
|
1870
|
-
function hasAnimatedAnimator(animator) {
|
|
1871
|
-
return animator.positionTrack !== undefined || animator.scaleTrack !== undefined || animator.rotationTrack !== undefined
|
|
1872
|
-
|| animator.opacityTrack !== undefined || animator.fillColorTrack !== undefined || animator.trackingTrack !== undefined
|
|
1873
|
-
|| animator.selector.startTrack !== undefined || animator.selector.endTrack !== undefined
|
|
1874
|
-
|| animator.selector.offsetTrack !== undefined || animator.selector.amountTrack !== undefined;
|
|
1875
|
-
}
|
|
1876
|
-
function cubicBezierYForX$1(x, x1, y1, x2, y2) {
|
|
1877
|
-
let low = 0;
|
|
1878
|
-
let high = 1;
|
|
1879
|
-
let t = x;
|
|
1880
|
-
for (let iteration = 0; iteration < 10; iteration++) {
|
|
1881
|
-
const estimate = cubic(t, 0, x1, x2, 1);
|
|
1882
|
-
if (estimate < x)
|
|
1883
|
-
low = t;
|
|
1884
|
-
else
|
|
1885
|
-
high = t;
|
|
1886
|
-
t = (low + high) * 0.5;
|
|
1887
|
-
}
|
|
1888
|
-
return cubic(t, 0, y1, y2, 1);
|
|
1889
|
-
}
|
|
1890
|
-
function cubic(t, a, b, c, d) {
|
|
1891
|
-
const inverse = 1 - t;
|
|
1892
|
-
return inverse ** 3 * a + 3 * inverse ** 2 * t * b + 3 * inverse * t ** 2 * c + t ** 3 * d;
|
|
1893
|
-
}
|
|
1894
|
-
function cssColor(color) {
|
|
1895
|
-
return `rgba(${Math.round(clamp$2(color[0] ?? 0, 0, 1) * 255)},${Math.round(clamp$2(color[1] ?? 0, 0, 1) * 255)},${Math.round(clamp$2(color[2] ?? 0, 0, 1) * 255)},${clamp$2(color[3] ?? 1, 0, 1)})`;
|
|
1896
|
-
}
|
|
1897
|
-
function quoteFont(value) { return `"${value.replace(/["\\]/g, '')}"`; }
|
|
1898
|
-
function closeTextTexture(texture, canvas) {
|
|
1899
|
-
if (texture && texture !== canvas && typeof texture.close === 'function')
|
|
1900
|
-
texture.close();
|
|
1901
|
-
}
|
|
1902
|
-
function mix$1(a, b, t) { return a + (b - a) * t; }
|
|
1903
|
-
function clamp$2(value, minimum, maximum) { return Math.max(minimum, Math.min(maximum, value)); }
|
|
1904
|
-
|
|
1905
|
-
/** HTML media-backed audio clip with composition-time seek and playback-rate synchronization. */
|
|
1906
|
-
class AnimationAudioClip {
|
|
1907
|
-
element;
|
|
1908
|
-
nodeStart;
|
|
1909
|
-
nodeEnd;
|
|
1910
|
-
startOffset;
|
|
1911
|
-
baseVolume;
|
|
1912
|
-
clipRate;
|
|
1913
|
-
loop;
|
|
1914
|
-
playPending = false;
|
|
1915
|
-
destroyed = false;
|
|
1916
|
-
stateMachineActive = false;
|
|
1917
|
-
playRequestEpoch = 0;
|
|
1918
|
-
constructor(uri, component, node, animationDuration) {
|
|
1919
|
-
this.nodeStart = node.start ?? 0;
|
|
1920
|
-
this.nodeEnd = this.nodeStart + (node.duration ?? animationDuration - this.nodeStart);
|
|
1921
|
-
this.startOffset = component.startOffset ?? 0;
|
|
1922
|
-
this.baseVolume = component.volume ?? 1;
|
|
1923
|
-
this.clipRate = component.playbackRate ?? 1;
|
|
1924
|
-
this.loop = component.loop ?? false;
|
|
1925
|
-
this.element = typeof Audio === 'undefined' ? null : new Audio(uri);
|
|
1926
|
-
if (this.element) {
|
|
1927
|
-
this.element.preload = 'auto';
|
|
1928
|
-
this.element.loop = this.loop;
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
sync(compositionTime, playing, speed, opacity) {
|
|
1932
|
-
const element = this.element;
|
|
1933
|
-
if (!element || this.destroyed)
|
|
1934
|
-
return;
|
|
1935
|
-
const active = compositionTime >= this.nodeStart && compositionTime <= this.nodeEnd;
|
|
1936
|
-
element.volume = clamp$1(this.baseVolume * opacity, 0, 1);
|
|
1937
|
-
element.playbackRate = clamp$1(this.clipRate * speed, 0.0625, 16);
|
|
1938
|
-
if (!active || !playing || speed <= 0) {
|
|
1939
|
-
this._cancelPendingPlay();
|
|
1940
|
-
return;
|
|
1941
|
-
}
|
|
1942
|
-
let target = this.startOffset + (compositionTime - this.nodeStart) * this.clipRate;
|
|
1943
|
-
if (this.loop && Number.isFinite(element.duration) && element.duration > 0)
|
|
1944
|
-
target %= element.duration;
|
|
1945
|
-
if (Number.isFinite(target) && Math.abs(element.currentTime - target) > 0.12) {
|
|
1946
|
-
try {
|
|
1947
|
-
element.currentTime = Math.max(0, target);
|
|
1948
|
-
}
|
|
1949
|
-
catch { /* Metadata may not be loaded yet. */ }
|
|
1950
|
-
}
|
|
1951
|
-
if (element.paused)
|
|
1952
|
-
this._requestPlay();
|
|
1953
|
-
}
|
|
1954
|
-
enterStateMachine(playing, speed, opacity, rejected) {
|
|
1955
|
-
const element = this.element;
|
|
1956
|
-
if (!element || this.destroyed)
|
|
1957
|
-
return;
|
|
1958
|
-
this.stateMachineActive = true;
|
|
1959
|
-
element.volume = clamp$1(this.baseVolume * opacity, 0, 1);
|
|
1960
|
-
element.playbackRate = clamp$1(this.clipRate * Math.max(speed, 0.0625), 0.0625, 16);
|
|
1961
|
-
try {
|
|
1962
|
-
element.currentTime = Math.max(0, this.startOffset);
|
|
1963
|
-
}
|
|
1964
|
-
catch { /* Metadata may not be loaded yet. */ }
|
|
1965
|
-
if (playing && speed > 0)
|
|
1966
|
-
this._requestPlay(rejected);
|
|
1967
|
-
else
|
|
1968
|
-
this._cancelPendingPlay();
|
|
1969
|
-
}
|
|
1970
|
-
restartStateMachine(playing, speed, opacity, rejected) {
|
|
1971
|
-
this.enterStateMachine(playing, speed, opacity, rejected);
|
|
1972
|
-
}
|
|
1973
|
-
setStateMachinePlaying(playing, speed, rejected) {
|
|
1974
|
-
const element = this.element;
|
|
1975
|
-
if (!element || this.destroyed || !this.stateMachineActive)
|
|
1976
|
-
return;
|
|
1977
|
-
element.playbackRate = clamp$1(this.clipRate * Math.max(speed, 0.0625), 0.0625, 16);
|
|
1978
|
-
if (playing && speed > 0)
|
|
1979
|
-
this._requestPlay(rejected);
|
|
1980
|
-
else
|
|
1981
|
-
this._cancelPendingPlay();
|
|
1982
|
-
}
|
|
1983
|
-
updateStateMachineProperties(speed, opacity) {
|
|
1984
|
-
const element = this.element;
|
|
1985
|
-
if (!element || this.destroyed || !this.stateMachineActive)
|
|
1986
|
-
return;
|
|
1987
|
-
element.volume = clamp$1(this.baseVolume * opacity, 0, 1);
|
|
1988
|
-
element.playbackRate = clamp$1(this.clipRate * Math.max(speed, 0.0625), 0.0625, 16);
|
|
1989
|
-
}
|
|
1990
|
-
exitStateMachine() {
|
|
1991
|
-
this.stateMachineActive = false;
|
|
1992
|
-
this._cancelPendingPlay();
|
|
1993
|
-
}
|
|
1994
|
-
pause() { this._cancelPendingPlay(); }
|
|
1995
|
-
destroy() {
|
|
1996
|
-
if (this.destroyed)
|
|
1997
|
-
return;
|
|
1998
|
-
this.destroyed = true;
|
|
1999
|
-
this.stateMachineActive = false;
|
|
2000
|
-
const element = this.element;
|
|
2001
|
-
if (!element)
|
|
2002
|
-
return;
|
|
2003
|
-
this._cancelPendingPlay();
|
|
2004
|
-
element.removeAttribute('src');
|
|
2005
|
-
element.load();
|
|
2006
|
-
}
|
|
2007
|
-
_requestPlay(rejected) {
|
|
2008
|
-
const element = this.element;
|
|
2009
|
-
if (!element || this.playPending || this.destroyed)
|
|
2010
|
-
return;
|
|
2011
|
-
const epoch = ++this.playRequestEpoch;
|
|
2012
|
-
const stateMachineRequest = rejected !== undefined;
|
|
2013
|
-
this.playPending = true;
|
|
2014
|
-
void element.play()
|
|
2015
|
-
.then(() => {
|
|
2016
|
-
if (epoch !== this.playRequestEpoch
|
|
2017
|
-
|| this.destroyed
|
|
2018
|
-
|| (stateMachineRequest && !this.stateMachineActive))
|
|
2019
|
-
element.pause();
|
|
2020
|
-
})
|
|
2021
|
-
.catch(reason => {
|
|
2022
|
-
if (epoch === this.playRequestEpoch)
|
|
2023
|
-
rejected?.(reason);
|
|
2024
|
-
})
|
|
2025
|
-
.finally(() => {
|
|
2026
|
-
if (epoch === this.playRequestEpoch)
|
|
2027
|
-
this.playPending = false;
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
_cancelPendingPlay() {
|
|
2031
|
-
this.playRequestEpoch++;
|
|
2032
|
-
this.playPending = false;
|
|
2033
|
-
this.element?.pause();
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
function clamp$1(value, minimum, maximum) {
|
|
2037
|
-
return Math.min(maximum, Math.max(minimum, value));
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
/** Single runtime mapping from screen-y-down HYA data to the engine's y-up emitter. */
|
|
2041
|
-
function particle2DDescriptorToEmitterOptions(component) {
|
|
2042
|
-
return {
|
|
2043
|
-
maxParticles: component.maxParticles,
|
|
2044
|
-
emissionRate: component.emissionRate,
|
|
2045
|
-
burst: component.burst ?? 0,
|
|
2046
|
-
duration: component.duration ?? Number.POSITIVE_INFINITY,
|
|
2047
|
-
loop: component.loop ?? true,
|
|
2048
|
-
seed: component.seed ?? 1,
|
|
2049
|
-
lifetime: component.lifetime,
|
|
2050
|
-
speed: component.speed,
|
|
2051
|
-
angle: [-component.angle[1], -component.angle[0]],
|
|
2052
|
-
gravity: [component.gravity?.[0] ?? 0, -(component.gravity?.[1] ?? 20)],
|
|
2053
|
-
startSize: component.startSize,
|
|
2054
|
-
endSize: component.endSize,
|
|
2055
|
-
startColor: component.startColor,
|
|
2056
|
-
endColor: component.endColor,
|
|
2057
|
-
shape: component.shape ?? 'point',
|
|
2058
|
-
shapeSize: component.shapeSize ?? [0, 0],
|
|
2059
|
-
shapeRadius: component.shapeRadius ?? 0,
|
|
2060
|
-
blendMode: component.blendMode ?? 'normal',
|
|
2061
|
-
radial: component.radial ?? true,
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
function createParticle2DEmitter(component) {
|
|
2065
|
-
return new ParticleEmitter2D(particle2DDescriptorToEmitterOptions(component));
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
let nextRuntimeInstanceId = 1;
|
|
2069
|
-
class Animation2DRuntime {
|
|
2070
|
-
_animation;
|
|
2071
|
-
_assetManager;
|
|
2072
|
-
_root;
|
|
2073
|
-
_nodes = [];
|
|
2074
|
-
_nodesById = new Map();
|
|
2075
|
-
_tracksByNode = new Map();
|
|
2076
|
-
_trackCursors;
|
|
2077
|
-
_trackIndices = new Map();
|
|
2078
|
-
_opacityMemo = new Map();
|
|
2079
|
-
_abortController = new AbortController();
|
|
2080
|
-
_assetHandles = [];
|
|
2081
|
-
_fontLoads = new Map();
|
|
2082
|
-
_fontFaces = [];
|
|
2083
|
-
_instanceId = nextRuntimeInstanceId++;
|
|
2084
|
-
_visualCount = 0;
|
|
2085
|
-
_unsupportedComponentCount = 0;
|
|
2086
|
-
_pendingResourceCount = 0;
|
|
2087
|
-
_failedResourceCount = 0;
|
|
2088
|
-
_textCount = 0;
|
|
2089
|
-
_particleCount = 0;
|
|
2090
|
-
_audioCount = 0;
|
|
2091
|
-
_destroyed = false;
|
|
2092
|
-
_playing = false;
|
|
2093
|
-
_speed = 1;
|
|
2094
|
-
_lastAppliedTime = Number.NaN;
|
|
2095
|
-
get stats() {
|
|
2096
|
-
return Object.freeze({
|
|
2097
|
-
nodeCount: this._nodes.length,
|
|
2098
|
-
visualCount: this._visualCount,
|
|
2099
|
-
unsupportedComponentCount: this._unsupportedComponentCount,
|
|
2100
|
-
pendingResourceCount: this._pendingResourceCount,
|
|
2101
|
-
failedResourceCount: this._failedResourceCount,
|
|
2102
|
-
textCount: this._textCount,
|
|
2103
|
-
particleCount: this._particleCount,
|
|
2104
|
-
audioCount: this._audioCount,
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
constructor(owner, _animation, runtimeExtensions, _assetManager) {
|
|
2108
|
-
this._animation = _animation;
|
|
2109
|
-
this._assetManager = _assetManager;
|
|
2110
|
-
for (const id of _animation.extensionsRequired) {
|
|
2111
|
-
if (id === HYA_STATE_MACHINE_EXTENSION_ID)
|
|
2112
|
-
continue;
|
|
2113
|
-
if (!runtimeExtensions?.has(id)) {
|
|
2114
|
-
throw new AnimationFormatError('E_ANIMATION_MISSING_EXTENSION', `Required animation runtime extension "${id}" is not registered.`, '$.extensionsRequired');
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
this._root = new Entity(`${_animation.name ?? 'Animation'} runtime`);
|
|
2118
|
-
this._root.addComponent(new Transform2D({ x: -_animation.canvas.width / 2, y: _animation.canvas.height / 2 }));
|
|
2119
|
-
owner.addChild(this._root);
|
|
2120
|
-
const compositeSources = new Set(_animation.nodes.flatMap(node => compositeLayers(node.composite).map(layer => layer.source)));
|
|
2121
|
-
const sourceGroupByNode = resolveSourceGroups(_animation.nodes, compositeSources);
|
|
2122
|
-
const compositeByNode = resolveInheritedComposites(_animation.nodes, sourceGroupByNode);
|
|
2123
|
-
const effectsByNode = resolveInheritedEffects(_animation.nodes, sourceGroupByNode);
|
|
2124
|
-
const sourceNodesById = new Map(_animation.nodes.map(node => [node.id, node]));
|
|
2125
|
-
let renderOrder = 0;
|
|
2126
|
-
for (const node of _animation.nodes) {
|
|
2127
|
-
const transformSource = node.transform;
|
|
2128
|
-
const position = transformSource?.position ?? [0, 0];
|
|
2129
|
-
const scale = transformSource?.scale ?? [1, 1];
|
|
2130
|
-
const transform = new Transform2D({
|
|
2131
|
-
x: position[0],
|
|
2132
|
-
y: -position[1],
|
|
2133
|
-
rotation: -(transformSource?.rotation ?? 0),
|
|
2134
|
-
scaleX: scale[0],
|
|
2135
|
-
scaleY: scale[1],
|
|
2136
|
-
});
|
|
2137
|
-
const entity = new Entity(node.name ?? node.id).addComponent(transform);
|
|
2138
|
-
const anchor = transformSource?.anchor ?? [0, 0];
|
|
2139
|
-
const content = new Entity(`${node.name ?? node.id} anchor`).addComponent(new Transform2D({
|
|
2140
|
-
x: -anchor[0],
|
|
2141
|
-
y: anchor[1],
|
|
2142
|
-
}));
|
|
2143
|
-
entity.addChild(content);
|
|
2144
|
-
const runtimeNode = {
|
|
2145
|
-
source: node,
|
|
2146
|
-
entity,
|
|
2147
|
-
content,
|
|
2148
|
-
transform,
|
|
2149
|
-
visuals: [],
|
|
2150
|
-
deferredVisuals: [],
|
|
2151
|
-
extensions: [],
|
|
2152
|
-
particles: [],
|
|
2153
|
-
audio: [],
|
|
2154
|
-
initialX: transform.x,
|
|
2155
|
-
initialY: transform.y,
|
|
2156
|
-
initialRotation: transform.rotation,
|
|
2157
|
-
initialScaleX: transform.scaleX,
|
|
2158
|
-
initialScaleY: transform.scaleY,
|
|
2159
|
-
initialOpacity: transformSource?.opacity ?? 1,
|
|
2160
|
-
opacity: transformSource?.opacity ?? 1,
|
|
2161
|
-
};
|
|
2162
|
-
const sourceComponents = node.components ?? [];
|
|
2163
|
-
for (let componentIndex = 0; componentIndex < sourceComponents.length; componentIndex++) {
|
|
2164
|
-
const component = sourceComponents[componentIndex];
|
|
2165
|
-
if (isParticle2DComponent(component)) {
|
|
2166
|
-
// Particle2D is rendered by the engine's independent pass. Do not silently
|
|
2167
|
-
// bypass animation alpha-composite semantics until both renderers share a graph item protocol.
|
|
2168
|
-
if (sourceGroupByNode.has(node.id) || compositeByNode.has(node.id)) {
|
|
2169
|
-
this._unsupportedComponentCount++;
|
|
2170
|
-
continue;
|
|
2171
|
-
}
|
|
2172
|
-
const emitter = createParticle2DEmitter(component);
|
|
2173
|
-
content.addChild(new Entity(`${entity.name} particles`).addComponent(emitter));
|
|
2174
|
-
runtimeNode.particles.push(emitter);
|
|
2175
|
-
this._visualCount++;
|
|
2176
|
-
this._particleCount++;
|
|
2177
|
-
if (component.resource)
|
|
2178
|
-
this._loadTexture(component.resource, handle => { emitter.textureSource = handle; });
|
|
2179
|
-
continue;
|
|
2180
|
-
}
|
|
2181
|
-
if (isAudioComponent(component)) {
|
|
2182
|
-
const resource = this._animation.resources.find(candidate => candidate.id === component.resource);
|
|
2183
|
-
if (!resource || resource.type !== 'audio') {
|
|
2184
|
-
this._unsupportedComponentCount++;
|
|
2185
|
-
continue;
|
|
2186
|
-
}
|
|
2187
|
-
runtimeNode.audio.push(new AnimationAudioClip(resource.uri, component, node, this._animation.duration));
|
|
2188
|
-
this._audioCount++;
|
|
2189
|
-
continue;
|
|
2190
|
-
}
|
|
2191
|
-
if (!isCoreVisualComponent(component)) {
|
|
2192
|
-
const handler = runtimeExtensions?.get(component.type);
|
|
2193
|
-
if (!handler) {
|
|
2194
|
-
this._unsupportedComponentCount++;
|
|
2195
|
-
continue;
|
|
2196
|
-
}
|
|
2197
|
-
try {
|
|
2198
|
-
const instance = handler.create({ animation: _animation, node, component, parent: content });
|
|
2199
|
-
if (instance) {
|
|
2200
|
-
runtimeNode.extensions.push(instance);
|
|
2201
|
-
this._visualCount++;
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
catch (error) {
|
|
2205
|
-
this._abortController.abort();
|
|
2206
|
-
for (const handle of this._assetHandles)
|
|
2207
|
-
handle.release();
|
|
2208
|
-
this._assetHandles.length = 0;
|
|
2209
|
-
for (const createdNode of [...this._nodes, runtimeNode]) {
|
|
2210
|
-
for (const audio of createdNode.audio)
|
|
2211
|
-
audio.destroy();
|
|
2212
|
-
for (const instance of createdNode.extensions)
|
|
2213
|
-
instance.destroy?.();
|
|
2214
|
-
}
|
|
2215
|
-
this._root.destroy();
|
|
2216
|
-
throw error;
|
|
2217
|
-
}
|
|
2218
|
-
continue;
|
|
2219
|
-
}
|
|
2220
|
-
const deferred = {
|
|
2221
|
-
component,
|
|
2222
|
-
componentIndex,
|
|
2223
|
-
order: renderOrder++,
|
|
2224
|
-
sourceGroup: sourceGroupByNode.get(node.id),
|
|
2225
|
-
composite: compositeByNode.get(node.id),
|
|
2226
|
-
effects: effectsByNode.get(node.id),
|
|
2227
|
-
};
|
|
2228
|
-
this._visualCount++;
|
|
2229
|
-
if (component.type === 'text2d')
|
|
2230
|
-
this._textCount++;
|
|
2231
|
-
if (isSourceNodeActiveAt(node, 0, sourceNodesById, _animation.duration)) {
|
|
2232
|
-
this._materializeCoreVisual(runtimeNode, deferred);
|
|
2233
|
-
}
|
|
2234
|
-
else {
|
|
2235
|
-
runtimeNode.deferredVisuals.push(deferred);
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
this._nodes.push(runtimeNode);
|
|
2239
|
-
this._nodesById.set(node.id, runtimeNode);
|
|
2240
|
-
}
|
|
2241
|
-
for (const node of this._nodes) {
|
|
2242
|
-
const parent = node.source.parent ? this._nodesById.get(node.source.parent)?.content : this._root;
|
|
2243
|
-
(parent ?? this._root).addChild(node.entity);
|
|
2244
|
-
}
|
|
2245
|
-
for (let index = 0; index < _animation.tracks.length; index++) {
|
|
2246
|
-
const track = _animation.tracks[index];
|
|
2247
|
-
let tracks = this._tracksByNode.get(track.node);
|
|
2248
|
-
if (!tracks)
|
|
2249
|
-
this._tracksByNode.set(track.node, tracks = []);
|
|
2250
|
-
tracks.push(track);
|
|
2251
|
-
this._trackIndices.set(track, index);
|
|
2252
|
-
}
|
|
2253
|
-
this._trackCursors = new Int32Array(_animation.tracks.length);
|
|
2254
|
-
}
|
|
2255
|
-
apply(time, playing = this._playing, speed = this._speed, forceParticleSeek = false) {
|
|
2256
|
-
const discontinuity = forceParticleSeek || (!Number.isFinite(this._lastAppliedTime)
|
|
2257
|
-
? time > 1e-6
|
|
2258
|
-
: time + 1e-6 < this._lastAppliedTime || Math.abs(time - this._lastAppliedTime) > 0.25);
|
|
2259
|
-
this._playing = playing;
|
|
2260
|
-
this._speed = speed;
|
|
2261
|
-
this._opacityMemo.clear();
|
|
2262
|
-
for (const node of this._nodes) {
|
|
2263
|
-
const start = node.source.start ?? 0;
|
|
2264
|
-
const end = start + (node.source.duration ?? this._animation.duration);
|
|
2265
|
-
node.entity.disabled = time < start || time > end;
|
|
2266
|
-
node.transform.setPosition(node.initialX, node.initialY);
|
|
2267
|
-
node.transform.rotation = node.initialRotation;
|
|
2268
|
-
node.transform.setScale(node.initialScaleX, node.initialScaleY);
|
|
2269
|
-
node.opacity = node.initialOpacity;
|
|
2270
|
-
for (const track of this._tracksByNode.get(node.source.id) ?? [])
|
|
2271
|
-
this._applyTrack(node, track, time);
|
|
2272
|
-
}
|
|
2273
|
-
for (const node of this._nodes) {
|
|
2274
|
-
const opacity = this._resolveOpacity(node);
|
|
2275
|
-
if (node.deferredVisuals.length > 0 && isRuntimeNodeActive(node, this._nodesById)) {
|
|
2276
|
-
for (const deferred of node.deferredVisuals.splice(0))
|
|
2277
|
-
this._materializeCoreVisual(node, deferred);
|
|
2278
|
-
}
|
|
2279
|
-
for (const particle of node.particles) {
|
|
2280
|
-
if (discontinuity)
|
|
2281
|
-
particle.seek(Math.max(0, time - (node.source.start ?? 0)));
|
|
2282
|
-
particle.opacity = opacity;
|
|
2283
|
-
particle.playing = playing && !node.entity.disabled;
|
|
2284
|
-
}
|
|
2285
|
-
for (const audio of node.audio)
|
|
2286
|
-
audio.sync(time, playing && !node.entity.disabled, speed, opacity);
|
|
2287
|
-
for (const visual of node.visuals) {
|
|
2288
|
-
if (visual.spriteUvRectTrack) {
|
|
2289
|
-
const frame = findVectorTrackFrame(visual.spriteUvRectTrack, time);
|
|
2290
|
-
if (frame !== visual.spriteUvRectFrame) {
|
|
2291
|
-
const offset = frame * 4;
|
|
2292
|
-
visual.component.setUvRect([
|
|
2293
|
-
visual.spriteUvRectTrack.values[offset],
|
|
2294
|
-
visual.spriteUvRectTrack.values[offset + 1],
|
|
2295
|
-
visual.spriteUvRectTrack.values[offset + 2],
|
|
2296
|
-
visual.spriteUvRectTrack.values[offset + 3],
|
|
2297
|
-
]);
|
|
2298
|
-
visual.spriteUvRectFrame = frame;
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
visual.textRasterizer?.setTime(time);
|
|
2302
|
-
if (visual.pathMorph && visual.lastMorphTime !== time) {
|
|
2303
|
-
const next = tessellateAnimationPath(sampleVectorPathMorph(visual.pathMorph, time));
|
|
2304
|
-
visual.component.geometry.setData(next.positions, next.indices);
|
|
2305
|
-
visual.component.revision++;
|
|
2306
|
-
visual.lastMorphTime = time;
|
|
2307
|
-
}
|
|
2308
|
-
if (visual.vectorShape && visual.lastVectorTime !== undefined && visual.lastVectorTime !== time) {
|
|
2309
|
-
updateVectorVisual(visual, time);
|
|
2310
|
-
visual.lastVectorTime = time;
|
|
2311
|
-
}
|
|
2312
|
-
if (visual.sourceEffects && visual.lastEffectTime !== time) {
|
|
2313
|
-
updateVisualEffects(visual.component, visual.sourceEffects, time);
|
|
2314
|
-
visual.component.revision++;
|
|
2315
|
-
visual.lastEffectTime = time;
|
|
2316
|
-
}
|
|
2317
|
-
if (visual.compositeExpansionTracks && visual.lastCompositeTime !== time) {
|
|
2318
|
-
for (let index = 0; index < visual.compositeExpansionTracks.length; index++) {
|
|
2319
|
-
const track = visual.compositeExpansionTracks[index];
|
|
2320
|
-
if (track)
|
|
2321
|
-
visual.component.setCompositeExpansion(index, sampleVectorTrack(track, time)[0] ?? 0);
|
|
2322
|
-
}
|
|
2323
|
-
visual.lastCompositeTime = time;
|
|
2324
|
-
}
|
|
2325
|
-
const alpha = visual.styleAlpha * opacity;
|
|
2326
|
-
if (Math.abs(alpha - visual.lastAlpha) < 1e-6)
|
|
2327
|
-
continue;
|
|
2328
|
-
visual.color[3] = alpha;
|
|
2329
|
-
visual.component.revision++;
|
|
2330
|
-
visual.lastAlpha = alpha;
|
|
2331
|
-
}
|
|
2332
|
-
for (const extension of node.extensions) {
|
|
2333
|
-
if (extension.apply)
|
|
2334
|
-
extension.apply(time, opacity);
|
|
2335
|
-
else
|
|
2336
|
-
extension.setOpacity?.(opacity);
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
this._lastAppliedTime = time;
|
|
2340
|
-
}
|
|
2341
|
-
destroy() {
|
|
2342
|
-
if (this._destroyed)
|
|
2343
|
-
return;
|
|
2344
|
-
this._destroyed = true;
|
|
2345
|
-
this._abortController.abort();
|
|
2346
|
-
for (const handle of this._assetHandles)
|
|
2347
|
-
handle.release();
|
|
2348
|
-
this._assetHandles.length = 0;
|
|
2349
|
-
if (typeof document !== 'undefined' && document.fonts) {
|
|
2350
|
-
for (const face of this._fontFaces)
|
|
2351
|
-
document.fonts.delete(face);
|
|
2352
|
-
}
|
|
2353
|
-
this._fontFaces.length = 0;
|
|
2354
|
-
this._fontLoads.clear();
|
|
2355
|
-
for (const node of this._nodes) {
|
|
2356
|
-
for (const visual of node.visuals)
|
|
2357
|
-
visual.textRasterizer?.destroy();
|
|
2358
|
-
for (const audio of node.audio)
|
|
2359
|
-
audio.destroy();
|
|
2360
|
-
node.audio.length = 0;
|
|
2361
|
-
for (const extension of node.extensions)
|
|
2362
|
-
extension.destroy?.();
|
|
2363
|
-
node.extensions.length = 0;
|
|
2364
|
-
}
|
|
2365
|
-
this._root.destroy();
|
|
2366
|
-
this._nodes.length = 0;
|
|
2367
|
-
this._nodesById.clear();
|
|
2368
|
-
this._tracksByNode.clear();
|
|
2369
|
-
this._trackIndices.clear();
|
|
2370
|
-
}
|
|
2371
|
-
setPlaying(playing) {
|
|
2372
|
-
this._playing = playing;
|
|
2373
|
-
if (playing)
|
|
2374
|
-
return;
|
|
2375
|
-
for (const node of this._nodes) {
|
|
2376
|
-
for (const particle of node.particles)
|
|
2377
|
-
particle.playing = false;
|
|
2378
|
-
for (const audio of node.audio)
|
|
2379
|
-
audio.pause();
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
_loadTexture(resourceId, apply) {
|
|
2383
|
-
const resource = this._animation.resources.find(candidate => candidate.id === resourceId);
|
|
2384
|
-
if (!resource || resource.type !== 'image' || !this._assetManager) {
|
|
2385
|
-
this._failedResourceCount++;
|
|
2386
|
-
return;
|
|
2387
|
-
}
|
|
2388
|
-
this._pendingResourceCount++;
|
|
2389
|
-
let request;
|
|
2390
|
-
try {
|
|
2391
|
-
request = this._assetManager.loadTexture(resource.uri, {
|
|
2392
|
-
label: `Animation2D:${resource.id}`,
|
|
2393
|
-
format: resource.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb',
|
|
2394
|
-
cacheKey: resource.integrity ?? resource.uri,
|
|
2395
|
-
signal: this._abortController.signal,
|
|
2396
|
-
});
|
|
2397
|
-
}
|
|
2398
|
-
catch {
|
|
2399
|
-
this._pendingResourceCount--;
|
|
2400
|
-
this._failedResourceCount++;
|
|
2401
|
-
return;
|
|
2402
|
-
}
|
|
2403
|
-
void request.then(handle => {
|
|
2404
|
-
if (this._destroyed) {
|
|
2405
|
-
handle.release();
|
|
2406
|
-
return;
|
|
2407
|
-
}
|
|
2408
|
-
this._assetHandles.push(handle);
|
|
2409
|
-
apply(handle);
|
|
2410
|
-
}).catch(() => {
|
|
2411
|
-
if (!this._destroyed)
|
|
2412
|
-
this._failedResourceCount++;
|
|
2413
|
-
}).finally(() => {
|
|
2414
|
-
this._pendingResourceCount = Math.max(0, this._pendingResourceCount - 1);
|
|
2415
|
-
});
|
|
2416
|
-
}
|
|
2417
|
-
_materializeCoreVisual(node, deferred) {
|
|
2418
|
-
let visual;
|
|
2419
|
-
try {
|
|
2420
|
-
visual = createCoreVisual(deferred.component, deferred.componentIndex, this._instanceId, node.source, deferred.order, deferred.sourceGroup, deferred.composite, deferred.effects);
|
|
2421
|
-
}
|
|
2422
|
-
catch {
|
|
2423
|
-
this._unsupportedComponentCount++;
|
|
2424
|
-
return;
|
|
2425
|
-
}
|
|
2426
|
-
node.content.addChild(new Entity(`${node.entity.name} visual`).addComponent(visual.component));
|
|
2427
|
-
node.visuals.push(visual.runtime);
|
|
2428
|
-
if (deferred.component.type === 'sprite2d') {
|
|
2429
|
-
this._loadTexture(deferred.component.resource, handle => visual.component.setTextureHandle(handle));
|
|
2430
|
-
}
|
|
2431
|
-
else if (deferred.component.type === 'text2d' && visual.runtime.textRasterizer) {
|
|
2432
|
-
this._loadTextFonts(deferred.component, visual.runtime.textRasterizer);
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
_loadTextFonts(component, rasterizer) {
|
|
2436
|
-
const descriptors = [
|
|
2437
|
-
component.fontResource ? {
|
|
2438
|
-
resource: component.fontResource,
|
|
2439
|
-
family: component.fontFamily ?? 'sans-serif',
|
|
2440
|
-
style: component.fontStyle ?? 'normal',
|
|
2441
|
-
weight: component.fontWeight ?? 400,
|
|
2442
|
-
} : null,
|
|
2443
|
-
...(component.documents ?? []).map(document => document.fontResource ? {
|
|
2444
|
-
resource: document.fontResource,
|
|
2445
|
-
family: document.fontFamily ?? component.fontFamily ?? 'sans-serif',
|
|
2446
|
-
style: document.fontStyle ?? component.fontStyle ?? 'normal',
|
|
2447
|
-
weight: document.fontWeight ?? component.fontWeight ?? 400,
|
|
2448
|
-
} : null),
|
|
2449
|
-
].filter((value) => value !== null);
|
|
2450
|
-
for (const descriptor of descriptors)
|
|
2451
|
-
this._loadFont(descriptor, rasterizer);
|
|
2452
|
-
}
|
|
2453
|
-
_loadFont(descriptor, rasterizer) {
|
|
2454
|
-
const key = `${descriptor.resource}:${descriptor.family}:${descriptor.style}:${descriptor.weight}`;
|
|
2455
|
-
const existing = this._fontLoads.get(key);
|
|
2456
|
-
if (existing) {
|
|
2457
|
-
if (existing.state === 'loaded')
|
|
2458
|
-
rasterizer.invalidateFont();
|
|
2459
|
-
else if (existing.state === 'loading')
|
|
2460
|
-
existing.rasterizers.add(rasterizer);
|
|
2461
|
-
return;
|
|
2462
|
-
}
|
|
2463
|
-
const load = { state: 'loading', rasterizers: new Set([rasterizer]) };
|
|
2464
|
-
this._fontLoads.set(key, load);
|
|
2465
|
-
const resource = this._animation.resources.find(candidate => candidate.id === descriptor.resource);
|
|
2466
|
-
if (!resource || resource.type !== 'binary' || !this._assetManager || typeof FontFace === 'undefined'
|
|
2467
|
-
|| typeof document === 'undefined' || !document.fonts) {
|
|
2468
|
-
this._fontLoads.set(key, { ...load, state: 'failed' });
|
|
2469
|
-
this._failedResourceCount++;
|
|
2470
|
-
return;
|
|
2471
|
-
}
|
|
2472
|
-
this._pendingResourceCount++;
|
|
2473
|
-
let retainedHandle = null;
|
|
2474
|
-
void this._assetManager.load(`Animation2D.font:${resource.integrity ?? resource.uri}`, async (signal) => {
|
|
2475
|
-
const response = await fetch(resource.uri, signal ? { signal } : {});
|
|
2476
|
-
if (!response.ok)
|
|
2477
|
-
throw new Error(`Font request failed with HTTP ${response.status}.`);
|
|
2478
|
-
return response.arrayBuffer();
|
|
2479
|
-
}, () => { }, { signal: this._abortController.signal }).then(async (handle) => {
|
|
2480
|
-
retainedHandle = handle;
|
|
2481
|
-
if (this._destroyed) {
|
|
2482
|
-
handle.release();
|
|
2483
|
-
return;
|
|
2484
|
-
}
|
|
2485
|
-
const face = new FontFace(descriptor.family, handle.value, {
|
|
2486
|
-
style: descriptor.style,
|
|
2487
|
-
weight: String(descriptor.weight),
|
|
2488
|
-
});
|
|
2489
|
-
await face.load();
|
|
2490
|
-
if (this._destroyed) {
|
|
2491
|
-
handle.release();
|
|
2492
|
-
return;
|
|
2493
|
-
}
|
|
2494
|
-
document.fonts.add(face);
|
|
2495
|
-
this._fontFaces.push(face);
|
|
2496
|
-
this._assetHandles.push(handle);
|
|
2497
|
-
retainedHandle = null;
|
|
2498
|
-
const current = this._fontLoads.get(key);
|
|
2499
|
-
if (current) {
|
|
2500
|
-
current.state = 'loaded';
|
|
2501
|
-
for (const target of current.rasterizers)
|
|
2502
|
-
target.invalidateFont();
|
|
2503
|
-
current.rasterizers.clear();
|
|
2504
|
-
}
|
|
2505
|
-
}).catch(() => {
|
|
2506
|
-
retainedHandle?.release();
|
|
2507
|
-
if (!this._destroyed) {
|
|
2508
|
-
const current = this._fontLoads.get(key);
|
|
2509
|
-
if (current) {
|
|
2510
|
-
current.state = 'failed';
|
|
2511
|
-
current.rasterizers.clear();
|
|
2512
|
-
}
|
|
2513
|
-
this._failedResourceCount++;
|
|
2514
|
-
}
|
|
2515
|
-
}).finally(() => {
|
|
2516
|
-
this._pendingResourceCount = Math.max(0, this._pendingResourceCount - 1);
|
|
2517
|
-
});
|
|
2518
|
-
}
|
|
2519
|
-
_applyTrack(node, track, time) {
|
|
2520
|
-
const trackIndex = this._trackIndices.get(track);
|
|
2521
|
-
const cursor = findFrame(track.times, time, this._trackCursors[trackIndex] ?? 0);
|
|
2522
|
-
this._trackCursors[trackIndex] = cursor;
|
|
2523
|
-
const progress = sampleProgress(track, cursor, time);
|
|
2524
|
-
const offset = cursor * track.valueSize;
|
|
2525
|
-
const nextOffset = Math.min(cursor + 1, track.times.length - 1) * track.valueSize;
|
|
2526
|
-
let x = mix(track.values[offset], track.values[nextOffset], progress);
|
|
2527
|
-
let y = track.valueSize === 2 ? mix(track.values[offset + 1], track.values[nextOffset + 1], progress) : 0;
|
|
2528
|
-
if (track.property === 'position' && track.spatialTangents && cursor < track.times.length - 1) {
|
|
2529
|
-
const tangentOffset = cursor * 4;
|
|
2530
|
-
x = spatialBezier(track.values[offset], track.values[offset] + track.spatialTangents[tangentOffset], track.values[nextOffset] + track.spatialTangents[tangentOffset + 2], track.values[nextOffset], progress);
|
|
2531
|
-
y = spatialBezier(track.values[offset + 1], track.values[offset + 1] + track.spatialTangents[tangentOffset + 1], track.values[nextOffset + 1] + track.spatialTangents[tangentOffset + 3], track.values[nextOffset + 1], progress);
|
|
2532
|
-
}
|
|
2533
|
-
switch (track.property) {
|
|
2534
|
-
case 'position':
|
|
2535
|
-
node.transform.setPosition(x, -y);
|
|
2536
|
-
break;
|
|
2537
|
-
case 'rotation':
|
|
2538
|
-
node.transform.rotation = -x;
|
|
2539
|
-
break;
|
|
2540
|
-
case 'scale':
|
|
2541
|
-
node.transform.setScale(x, y);
|
|
2542
|
-
break;
|
|
2543
|
-
case 'opacity':
|
|
2544
|
-
node.opacity = clamp(x, 0, 1);
|
|
2545
|
-
break;
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
_resolveOpacity(node) {
|
|
2549
|
-
const cached = this._opacityMemo.get(node.source.id);
|
|
2550
|
-
if (cached !== undefined)
|
|
2551
|
-
return cached;
|
|
2552
|
-
const parent = node.source.parent ? this._nodesById.get(node.source.parent) : undefined;
|
|
2553
|
-
const result = clamp(node.opacity * (parent ? this._resolveOpacity(parent) : 1), 0, 1);
|
|
2554
|
-
this._opacityMemo.set(node.source.id, result);
|
|
2555
|
-
return result;
|
|
2556
|
-
}
|
|
2557
|
-
_isNodeActive(node) {
|
|
2558
|
-
return isRuntimeNodeActive(node, this._nodesById);
|
|
2559
|
-
}
|
|
2560
|
-
/** @internal Applies state-machine-only component channels to the shared visual instance. */
|
|
2561
|
-
_applyStateMachineVisualChannel(node, path, value) {
|
|
2562
|
-
const match = /^components\.(\d+)\.(sprite\.uv-rect|vector\.morph|path\.morph)$/u.exec(path);
|
|
2563
|
-
if (!match)
|
|
2564
|
-
return false;
|
|
2565
|
-
const componentIndex = Number(match[1]);
|
|
2566
|
-
if (node.deferredVisuals.length > 0 && this._isNodeActive(node)) {
|
|
2567
|
-
const deferred = node.deferredVisuals.filter(item => item.componentIndex === componentIndex);
|
|
2568
|
-
node.deferredVisuals.splice(0, node.deferredVisuals.length, ...node.deferredVisuals.filter(item => item.componentIndex !== componentIndex));
|
|
2569
|
-
for (const item of deferred)
|
|
2570
|
-
this._materializeCoreVisual(node, item);
|
|
2571
|
-
}
|
|
2572
|
-
const visuals = node.visuals.filter(visual => visual.sourceComponentIndex === componentIndex);
|
|
2573
|
-
if (match[2] === 'sprite.uv-rect') {
|
|
2574
|
-
const uv = numericStateMachineValue(value, path, 4);
|
|
2575
|
-
for (const visual of visuals)
|
|
2576
|
-
visual.component.setUvRect([uv[0], uv[1], uv[2], uv[3]]);
|
|
2577
|
-
return true;
|
|
2578
|
-
}
|
|
2579
|
-
const morph = numericStateMachineValue(value, path, 1);
|
|
2580
|
-
for (const visual of visuals) {
|
|
2581
|
-
if (match[2] === 'path.morph' && visual.pathMorph) {
|
|
2582
|
-
const next = tessellateAnimationPath({
|
|
2583
|
-
type: 'path2d',
|
|
2584
|
-
commands: visual.pathMorph.commands,
|
|
2585
|
-
values: morph,
|
|
2586
|
-
fill: visual.pathMorph.fill,
|
|
2587
|
-
fillRule: visual.pathMorph.fillRule,
|
|
2588
|
-
...(visual.pathMorph.tolerance === undefined ? {} : { tolerance: visual.pathMorph.tolerance }),
|
|
2589
|
-
});
|
|
2590
|
-
visual.component.geometry.setData(next.positions, next.indices);
|
|
2591
|
-
visual.component.revision++;
|
|
2592
|
-
}
|
|
2593
|
-
else if (match[2] === 'vector.morph' && visual.vectorShape) {
|
|
2594
|
-
const shape = visual.vectorShape;
|
|
2595
|
-
let pathValues = morph;
|
|
2596
|
-
if (shape.morphRelative) {
|
|
2597
|
-
pathValues = new Float32Array(morph.length);
|
|
2598
|
-
for (let index = 0; index < morph.length; index++) {
|
|
2599
|
-
pathValues[index] = morph[index] + (shape.values[index] ?? 0);
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
updateVectorVisual(visual, 0, pathValues);
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
return true;
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
function numericStateMachineValue(value, bindingPath, minimumSize) {
|
|
2609
|
-
if (!(value instanceof Float32Array) || value.length < minimumSize) {
|
|
2610
|
-
throw new TypeError(`Animation2D state-machine binding "${bindingPath}" requires at least ${minimumSize} Float32 values.`);
|
|
2611
|
-
}
|
|
2612
|
-
return value;
|
|
2613
|
-
}
|
|
2614
|
-
function createEllipseGeometry(width, height, centerX, centerY) {
|
|
2615
|
-
const segments = 48;
|
|
2616
|
-
const positions = new Float32Array((segments + 1) * 2);
|
|
2617
|
-
positions[0] = centerX;
|
|
2618
|
-
positions[1] = centerY;
|
|
2619
|
-
for (let index = 0; index < segments; index++) {
|
|
2620
|
-
const angle = index / segments * Math.PI * 2;
|
|
2621
|
-
positions[(index + 1) * 2] = centerX + Math.cos(angle) * width / 2;
|
|
2622
|
-
positions[(index + 1) * 2 + 1] = centerY + Math.sin(angle) * height / 2;
|
|
2623
|
-
}
|
|
2624
|
-
const indices = new Uint16Array(segments * 3);
|
|
2625
|
-
for (let index = 0; index < segments; index++) {
|
|
2626
|
-
indices[index * 3] = 0;
|
|
2627
|
-
indices[index * 3 + 1] = index + 1;
|
|
2628
|
-
indices[index * 3 + 2] = index + 2 > segments ? 1 : index + 2;
|
|
2629
|
-
}
|
|
2630
|
-
return new Geometry2D(positions, indices);
|
|
2631
|
-
}
|
|
2632
|
-
function isShape2DComponent(component) {
|
|
2633
|
-
return component.type === 'shape2d'
|
|
2634
|
-
&& component.size !== undefined
|
|
2635
|
-
&& component.fill !== undefined;
|
|
2636
|
-
}
|
|
2637
|
-
function isPath2DComponent(component) {
|
|
2638
|
-
return component.type === 'path2d'
|
|
2639
|
-
&& typeof component.commands === 'string'
|
|
2640
|
-
&& component.values !== undefined;
|
|
2641
|
-
}
|
|
2642
|
-
function isSprite2DComponent(component) {
|
|
2643
|
-
return component.type === 'sprite2d'
|
|
2644
|
-
&& typeof component.resource === 'string'
|
|
2645
|
-
&& component.size !== undefined;
|
|
2646
|
-
}
|
|
2647
|
-
function isText2DComponent(component) {
|
|
2648
|
-
return component.type === 'text2d'
|
|
2649
|
-
&& typeof component.text === 'string'
|
|
2650
|
-
&& component.size !== undefined;
|
|
2651
|
-
}
|
|
2652
|
-
function isParticle2DComponent(component) {
|
|
2653
|
-
return component.type === 'particle2d'
|
|
2654
|
-
&& typeof component.maxParticles === 'number';
|
|
2655
|
-
}
|
|
2656
|
-
function isAudioComponent(component) {
|
|
2657
|
-
return component.type === 'audio' && typeof component.resource === 'string';
|
|
2658
|
-
}
|
|
2659
|
-
function isVectorStrokePathComponent(component) {
|
|
2660
|
-
const candidate = component;
|
|
2661
|
-
const inlinePath = typeof candidate.commands === 'string'
|
|
2662
|
-
&& /^[MLQCZ]+$/.test(candidate.commands)
|
|
2663
|
-
&& candidate.commands[0] === 'M'
|
|
2664
|
-
&& Array.isArray(candidate.values)
|
|
2665
|
-
&& candidate.values.length === strokePathValueCount(candidate.commands)
|
|
2666
|
-
&& candidate.values.every(value => typeof value === 'number' && Number.isFinite(value));
|
|
2667
|
-
const sourcePath = typeof candidate.sourceComponent === 'number'
|
|
2668
|
-
&& Number.isSafeInteger(candidate.sourceComponent)
|
|
2669
|
-
&& candidate.sourceComponent >= 0;
|
|
2670
|
-
return component.type === 'org.haiyue.vector-stroke@1'
|
|
2671
|
-
&& (inlinePath || sourcePath)
|
|
2672
|
-
&& Array.isArray(candidate.color)
|
|
2673
|
-
&& candidate.color.length === 4
|
|
2674
|
-
&& candidate.color.every(value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
|
|
2675
|
-
&& typeof candidate.width === 'number' && Number.isFinite(candidate.width) && candidate.width > 0
|
|
2676
|
-
&& (candidate.lineCap === 'butt' || candidate.lineCap === 'round' || candidate.lineCap === 'square')
|
|
2677
|
-
&& (candidate.lineJoin === 'miter' || candidate.lineJoin === 'round' || candidate.lineJoin === 'bevel')
|
|
2678
|
-
&& typeof candidate.miterLimit === 'number' && Number.isFinite(candidate.miterLimit) && candidate.miterLimit >= 1
|
|
2679
|
-
&& (candidate.tolerance === undefined
|
|
2680
|
-
|| (typeof candidate.tolerance === 'number' && Number.isFinite(candidate.tolerance) && candidate.tolerance > 0));
|
|
2681
|
-
}
|
|
2682
|
-
function isVectorPathMorphComponent(component) {
|
|
2683
|
-
const candidate = component;
|
|
2684
|
-
return component.type === 'org.haiyue.vector-path-morph@1'
|
|
2685
|
-
&& typeof candidate.commands === 'string'
|
|
2686
|
-
&& /^[MLQCZ]+$/.test(candidate.commands)
|
|
2687
|
-
&& (Array.isArray(candidate.times) || candidate.times instanceof Float32Array) && candidate.times.length > 0
|
|
2688
|
-
&& Array.from(candidate.times).every(value => typeof value === 'number' && Number.isFinite(value))
|
|
2689
|
-
&& (Array.isArray(candidate.values) || candidate.values instanceof Float32Array)
|
|
2690
|
-
&& typeof candidate.valueSize === 'number' && Number.isSafeInteger(candidate.valueSize) && candidate.valueSize > 0
|
|
2691
|
-
&& candidate.values.length === candidate.times.length * candidate.valueSize
|
|
2692
|
-
&& Array.from(candidate.values).every(value => typeof value === 'number' && Number.isFinite(value))
|
|
2693
|
-
&& (candidate.interpolation === 'step' || candidate.interpolation === 'linear' || candidate.interpolation === 'cubic-bezier')
|
|
2694
|
-
&& Array.isArray(candidate.fill) && candidate.fill.length === 4;
|
|
2695
|
-
}
|
|
2696
|
-
function strokePathValueCount(commands) {
|
|
2697
|
-
let count = 0;
|
|
2698
|
-
for (const command of commands)
|
|
2699
|
-
count += command === 'M' || command === 'L' ? 2 : command === 'Q' ? 4 : command === 'C' ? 6 : 0;
|
|
2700
|
-
return count;
|
|
2701
|
-
}
|
|
2702
|
-
function isCoreVisualComponent(component) {
|
|
2703
|
-
return isShape2DComponent(component) || isPath2DComponent(component) || isSprite2DComponent(component)
|
|
2704
|
-
|| isText2DComponent(component) || isVectorShapeComponent(component)
|
|
2705
|
-
|| isVectorStrokePathComponent(component) || isVectorPathMorphComponent(component);
|
|
2706
|
-
}
|
|
2707
|
-
function isVectorShapeComponent(component) {
|
|
2708
|
-
return component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID
|
|
2709
|
-
&& typeof component.commands === 'string';
|
|
2710
|
-
}
|
|
2711
|
-
function createCoreVisual(component, componentIndex, instanceId, node, order, sourceGroup, composite, effects) {
|
|
2712
|
-
let geometry;
|
|
2713
|
-
let color;
|
|
2714
|
-
let textMaterial = null;
|
|
2715
|
-
let gradient = null;
|
|
2716
|
-
let vectorPaint;
|
|
2717
|
-
if (component.type === 'path2d') {
|
|
2718
|
-
geometry = tessellateAnimationPath(component);
|
|
2719
|
-
color = [...component.fill];
|
|
2720
|
-
}
|
|
2721
|
-
else if (component.type === 'org.haiyue.vector-path-morph@1') {
|
|
2722
|
-
geometry = tessellateAnimationPath(sampleVectorPathMorph(component, component.times[0] ?? 0));
|
|
2723
|
-
color = [...component.fill];
|
|
2724
|
-
}
|
|
2725
|
-
else if (component.type === 'org.haiyue.vector-stroke@1') {
|
|
2726
|
-
geometry = tessellateVectorStrokePath(resolveVectorStrokePath(component, node));
|
|
2727
|
-
color = [...component.color];
|
|
2728
|
-
}
|
|
2729
|
-
else if (component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID) {
|
|
2730
|
-
const initialTime = component.morph?.times[0] ?? firstVectorModifierTime(component) ?? 0;
|
|
2731
|
-
const pathValues = sampleVectorShapePath(component, initialTime);
|
|
2732
|
-
const resolvedPath = resolveVectorPath(component, pathValues, initialTime, component.fill !== undefined);
|
|
2733
|
-
if (component.fill) {
|
|
2734
|
-
geometry = resolvedPath.commands.length === 0 ? emptyGeometry2D() : tessellateRuntimeFillPath({
|
|
2735
|
-
type: 'path2d', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
2736
|
-
fill: [1, 1, 1, 1], fillRule: component.fillRule ?? 'nonzero',
|
|
2737
|
-
...(component.tolerance === undefined ? {} : { tolerance: component.tolerance }),
|
|
2738
|
-
});
|
|
2739
|
-
vectorPaint = 'fill';
|
|
2740
|
-
if (component.fill.kind === 'solid')
|
|
2741
|
-
color = [...component.fill.color];
|
|
2742
|
-
else {
|
|
2743
|
-
color = [1, 1, 1, 1];
|
|
2744
|
-
gradient = createRuntimeGradient(component.fill);
|
|
2745
|
-
}
|
|
2746
|
-
}
|
|
2747
|
-
else {
|
|
2748
|
-
const stroke = component.stroke;
|
|
2749
|
-
geometry = resolvedPath.commands.length === 0 ? emptyGeometry2D() : tessellateVectorStrokePath({
|
|
2750
|
-
type: 'org.haiyue.vector-stroke@1', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
2751
|
-
color: stroke.color, width: stroke.width, lineCap: stroke.lineCap, lineJoin: stroke.lineJoin,
|
|
2752
|
-
miterLimit: stroke.miterLimit,
|
|
2753
|
-
...(component.tolerance === undefined ? {} : { tolerance: component.tolerance }),
|
|
2754
|
-
...(stroke.dash ? { dash: stroke.dash } : {}),
|
|
2755
|
-
...(stroke.dashOffset === undefined ? {} : { dashOffset: stroke.dashOffset }),
|
|
2756
|
-
}, true);
|
|
2757
|
-
vectorPaint = 'stroke';
|
|
2758
|
-
color = stroke.gradient ? [1, 1, 1, 1] : [...stroke.color];
|
|
2759
|
-
if (stroke.gradient)
|
|
2760
|
-
gradient = createRuntimeGradient(stroke.gradient);
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
else {
|
|
2764
|
-
const position = component.position ?? [0, 0];
|
|
2765
|
-
const centerX = position[0];
|
|
2766
|
-
const centerY = -position[1];
|
|
2767
|
-
geometry = component.type === 'shape2d' && component.shape === 'ellipse'
|
|
2768
|
-
? createEllipseGeometry(component.size[0], component.size[1], centerX, centerY)
|
|
2769
|
-
: createRect2D({ width: component.size[0], height: component.size[1], x: centerX, y: centerY });
|
|
2770
|
-
color = component.type === 'shape2d' ? [...component.fill]
|
|
2771
|
-
: component.type === 'sprite2d' ? [...(component.tint ?? [1, 1, 1, 1])] : [1, 1, 1, 1];
|
|
2772
|
-
if (component.type === 'text2d') {
|
|
2773
|
-
textMaterial = new AnimationTextRasterizer(component);
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
const visual = new AnimationVisual2D({
|
|
2777
|
-
geometry,
|
|
2778
|
-
color,
|
|
2779
|
-
instanceId,
|
|
2780
|
-
nodeId: sourceGroup ?? node.id,
|
|
2781
|
-
order,
|
|
2782
|
-
sourceOnly: sourceGroup !== undefined,
|
|
2783
|
-
...(composite ? { composite } : {}),
|
|
2784
|
-
...(component.type === 'sprite2d' ? { requiresTexture: true, uvRect: component.uvRect ?? [0, 0, 1, 1] } : {}),
|
|
2785
|
-
...(component.type === 'text2d' ? { requiresTexture: true, textMaterial } : {}),
|
|
2786
|
-
...(gradient ? { gradient } : {}),
|
|
2787
|
-
...(effects?.length ? { effects: createVisualEffects(effects, 0) } : {}),
|
|
2788
|
-
});
|
|
2789
|
-
const initialStyleAlpha = component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID
|
|
2790
|
-
? initialVectorStyleAlpha(component)
|
|
2791
|
-
: visual.color[3];
|
|
2792
|
-
const compositeExpansionTracks = composite
|
|
2793
|
-
? compositeLayers(composite).map(layer => layer.expansionTrack)
|
|
2794
|
-
: [];
|
|
2795
|
-
return {
|
|
2796
|
-
component: visual,
|
|
2797
|
-
runtime: {
|
|
2798
|
-
component: visual,
|
|
2799
|
-
sourceComponentIndex: componentIndex,
|
|
2800
|
-
color: visual.color,
|
|
2801
|
-
baseAlpha: visual.color[3],
|
|
2802
|
-
styleAlpha: initialStyleAlpha,
|
|
2803
|
-
lastAlpha: Number.NaN,
|
|
2804
|
-
...(component.type === 'org.haiyue.vector-path-morph@1' ? { pathMorph: component, lastMorphTime: component.times[0] ?? 0 } : {}),
|
|
2805
|
-
...(component.type === ANIMATION_VECTOR_SHAPE_EXTENSION_ID ? {
|
|
2806
|
-
vectorShape: component,
|
|
2807
|
-
vectorPaint: vectorPaint,
|
|
2808
|
-
...(isVectorShapeDynamic(component) ? { lastVectorTime: Number.NaN } : {}),
|
|
2809
|
-
} : {}),
|
|
2810
|
-
...(textMaterial ? { textRasterizer: textMaterial } : {}),
|
|
2811
|
-
...(effects?.length ? { sourceEffects: effects, lastEffectTime: 0 } : {}),
|
|
2812
|
-
...(compositeExpansionTracks.some(track => track !== undefined) ? {
|
|
2813
|
-
compositeExpansionTracks,
|
|
2814
|
-
lastCompositeTime: Number.NaN,
|
|
2815
|
-
} : {}),
|
|
2816
|
-
...(component.type === 'sprite2d' && component.uvRectTrack ? {
|
|
2817
|
-
spriteUvRectTrack: component.uvRectTrack,
|
|
2818
|
-
spriteUvRectFrame: -1,
|
|
2819
|
-
} : {}),
|
|
2820
|
-
},
|
|
2821
|
-
};
|
|
2822
|
-
}
|
|
2823
|
-
function createVisualEffects(effects, time) {
|
|
2824
|
-
return effects.map(effect => ({ kind: effect.kind, values: sampleEffectValues(effect, time) }));
|
|
2825
|
-
}
|
|
2826
|
-
function updateVisualEffects(visual, effects, time) {
|
|
2827
|
-
const count = Math.min(visual.effects.length, effects.length);
|
|
2828
|
-
for (let index = 0; index < count; index++) {
|
|
2829
|
-
const sampled = sampleEffectValues(effects[index], time);
|
|
2830
|
-
const target = visual.effects[index].values;
|
|
2831
|
-
if (target.length === sampled.length)
|
|
2832
|
-
target.set(sampled);
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
function sampleEffectValues(effect, time) {
|
|
2836
|
-
switch (effect.kind) {
|
|
2837
|
-
case 'tint': {
|
|
2838
|
-
const black = effect.blackTrack ? sampleVectorTrack(effect.blackTrack, time) : effect.black;
|
|
2839
|
-
const white = effect.whiteTrack ? sampleVectorTrack(effect.whiteTrack, time) : effect.white;
|
|
2840
|
-
const amount = effect.amountTrack ? sampleVectorTrack(effect.amountTrack, time)[0] ?? effect.amount : effect.amount;
|
|
2841
|
-
return new Float32Array([
|
|
2842
|
-
black[0] ?? effect.black[0], black[1] ?? effect.black[1], black[2] ?? effect.black[2],
|
|
2843
|
-
white[0] ?? effect.white[0], white[1] ?? effect.white[1], white[2] ?? effect.white[2],
|
|
2844
|
-
clamp(amount, 0, 1),
|
|
2845
|
-
]);
|
|
2846
|
-
}
|
|
2847
|
-
case 'fill': {
|
|
2848
|
-
const color = effect.colorTrack ? sampleVectorTrack(effect.colorTrack, time) : effect.color;
|
|
2849
|
-
const opacity = effect.opacityTrack ? sampleVectorTrack(effect.opacityTrack, time)[0] ?? effect.opacity ?? 1 : effect.opacity ?? 1;
|
|
2850
|
-
return new Float32Array([
|
|
2851
|
-
color[0] ?? effect.color[0], color[1] ?? effect.color[1], color[2] ?? effect.color[2], color[3] ?? effect.color[3],
|
|
2852
|
-
clamp(opacity, 0, 1),
|
|
2853
|
-
]);
|
|
2854
|
-
}
|
|
2855
|
-
case 'opacity': {
|
|
2856
|
-
const opacity = effect.opacityTrack ? sampleVectorTrack(effect.opacityTrack, time)[0] ?? effect.opacity : effect.opacity;
|
|
2857
|
-
return new Float32Array([clamp(opacity, 0, 1)]);
|
|
2858
|
-
}
|
|
2859
|
-
case 'color-matrix':
|
|
2860
|
-
return new Float32Array(effect.matrixTrack ? sampleVectorTrack(effect.matrixTrack, time) : effect.matrix);
|
|
2861
|
-
case 'blur': {
|
|
2862
|
-
const radius = effect.radiusTrack ? sampleVectorTrack(effect.radiusTrack, time) : effect.radius;
|
|
2863
|
-
return new Float32Array([Math.max(0, radius[0] ?? effect.radius[0]), Math.max(0, radius[1] ?? effect.radius[1])]);
|
|
2864
|
-
}
|
|
2865
|
-
case 'drop-shadow': {
|
|
2866
|
-
const color = effect.colorTrack ? sampleVectorTrack(effect.colorTrack, time) : effect.color;
|
|
2867
|
-
const opacity = effect.opacityTrack ? sampleVectorTrack(effect.opacityTrack, time)[0] ?? effect.opacity : effect.opacity;
|
|
2868
|
-
const offset = effect.offsetTrack ? sampleVectorTrack(effect.offsetTrack, time) : effect.offset;
|
|
2869
|
-
const blur = effect.blurTrack ? sampleVectorTrack(effect.blurTrack, time)[0] ?? effect.blur : effect.blur;
|
|
2870
|
-
return new Float32Array([
|
|
2871
|
-
color[0] ?? effect.color[0], color[1] ?? effect.color[1], color[2] ?? effect.color[2], color[3] ?? effect.color[3],
|
|
2872
|
-
clamp(opacity, 0, 1), offset[0] ?? effect.offset[0], offset[1] ?? effect.offset[1], Math.max(0, blur),
|
|
2873
|
-
]);
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
function updateVectorVisual(visual, time, pathOverride) {
|
|
2878
|
-
const shape = visual.vectorShape;
|
|
2879
|
-
const paint = visual.vectorPaint;
|
|
2880
|
-
const pathValues = pathOverride ?? sampleVectorShapePath(shape, time);
|
|
2881
|
-
let geometryChanged = shape.morph !== undefined || hasAnimatedVectorModifier(shape);
|
|
2882
|
-
if (paint === 'fill') {
|
|
2883
|
-
const fill = shape.fill;
|
|
2884
|
-
if (geometryChanged) {
|
|
2885
|
-
const resolvedPath = resolveVectorPath(shape, pathValues, time, true);
|
|
2886
|
-
const next = !resolvedPath.commands.length ? emptyGeometry2D() : tessellateRuntimeFillPath({
|
|
2887
|
-
type: 'path2d', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
2888
|
-
fill: [1, 1, 1, 1], fillRule: shape.fillRule ?? 'nonzero',
|
|
2889
|
-
...(shape.tolerance === undefined ? {} : { tolerance: shape.tolerance }),
|
|
2890
|
-
});
|
|
2891
|
-
visual.component.geometry.setData(next.positions, next.indices);
|
|
2892
|
-
}
|
|
2893
|
-
if (fill.kind === 'solid') {
|
|
2894
|
-
const sampledColor = fill.colorTrack ? sampleVectorTrack(fill.colorTrack, time) : fill.color;
|
|
2895
|
-
visual.color[0] = sampledColor[0] ?? fill.color[0];
|
|
2896
|
-
visual.color[1] = sampledColor[1] ?? fill.color[1];
|
|
2897
|
-
visual.color[2] = sampledColor[2] ?? fill.color[2];
|
|
2898
|
-
const sampledOpacity = fill.opacityTrack
|
|
2899
|
-
? sampleVectorTrack(fill.opacityTrack, time)[0] ?? fill.opacity ?? 1
|
|
2900
|
-
: fill.opacity ?? 1;
|
|
2901
|
-
visual.styleAlpha = clamp((sampledColor[3] ?? fill.color[3]) * sampledOpacity, 0, 1);
|
|
2902
|
-
}
|
|
2903
|
-
else {
|
|
2904
|
-
updateRuntimeGradient(visual, fill, time);
|
|
2905
|
-
visual.styleAlpha = 1;
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
else {
|
|
2909
|
-
const stroke = shape.stroke;
|
|
2910
|
-
const width = stroke.widthTrack ? sampleVectorTrack(stroke.widthTrack, time)[0] ?? stroke.width : stroke.width;
|
|
2911
|
-
const dashOffset = stroke.dashOffsetTrack
|
|
2912
|
-
? sampleVectorTrack(stroke.dashOffsetTrack, time)[0] ?? stroke.dashOffset ?? 0
|
|
2913
|
-
: stroke.dashOffset;
|
|
2914
|
-
geometryChanged ||= stroke.widthTrack !== undefined || stroke.dashOffsetTrack !== undefined;
|
|
2915
|
-
if (geometryChanged) {
|
|
2916
|
-
const resolvedPath = resolveVectorPath(shape, pathValues, time, false);
|
|
2917
|
-
const next = !resolvedPath.commands.length ? emptyGeometry2D() : tessellateVectorStrokePath({
|
|
2918
|
-
type: 'org.haiyue.vector-stroke@1', commands: resolvedPath.commands, values: resolvedPath.values,
|
|
2919
|
-
color: stroke.color, width, lineCap: stroke.lineCap, lineJoin: stroke.lineJoin,
|
|
2920
|
-
miterLimit: stroke.miterLimit,
|
|
2921
|
-
...(shape.tolerance === undefined ? {} : { tolerance: shape.tolerance }),
|
|
2922
|
-
...(stroke.dash ? { dash: stroke.dash } : {}),
|
|
2923
|
-
...(dashOffset === undefined ? {} : { dashOffset }),
|
|
2924
|
-
}, true);
|
|
2925
|
-
visual.component.geometry.setData(next.positions, next.indices);
|
|
2926
|
-
}
|
|
2927
|
-
const sampledColor = stroke.colorTrack ? sampleVectorTrack(stroke.colorTrack, time) : stroke.color;
|
|
2928
|
-
visual.color[0] = stroke.gradient ? 1 : sampledColor[0] ?? stroke.color[0];
|
|
2929
|
-
visual.color[1] = stroke.gradient ? 1 : sampledColor[1] ?? stroke.color[1];
|
|
2930
|
-
visual.color[2] = stroke.gradient ? 1 : sampledColor[2] ?? stroke.color[2];
|
|
2931
|
-
const sampledOpacity = stroke.opacityTrack
|
|
2932
|
-
? sampleVectorTrack(stroke.opacityTrack, time)[0] ?? stroke.opacity ?? 1
|
|
2933
|
-
: stroke.opacity ?? 1;
|
|
2934
|
-
visual.styleAlpha = stroke.gradient ? 1 : clamp((sampledColor[3] ?? stroke.color[3]) * sampledOpacity, 0, 1);
|
|
2935
|
-
if (stroke.gradient)
|
|
2936
|
-
updateRuntimeGradient(visual, stroke.gradient, time);
|
|
2937
|
-
}
|
|
2938
|
-
visual.component.revision++;
|
|
2939
|
-
}
|
|
2940
|
-
function sampleVectorShapePath(shape, time) {
|
|
2941
|
-
if (!shape.morph)
|
|
2942
|
-
return shape.values instanceof Float32Array
|
|
2943
|
-
? shape.values
|
|
2944
|
-
: new Float32Array(shape.values);
|
|
2945
|
-
const sampled = sampleVectorTrack(shape.morph, time);
|
|
2946
|
-
if (!shape.morphRelative)
|
|
2947
|
-
return sampled;
|
|
2948
|
-
for (let index = 0; index < sampled.length; index++) {
|
|
2949
|
-
sampled[index] = (sampled[index] ?? 0) + (shape.values[index] ?? 0);
|
|
2950
|
-
}
|
|
2951
|
-
return sampled;
|
|
2952
|
-
}
|
|
2953
|
-
function createRuntimeGradient(paint) {
|
|
2954
|
-
return {
|
|
2955
|
-
kind: paint.kind === 'linear-gradient' ? 'linear' : 'radial',
|
|
2956
|
-
start: [paint.start[0], -paint.start[1]],
|
|
2957
|
-
end: [paint.end[0], -paint.end[1]],
|
|
2958
|
-
stops: new Float32Array(paint.stops),
|
|
2959
|
-
opacity: paint.opacity ?? 1,
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
function updateRuntimeGradient(visual, paint, time) {
|
|
2963
|
-
const gradient = visual.component.gradient ??= createRuntimeGradient(paint);
|
|
2964
|
-
const start = paint.startTrack ? sampleVectorTrack(paint.startTrack, time) : paint.start;
|
|
2965
|
-
const end = paint.endTrack ? sampleVectorTrack(paint.endTrack, time) : paint.end;
|
|
2966
|
-
const stops = paint.stopsTrack ? sampleVectorTrack(paint.stopsTrack, time) : paint.stops;
|
|
2967
|
-
gradient.start[0] = start[0] ?? paint.start[0];
|
|
2968
|
-
gradient.start[1] = -(start[1] ?? paint.start[1]);
|
|
2969
|
-
gradient.end[0] = end[0] ?? paint.end[0];
|
|
2970
|
-
gradient.end[1] = -(end[1] ?? paint.end[1]);
|
|
2971
|
-
if (gradient.stops.length !== stops.length)
|
|
2972
|
-
gradient.stops = new Float32Array(stops);
|
|
2973
|
-
else
|
|
2974
|
-
gradient.stops.set(stops);
|
|
2975
|
-
gradient.opacity = paint.opacityTrack
|
|
2976
|
-
? sampleVectorTrack(paint.opacityTrack, time)[0] ?? paint.opacity ?? 1
|
|
2977
|
-
: paint.opacity ?? 1;
|
|
2978
|
-
}
|
|
2979
|
-
function initialVectorStyleAlpha(component) {
|
|
2980
|
-
if (component.fill)
|
|
2981
|
-
return component.fill.kind === 'solid'
|
|
2982
|
-
? component.fill.color[3] * (component.fill.opacity ?? 1)
|
|
2983
|
-
: 1;
|
|
2984
|
-
const stroke = component.stroke;
|
|
2985
|
-
return stroke.gradient ? 1 : stroke.color[3] * (stroke.opacity ?? 1);
|
|
2986
|
-
}
|
|
2987
|
-
function isVectorShapeDynamic(component) {
|
|
2988
|
-
if (component.morph || hasAnimatedVectorModifier(component))
|
|
2989
|
-
return true;
|
|
2990
|
-
if (component.fill)
|
|
2991
|
-
return component.fill.kind === 'solid'
|
|
2992
|
-
? component.fill.colorTrack !== undefined || component.fill.opacityTrack !== undefined
|
|
2993
|
-
: component.fill.startTrack !== undefined || component.fill.endTrack !== undefined
|
|
2994
|
-
|| component.fill.stopsTrack !== undefined || component.fill.opacityTrack !== undefined;
|
|
2995
|
-
const stroke = component.stroke;
|
|
2996
|
-
return stroke.colorTrack !== undefined || stroke.opacityTrack !== undefined || stroke.widthTrack !== undefined
|
|
2997
|
-
|| stroke.dashOffsetTrack !== undefined || (stroke.gradient !== undefined && (stroke.gradient.startTrack !== undefined || stroke.gradient.endTrack !== undefined
|
|
2998
|
-
|| stroke.gradient.stopsTrack !== undefined || stroke.gradient.opacityTrack !== undefined));
|
|
2999
|
-
}
|
|
3000
|
-
function resolveVectorPath(component, values, time, closeOpen) {
|
|
3001
|
-
if (!component.modifiers?.length)
|
|
3002
|
-
return { commands: component.commands, values: new Float32Array(values) };
|
|
3003
|
-
return applyVectorPathModifiers(component.commands, values, component.tolerance ?? 0.35, component.modifiers.map(modifier => modifier.kind === 'round-corners'
|
|
3004
|
-
? {
|
|
3005
|
-
kind: modifier.kind,
|
|
3006
|
-
radius: modifier.radiusTrack ? sampleVectorTrack(modifier.radiusTrack, time)[0] ?? modifier.radius : modifier.radius,
|
|
3007
|
-
}
|
|
3008
|
-
: {
|
|
3009
|
-
kind: modifier.kind,
|
|
3010
|
-
start: modifier.startTrack ? sampleVectorTrack(modifier.startTrack, time)[0] ?? modifier.start : modifier.start,
|
|
3011
|
-
end: modifier.endTrack ? sampleVectorTrack(modifier.endTrack, time)[0] ?? modifier.end : modifier.end,
|
|
3012
|
-
offset: modifier.offsetTrack ? sampleVectorTrack(modifier.offsetTrack, time)[0] ?? modifier.offset : modifier.offset,
|
|
3013
|
-
mode: modifier.mode,
|
|
3014
|
-
}), closeOpen);
|
|
3015
|
-
}
|
|
3016
|
-
function hasAnimatedVectorModifier(component) {
|
|
3017
|
-
return component.modifiers?.some(modifier => modifier.kind === 'round-corners'
|
|
3018
|
-
? modifier.radiusTrack !== undefined
|
|
3019
|
-
: modifier.startTrack !== undefined || modifier.endTrack !== undefined || modifier.offsetTrack !== undefined) ?? false;
|
|
3020
|
-
}
|
|
3021
|
-
function firstVectorModifierTime(component) {
|
|
3022
|
-
for (const modifier of component.modifiers ?? []) {
|
|
3023
|
-
const track = modifier.kind === 'round-corners'
|
|
3024
|
-
? modifier.radiusTrack
|
|
3025
|
-
: modifier.startTrack ?? modifier.endTrack ?? modifier.offsetTrack;
|
|
3026
|
-
if (track)
|
|
3027
|
-
return track.times[0];
|
|
3028
|
-
}
|
|
3029
|
-
return undefined;
|
|
3030
|
-
}
|
|
3031
|
-
function emptyGeometry2D() {
|
|
3032
|
-
// Geometry2D deliberately rejects empty buffers; a zero-area triangle keeps
|
|
3033
|
-
// the renderer contract valid while producing no fragments for an empty trim.
|
|
3034
|
-
return new Geometry2D(new Float32Array(6), new Uint16Array([0, 1, 2]));
|
|
3035
|
-
}
|
|
3036
|
-
function tessellateRuntimeFillPath(component) {
|
|
3037
|
-
try {
|
|
3038
|
-
return tessellateAnimationPath(component);
|
|
3039
|
-
}
|
|
3040
|
-
catch (error) {
|
|
3041
|
-
if (error instanceof RangeError && (error.message === 'Animation path contains no drawable closed contour.'
|
|
3042
|
-
|| error.message === 'Animation path tessellation produced no triangles.'))
|
|
3043
|
-
return emptyGeometry2D();
|
|
3044
|
-
throw error;
|
|
3045
|
-
}
|
|
3046
|
-
}
|
|
3047
|
-
function sampleVectorTrack(track, time) {
|
|
3048
|
-
const frameCount = track.times.length;
|
|
3049
|
-
if (frameCount === 0 || track.valueSize <= 0)
|
|
3050
|
-
return new Float32Array();
|
|
3051
|
-
const frame = findVectorTrackFrame(track, time);
|
|
3052
|
-
const next = Math.min(frame + 1, frameCount - 1);
|
|
3053
|
-
let progress = 0;
|
|
3054
|
-
if (next !== frame && track.interpolation !== 'step') {
|
|
3055
|
-
const start = track.times[frame];
|
|
3056
|
-
const end = track.times[next];
|
|
3057
|
-
progress = clamp((time - start) / Math.max(end - start, 1e-8), 0, 1);
|
|
3058
|
-
if (track.interpolation === 'cubic-bezier' && track.easings) {
|
|
3059
|
-
const offset = frame * 4;
|
|
3060
|
-
progress = cubicBezierYForX(progress, track.easings[offset] ?? 0.333, track.easings[offset + 1] ?? 0.333, track.easings[offset + 2] ?? 0.667, track.easings[offset + 3] ?? 0.667);
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
const result = new Float32Array(track.valueSize);
|
|
3064
|
-
const fromOffset = frame * track.valueSize;
|
|
3065
|
-
const toOffset = next * track.valueSize;
|
|
3066
|
-
for (let index = 0; index < track.valueSize; index++) {
|
|
3067
|
-
const from = track.values[fromOffset + index] ?? 0;
|
|
3068
|
-
result[index] = mix(from, track.values[toOffset + index] ?? from, progress);
|
|
3069
|
-
}
|
|
3070
|
-
return result;
|
|
3071
|
-
}
|
|
3072
|
-
function findVectorTrackFrame(track, time) {
|
|
3073
|
-
let low = 0;
|
|
3074
|
-
let high = track.times.length - 1;
|
|
3075
|
-
while (low < high) {
|
|
3076
|
-
const middle = Math.ceil((low + high) / 2);
|
|
3077
|
-
if (track.times[middle] <= time)
|
|
3078
|
-
low = middle;
|
|
3079
|
-
else
|
|
3080
|
-
high = middle - 1;
|
|
3081
|
-
}
|
|
3082
|
-
return low;
|
|
3083
|
-
}
|
|
3084
|
-
function isSourceNodeActiveAt(node, time, nodesById, animationDuration) {
|
|
3085
|
-
let cursor = node;
|
|
3086
|
-
while (cursor) {
|
|
3087
|
-
const start = cursor.start ?? 0;
|
|
3088
|
-
const end = start + (cursor.duration ?? animationDuration);
|
|
3089
|
-
if (time < start || time > end)
|
|
3090
|
-
return false;
|
|
3091
|
-
cursor = cursor.parent ? nodesById.get(cursor.parent) : undefined;
|
|
3092
|
-
}
|
|
3093
|
-
return true;
|
|
3094
|
-
}
|
|
3095
|
-
function isRuntimeNodeActive(node, nodesById) {
|
|
3096
|
-
let cursor = node;
|
|
3097
|
-
while (cursor) {
|
|
3098
|
-
if (cursor.entity.disabled)
|
|
3099
|
-
return false;
|
|
3100
|
-
cursor = cursor.source.parent ? nodesById.get(cursor.source.parent) : undefined;
|
|
3101
|
-
}
|
|
3102
|
-
return true;
|
|
3103
|
-
}
|
|
3104
|
-
function resolveVectorStrokePath(component, node) {
|
|
3105
|
-
if (typeof component.commands === 'string' && component.values !== undefined)
|
|
3106
|
-
return component;
|
|
3107
|
-
const sourceIndex = component.sourceComponent;
|
|
3108
|
-
const source = typeof sourceIndex === 'number' ? node.components?.[sourceIndex] : undefined;
|
|
3109
|
-
if (!source || !isPath2DComponent(source))
|
|
3110
|
-
throw new TypeError('Lottie stroke extension references a missing path2d source component.');
|
|
3111
|
-
return { ...component, commands: source.commands, values: source.values };
|
|
3112
|
-
}
|
|
3113
|
-
function resolveSourceGroups(nodes, sources) {
|
|
3114
|
-
const byId = new Map(nodes.map(node => [node.id, node]));
|
|
3115
|
-
const result = new Map();
|
|
3116
|
-
for (const node of nodes) {
|
|
3117
|
-
let cursor = node;
|
|
3118
|
-
while (cursor) {
|
|
3119
|
-
if (sources.has(cursor.id)) {
|
|
3120
|
-
result.set(node.id, cursor.id);
|
|
3121
|
-
break;
|
|
3122
|
-
}
|
|
3123
|
-
cursor = cursor.parent ? byId.get(cursor.parent) : undefined;
|
|
3124
|
-
}
|
|
3125
|
-
}
|
|
3126
|
-
return result;
|
|
3127
|
-
}
|
|
3128
|
-
function resolveInheritedEffects(nodes, sourceGroups) {
|
|
3129
|
-
const byId = new Map(nodes.map(node => [node.id, node]));
|
|
3130
|
-
const result = new Map();
|
|
3131
|
-
for (const node of nodes) {
|
|
3132
|
-
const sourceBoundary = sourceGroups.get(node.id);
|
|
3133
|
-
let cursor = node;
|
|
3134
|
-
while (cursor) {
|
|
3135
|
-
if (cursor.effects?.length) {
|
|
3136
|
-
result.set(node.id, cursor.effects);
|
|
3137
|
-
break;
|
|
3138
|
-
}
|
|
3139
|
-
if (cursor.id === sourceBoundary)
|
|
3140
|
-
break;
|
|
3141
|
-
cursor = cursor.parent ? byId.get(cursor.parent) : undefined;
|
|
3142
|
-
}
|
|
3143
|
-
}
|
|
3144
|
-
return result;
|
|
3145
|
-
}
|
|
3146
|
-
function resolveInheritedComposites(nodes, sourceGroups) {
|
|
3147
|
-
const byId = new Map(nodes.map(node => [node.id, node]));
|
|
3148
|
-
const result = new Map();
|
|
3149
|
-
for (const node of nodes) {
|
|
3150
|
-
const sourceBoundary = sourceGroups.get(node.id);
|
|
3151
|
-
let cursor = node;
|
|
3152
|
-
while (cursor) {
|
|
3153
|
-
if (cursor.composite) {
|
|
3154
|
-
result.set(node.id, cursor.composite);
|
|
3155
|
-
break;
|
|
3156
|
-
}
|
|
3157
|
-
if (cursor.id === sourceBoundary)
|
|
3158
|
-
break;
|
|
3159
|
-
cursor = cursor.parent ? byId.get(cursor.parent) : undefined;
|
|
3160
|
-
}
|
|
3161
|
-
}
|
|
3162
|
-
return result;
|
|
3163
|
-
}
|
|
3164
|
-
function compositeLayers(composite) {
|
|
3165
|
-
if (!composite)
|
|
3166
|
-
return [];
|
|
3167
|
-
return 'layers' in composite ? composite.layers : [composite];
|
|
3168
|
-
}
|
|
3169
|
-
function findFrame(times, time, previous) {
|
|
3170
|
-
if (times.length <= 1 || time <= times[0])
|
|
3171
|
-
return 0;
|
|
3172
|
-
if (time >= times[times.length - 1])
|
|
3173
|
-
return times.length - 1;
|
|
3174
|
-
let cursor = Math.min(previous, times.length - 2);
|
|
3175
|
-
if (time >= times[cursor] && time < times[cursor + 1])
|
|
3176
|
-
return cursor;
|
|
3177
|
-
let low = 0;
|
|
3178
|
-
let high = times.length - 1;
|
|
3179
|
-
while (low + 1 < high) {
|
|
3180
|
-
const middle = (low + high) >>> 1;
|
|
3181
|
-
if (times[middle] <= time)
|
|
3182
|
-
low = middle;
|
|
3183
|
-
else
|
|
3184
|
-
high = middle;
|
|
3185
|
-
}
|
|
3186
|
-
return low;
|
|
3187
|
-
}
|
|
3188
|
-
function sampleProgress(track, frame, time) {
|
|
3189
|
-
if (frame >= track.times.length - 1 || track.interpolation === 'step')
|
|
3190
|
-
return 0;
|
|
3191
|
-
const start = track.times[frame];
|
|
3192
|
-
const end = track.times[frame + 1];
|
|
3193
|
-
const linear = clamp((time - start) / Math.max(end - start, 1e-8), 0, 1);
|
|
3194
|
-
if (track.interpolation !== 'cubic-bezier' || !track.easings)
|
|
3195
|
-
return linear;
|
|
3196
|
-
const offset = frame * 4;
|
|
3197
|
-
return cubicBezierYForX(linear, track.easings[offset], track.easings[offset + 1], track.easings[offset + 2], track.easings[offset + 3]);
|
|
3198
|
-
}
|
|
3199
|
-
function cubicBezierYForX(x, x1, y1, x2, y2) {
|
|
3200
|
-
let t = x;
|
|
3201
|
-
for (let iteration = 0; iteration < 5; iteration++) {
|
|
3202
|
-
const estimate = bezier(t, x1, x2) - x;
|
|
3203
|
-
const derivative = bezierDerivative(t, x1, x2);
|
|
3204
|
-
if (Math.abs(derivative) < 1e-5)
|
|
3205
|
-
break;
|
|
3206
|
-
t = clamp(t - estimate / derivative, 0, 1);
|
|
3207
|
-
}
|
|
3208
|
-
return bezier(t, y1, y2);
|
|
3209
|
-
}
|
|
3210
|
-
function bezier(t, a, b) {
|
|
3211
|
-
const inverse = 1 - t;
|
|
3212
|
-
return 3 * inverse * inverse * t * a + 3 * inverse * t * t * b + t * t * t;
|
|
3213
|
-
}
|
|
3214
|
-
function bezierDerivative(t, a, b) {
|
|
3215
|
-
const inverse = 1 - t;
|
|
3216
|
-
return 3 * inverse * inverse * a + 6 * inverse * t * (b - a) + 3 * t * t * (1 - b);
|
|
3217
|
-
}
|
|
3218
|
-
function mix(a, b, t) {
|
|
3219
|
-
return a + (b - a) * t;
|
|
3220
|
-
}
|
|
3221
|
-
function spatialBezier(start, control1, control2, end, t) {
|
|
3222
|
-
const inverse = 1 - t;
|
|
3223
|
-
return inverse * inverse * inverse * start
|
|
3224
|
-
+ 3 * inverse * inverse * t * control1
|
|
3225
|
-
+ 3 * inverse * t * t * control2
|
|
3226
|
-
+ t * t * t * end;
|
|
3227
|
-
}
|
|
3228
|
-
function clamp(value, min, max) {
|
|
3229
|
-
return Math.min(max, Math.max(min, value));
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
/**
|
|
3233
|
-
* Creates a shared state-machine controller for a 2D mixer adapter.
|
|
3234
|
-
*
|
|
3235
|
-
* Animation2DComponent remains a single-composition player. A port must own
|
|
3236
|
-
* the 2D pose sampling, blending, discrete-property policy, and side-effect
|
|
3237
|
-
* lifecycle; this factory intentionally does not emulate those semantics by
|
|
3238
|
-
* switching component timelines.
|
|
3239
|
-
*/
|
|
3240
|
-
function createAnimation2DStateMachineController(definition, port, options) {
|
|
3241
|
-
return new Animation3DStateMachineController(compileAnimationStateMachineDefinition(definition), port, options);
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
export { Animation2DRuntime as A, AnimationVisual2D as a, createAnimation2DStateMachineController as c, tessellateAnimationPath as t };
|
|
3245
|
-
//# sourceMappingURL=Animation2DStateMachine-9IonguNa.js.map
|