@haiyue/extensions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/animation/Animation2DComponent.d.ts +46 -0
  4. package/dist/animation/Animation2DExtensionRegistry.d.ts +27 -0
  5. package/dist/animation/Animation2DRenderSystem.d.ts +89 -0
  6. package/dist/animation/Animation2DRuntime.d.ts +99 -0
  7. package/dist/animation/Animation2DStateMachine.d.ts +13 -0
  8. package/dist/animation/Animation2DStateMachineComponent.d.ts +54 -0
  9. package/dist/animation/Animation2DStateMachineRuntime.d.ts +30 -0
  10. package/dist/animation/Animation2DStateMachineSystem.d.ts +14 -0
  11. package/dist/animation/Animation2DStateMachineVisualRuntime.d.ts +27 -0
  12. package/dist/animation/Animation2DSystem.d.ts +14 -0
  13. package/dist/animation/AnimationAssetLoader.d.ts +9 -0
  14. package/dist/animation/AnimationAudioClip.d.ts +26 -0
  15. package/dist/animation/AnimationPathTessellator.d.ts +52 -0
  16. package/dist/animation/AnimationTextRasterizer.d.ts +21 -0
  17. package/dist/animation/AnimationVisual2D.d.ts +68 -0
  18. package/dist/animation/HyaAnimation2DClipAdapter.d.ts +16 -0
  19. package/dist/animation/Particle2DDescriptorAdapter.d.ts +5 -0
  20. package/dist/animation/runtime/mixer/Action.d.ts +150 -0
  21. package/dist/animation/runtime/mixer/Mixer.d.ts +54 -0
  22. package/dist/animation/runtime/mixer/PoseBuffer.d.ts +23 -0
  23. package/dist/animation/runtime/mixer/Sampler.d.ts +17 -0
  24. package/dist/animation/runtime/mixer/Types.d.ts +101 -0
  25. package/dist/animation/runtime/mixer/index.d.ts +6 -0
  26. package/dist/animation-state-machine/AnimationStateMachine.d.ts +111 -0
  27. package/dist/animation-state-machine/AnimationStateMachineChannels.d.ts +23 -0
  28. package/dist/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.d.ts +51 -0
  29. package/dist/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.d.ts +14 -0
  30. package/dist/animation-state-machine/runtime/AnimationStateMachineCompiler.d.ts +109 -0
  31. package/dist/animation-state-machine/runtime/AnimationStateMachineController.d.ts +103 -0
  32. package/dist/animation-state-machine/runtime/AnimationStateMachineMixerPort.d.ts +35 -0
  33. package/dist/animation-state-machine/runtime/index.d.ts +7 -0
  34. package/dist/animation.d.ts +15 -0
  35. package/dist/animation.js +1171 -0
  36. package/dist/animation3d/Animation3DAction.d.ts +46 -0
  37. package/dist/animation3d/Animation3DBinding.d.ts +85 -0
  38. package/dist/animation3d/Animation3DClip.d.ts +20 -0
  39. package/dist/animation3d/Animation3DError.d.ts +13 -0
  40. package/dist/animation3d/Animation3DMixer.d.ts +27 -0
  41. package/dist/animation3d/Animation3DPose.d.ts +40 -0
  42. package/dist/animation3d/Animation3DPoseBuffer.d.ts +24 -0
  43. package/dist/animation3d/Animation3DResource.d.ts +28 -0
  44. package/dist/animation3d/Animation3DStateMachine.d.ts +95 -0
  45. package/dist/animation3d/Animation3DStateMachineRuntime.d.ts +70 -0
  46. package/dist/animation3d/Animation3DTrack.d.ts +19 -0
  47. package/dist/animation3d/hya/HyaAnimation3DRuntime.d.ts +73 -0
  48. package/dist/animation3d/hya/HyaAnimation3DStateMachineRuntime.d.ts +65 -0
  49. package/dist/animation3d/hya/HyaAnimation3DTypes.d.ts +170 -0
  50. package/dist/animation3d/hya/index.d.ts +4 -0
  51. package/dist/animation3d/index.d.ts +14 -0
  52. package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.d.ts +81 -0
  53. package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.d.ts +39 -0
  54. package/dist/animation3d/runtime/integration/index.d.ts +2 -0
  55. package/dist/animation3d/runtime/mixer/Action.d.ts +150 -0
  56. package/dist/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.d.ts +6 -0
  57. package/dist/animation3d/runtime/mixer/Mixer.d.ts +59 -0
  58. package/dist/animation3d/runtime/mixer/PoseBuffer.d.ts +1 -0
  59. package/dist/animation3d/runtime/mixer/TrackSampler.d.ts +24 -0
  60. package/dist/animation3d/runtime/mixer/index.d.ts +4 -0
  61. package/dist/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.d.ts +1 -0
  62. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.d.ts +1 -0
  63. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineController.d.ts +1 -0
  64. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.d.ts +1 -0
  65. package/dist/animation3d/runtime/state-machine/index.d.ts +6 -0
  66. package/dist/animation3d.d.ts +5 -0
  67. package/dist/animation3d.js +1708 -0
  68. package/dist/benchmark.d.ts +30 -0
  69. package/dist/benchmark.js +119 -0
  70. package/dist/canvas-text/CanvasText2DRenderSystem.d.ts +24 -0
  71. package/dist/canvas-text/CanvasTextComponent.d.ts +18 -0
  72. package/dist/canvas-text.d.ts +4 -0
  73. package/dist/canvas-text.js +276 -0
  74. package/dist/chunks/2d-ui-animation-2d.generated-D4aHzFU2.js +4 -0
  75. package/dist/chunks/2d-ui-canvas-text-2d.generated-C0Eaej2m.js +4 -0
  76. package/dist/chunks/2d-ui-spine2d.generated-CUUi8xjl.js +4 -0
  77. package/dist/chunks/2d-ui-tilemap2d.generated-gebw4mX7.js +4 -0
  78. package/dist/chunks/Animation2DStateMachine-9IonguNa.js +3245 -0
  79. package/dist/chunks/AnimationStateMachineController-BIcAh-sT.js +1438 -0
  80. package/dist/chunks/GltfAnimation3DAdapter-Cu3rQQsW.js +2240 -0
  81. package/dist/chunks/GltfAnimationRuntime-CRHsLycN.js +631 -0
  82. package/dist/chunks/RenderSystem2DBase-DDT61dAu.js +227 -0
  83. package/dist/chunks/Spine2DRuntime-D9M9LAQR.js +3056 -0
  84. package/dist/chunks/SpineAssetParser-BSDmPge1.js +378 -0
  85. package/dist/chunks/gltfLoader-Dm22jLYB.js +2085 -0
  86. package/dist/chunks/pluginUtils-CvwroRMd.js +33 -0
  87. package/dist/experimental-gltf-worker.d.ts +4 -0
  88. package/dist/experimental-gltf-worker.js +138 -0
  89. package/dist/experimental-spine-worker.d.ts +4 -0
  90. package/dist/experimental-spine-worker.js +110 -0
  91. package/dist/gltf/GltfAccessorReader.d.ts +7 -0
  92. package/dist/gltf/GltfAnimation3DAdapter.d.ts +47 -0
  93. package/dist/gltf/GltfAnimationRuntime.d.ts +10 -0
  94. package/dist/gltf/GltfAssetWorkerClient.d.ts +20 -0
  95. package/dist/gltf/GltfCompatibilityReport.d.ts +70 -0
  96. package/dist/gltf/GltfConservativeBounds.d.ts +18 -0
  97. package/dist/gltf/GltfDracoDecoder.d.ts +4 -0
  98. package/dist/gltf/GltfExtensionAdapter.d.ts +54 -0
  99. package/dist/gltf/GltfLoaderContract.d.ts +249 -0
  100. package/dist/gltf/GltfLoaderErrors.d.ts +4 -0
  101. package/dist/gltf/GltfMaterialDescriptor.d.ts +23 -0
  102. package/dist/gltf/GltfMaterialEncoder.d.ts +20 -0
  103. package/dist/gltf/GltfMaterialLoader.d.ts +6 -0
  104. package/dist/gltf/GltfModelComponent.d.ts +72 -0
  105. package/dist/gltf/GltfModelSystem.d.ts +37 -0
  106. package/dist/gltf/GltfPlugin.d.ts +6 -0
  107. package/dist/gltf/GltfSchema.d.ts +225 -0
  108. package/dist/gltf/GltfUvSemanticPlanner.d.ts +33 -0
  109. package/dist/gltf/gltfLoader.d.ts +9 -0
  110. package/dist/gltf-animation3d.d.ts +3 -0
  111. package/dist/gltf-animation3d.js +8 -0
  112. package/dist/gltf-worker-runtime.d.ts +7 -0
  113. package/dist/gltf-worker-runtime.js +6981 -0
  114. package/dist/gltf.d.ts +12 -0
  115. package/dist/gltf.js +517 -0
  116. package/dist/grid/Grid2DComponent.d.ts +25 -0
  117. package/dist/grid.d.ts +2 -0
  118. package/dist/grid.js +56 -0
  119. package/dist/hya-state-machine.d.ts +7 -0
  120. package/dist/hya-state-machine.js +2934 -0
  121. package/dist/index.d.ts +2 -0
  122. package/dist/index.js +7 -0
  123. package/dist/internal/2d-ui-shader-artifact.js +870 -0
  124. package/dist/plugins/pluginUtils.d.ts +20 -0
  125. package/dist/plugins.d.ts +6 -0
  126. package/dist/shaders/generated/2d-ui-artifact.generated.d.ts +658 -0
  127. package/dist/spine/Spine2DComponent.d.ts +116 -0
  128. package/dist/spine/Spine2DGpuRenderer.d.ts +65 -0
  129. package/dist/spine/Spine2DRenderSystem.d.ts +27 -0
  130. package/dist/spine/Spine2DRuntime.d.ts +103 -0
  131. package/dist/spine/SpineAnimationBoneTimelines.d.ts +8 -0
  132. package/dist/spine/SpineAssetParser.d.ts +18 -0
  133. package/dist/spine/SpineAssetWorkerClient.d.ts +19 -0
  134. package/dist/spine/SpineAssetWorkerContract.d.ts +6 -0
  135. package/dist/spine/SpineAtlasParser.d.ts +21 -0
  136. package/dist/spine/SpineFloatBuilder.d.ts +15 -0
  137. package/dist/spine/SpinePathConstraintSolver.d.ts +32 -0
  138. package/dist/spine/SpinePlugin.d.ts +8 -0
  139. package/dist/spine/SpineSkeletonRuntime.d.ts +185 -0
  140. package/dist/spine/SpineSlotAnimation.d.ts +17 -0
  141. package/dist/spine/SpineTimelineRuntime.d.ts +25 -0
  142. package/dist/spine/SpineVertexBuilder.d.ts +61 -0
  143. package/dist/spine.d.ts +7 -0
  144. package/dist/spine.js +578 -0
  145. package/dist/tilemap/Tilemap2DComponent.d.ts +110 -0
  146. package/dist/tilemap/Tilemap2DRenderSystem.d.ts +11 -0
  147. package/dist/tilemap/Tilemap2DRenderer.d.ts +24 -0
  148. package/dist/tilemap/TilemapPlugin.d.ts +8 -0
  149. package/dist/tilemap.d.ts +6 -0
  150. package/dist/tilemap.js +535 -0
  151. package/dist/tween/Tween2DComponent.d.ts +70 -0
  152. package/dist/tween/Tween2DSystem.d.ts +12 -0
  153. package/dist/tween.d.ts +4 -0
  154. package/dist/tween.js +138 -0
  155. package/dist/utils/RenderSystem2DBase.d.ts +42 -0
  156. package/dist/utils/arrayAccess.d.ts +4 -0
  157. package/dist/utils/camera2d.d.ts +12 -0
  158. package/dist/utils/render2dGpu.d.ts +32 -0
  159. package/package.json +124 -0
  160. package/src/animation/Animation2DComponent.ts +135 -0
  161. package/src/animation/Animation2DExtensionRegistry.ts +52 -0
  162. package/src/animation/Animation2DRenderSystem.ts +1026 -0
  163. package/src/animation/Animation2DRuntime.ts +1326 -0
  164. package/src/animation/Animation2DStateMachine.ts +39 -0
  165. package/src/animation/Animation2DStateMachineComponent.ts +228 -0
  166. package/src/animation/Animation2DStateMachineRuntime.ts +141 -0
  167. package/src/animation/Animation2DStateMachineSystem.ts +60 -0
  168. package/src/animation/Animation2DStateMachineVisualRuntime.ts +282 -0
  169. package/src/animation/Animation2DSystem.ts +72 -0
  170. package/src/animation/AnimationAssetLoader.ts +84 -0
  171. package/src/animation/AnimationAudioClip.ts +140 -0
  172. package/src/animation/AnimationPathTessellator.ts +691 -0
  173. package/src/animation/AnimationTextRasterizer.ts +340 -0
  174. package/src/animation/AnimationVisual2D.ts +153 -0
  175. package/src/animation/HyaAnimation2DClipAdapter.ts +845 -0
  176. package/src/animation/Particle2DDescriptorAdapter.ts +35 -0
  177. package/src/animation/runtime/mixer/Action.ts +876 -0
  178. package/src/animation/runtime/mixer/Mixer.ts +505 -0
  179. package/src/animation/runtime/mixer/PoseBuffer.ts +105 -0
  180. package/src/animation/runtime/mixer/Sampler.ts +252 -0
  181. package/src/animation/runtime/mixer/Types.ts +138 -0
  182. package/src/animation/runtime/mixer/index.ts +31 -0
  183. package/src/animation-state-machine/AnimationStateMachine.ts +146 -0
  184. package/src/animation-state-machine/AnimationStateMachineChannels.ts +116 -0
  185. package/src/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.ts +197 -0
  186. package/src/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.ts +503 -0
  187. package/src/animation-state-machine/runtime/AnimationStateMachineCompiler.ts +763 -0
  188. package/src/animation-state-machine/runtime/AnimationStateMachineController.ts +987 -0
  189. package/src/animation-state-machine/runtime/AnimationStateMachineMixerPort.ts +54 -0
  190. package/src/animation-state-machine/runtime/index.ts +47 -0
  191. package/src/animation.ts +33 -0
  192. package/src/animation3d/Animation3DAction.ts +73 -0
  193. package/src/animation3d/Animation3DBinding.ts +116 -0
  194. package/src/animation3d/Animation3DClip.ts +22 -0
  195. package/src/animation3d/Animation3DError.ts +34 -0
  196. package/src/animation3d/Animation3DMixer.ts +82 -0
  197. package/src/animation3d/Animation3DPose.ts +100 -0
  198. package/src/animation3d/Animation3DPoseBuffer.ts +85 -0
  199. package/src/animation3d/Animation3DResource.ts +49 -0
  200. package/src/animation3d/Animation3DStateMachine.ts +126 -0
  201. package/src/animation3d/Animation3DStateMachineRuntime.ts +191 -0
  202. package/src/animation3d/Animation3DTrack.ts +29 -0
  203. package/src/animation3d/hya/HyaAnimation3DRuntime.ts +756 -0
  204. package/src/animation3d/hya/HyaAnimation3DStateMachineRuntime.ts +618 -0
  205. package/src/animation3d/hya/HyaAnimation3DTypes.ts +168 -0
  206. package/src/animation3d/hya/index.ts +10 -0
  207. package/src/animation3d/index.ts +76 -0
  208. package/src/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.ts +325 -0
  209. package/src/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.ts +158 -0
  210. package/src/animation3d/runtime/integration/index.ts +9 -0
  211. package/src/animation3d/runtime/mixer/Action.ts +833 -0
  212. package/src/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.ts +26 -0
  213. package/src/animation3d/runtime/mixer/Mixer.ts +562 -0
  214. package/src/animation3d/runtime/mixer/PoseBuffer.ts +1 -0
  215. package/src/animation3d/runtime/mixer/TrackSampler.ts +399 -0
  216. package/src/animation3d/runtime/mixer/index.ts +8 -0
  217. package/src/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.ts +4 -0
  218. package/src/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.ts +1 -0
  219. package/src/animation3d/runtime/state-machine/Animation3DStateMachineController.ts +1 -0
  220. package/src/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.ts +1 -0
  221. package/src/animation3d/runtime/state-machine/index.ts +45 -0
  222. package/src/animation3d.ts +5 -0
  223. package/src/benchmark.ts +168 -0
  224. package/src/canvas-text/CanvasText2DRenderSystem.ts +292 -0
  225. package/src/canvas-text/CanvasTextComponent.ts +47 -0
  226. package/src/canvas-text.ts +4 -0
  227. package/src/experimental-gltf-worker.ts +18 -0
  228. package/src/experimental-spine-worker.ts +9 -0
  229. package/src/gltf/GltfAccessorReader.ts +271 -0
  230. package/src/gltf/GltfAnimation3DAdapter.ts +680 -0
  231. package/src/gltf/GltfAnimationRuntime.ts +392 -0
  232. package/src/gltf/GltfAssetWorkerClient.ts +136 -0
  233. package/src/gltf/GltfCompatibilityReport.ts +214 -0
  234. package/src/gltf/GltfConservativeBounds.ts +219 -0
  235. package/src/gltf/GltfDracoDecoder.ts +251 -0
  236. package/src/gltf/GltfExtensionAdapter.ts +340 -0
  237. package/src/gltf/GltfLoaderContract.ts +333 -0
  238. package/src/gltf/GltfLoaderErrors.ts +59 -0
  239. package/src/gltf/GltfMaterialDescriptor.ts +134 -0
  240. package/src/gltf/GltfMaterialEncoder.ts +173 -0
  241. package/src/gltf/GltfMaterialLoader.ts +293 -0
  242. package/src/gltf/GltfModelComponent.ts +115 -0
  243. package/src/gltf/GltfModelSystem.ts +208 -0
  244. package/src/gltf/GltfPlugin.ts +84 -0
  245. package/src/gltf/GltfSchema.ts +229 -0
  246. package/src/gltf/GltfUvSemanticPlanner.ts +166 -0
  247. package/src/gltf/gltfLoader.ts +910 -0
  248. package/src/gltf-animation3d.ts +10 -0
  249. package/src/gltf-worker-runtime.ts +10 -0
  250. package/src/gltf.ts +56 -0
  251. package/src/grid/Grid2DComponent.ts +69 -0
  252. package/src/grid.ts +2 -0
  253. package/src/hya-state-machine.ts +19 -0
  254. package/src/index.ts +2 -0
  255. package/src/plugins/pluginUtils.ts +50 -0
  256. package/src/plugins.ts +6 -0
  257. package/src/shaders/generated/2d-ui-animation-2d.generated.wgsl +242 -0
  258. package/src/shaders/generated/2d-ui-artifact.generated.ts +867 -0
  259. package/src/shaders/generated/2d-ui-canvas-text-2d.generated.wgsl +38 -0
  260. package/src/shaders/generated/2d-ui-spine2d.generated.wgsl +41 -0
  261. package/src/shaders/generated/2d-ui-tilemap2d.generated.wgsl +36 -0
  262. package/src/spine/Spine2DComponent.ts +168 -0
  263. package/src/spine/Spine2DGpuRenderer.ts +449 -0
  264. package/src/spine/Spine2DRenderSystem.ts +199 -0
  265. package/src/spine/Spine2DRuntime.ts +1529 -0
  266. package/src/spine/SpineAnimationBoneTimelines.ts +30 -0
  267. package/src/spine/SpineAssetParser.ts +20 -0
  268. package/src/spine/SpineAssetWorkerClient.ts +112 -0
  269. package/src/spine/SpineAssetWorkerContract.ts +4 -0
  270. package/src/spine/SpineAtlasParser.ts +127 -0
  271. package/src/spine/SpineFloatBuilder.ts +66 -0
  272. package/src/spine/SpinePathConstraintSolver.ts +582 -0
  273. package/src/spine/SpinePlugin.ts +76 -0
  274. package/src/spine/SpineSkeletonRuntime.ts +521 -0
  275. package/src/spine/SpineSlotAnimation.ts +195 -0
  276. package/src/spine/SpineTimelineRuntime.ts +453 -0
  277. package/src/spine/SpineVertexBuilder.ts +686 -0
  278. package/src/spine.ts +7 -0
  279. package/src/tilemap/Tilemap2DComponent.ts +232 -0
  280. package/src/tilemap/Tilemap2DRenderSystem.ts +58 -0
  281. package/src/tilemap/Tilemap2DRenderer.ts +289 -0
  282. package/src/tilemap/TilemapPlugin.ts +75 -0
  283. package/src/tilemap.ts +6 -0
  284. package/src/tween/Tween2DComponent.ts +83 -0
  285. package/src/tween/Tween2DSystem.ts +78 -0
  286. package/src/tween.ts +4 -0
  287. package/src/types/earcut.d.ts +7 -0
  288. package/src/types/wgsl.d.ts +4 -0
  289. package/src/utils/RenderSystem2DBase.ts +164 -0
  290. package/src/utils/arrayAccess.ts +17 -0
  291. package/src/utils/camera2d.ts +68 -0
  292. package/src/utils/render2dGpu.ts +132 -0
  293. package/test/fixtures/gltf/animation-characterization.gltf +344 -0
@@ -0,0 +1,3245 @@
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