@haiyue/extensions 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +25 -0
  2. package/dist/animation/Animation2DComponent.d.ts +18 -0
  3. package/dist/animation/Animation2DExtensionRegistry.d.ts +7 -0
  4. package/dist/animation/Animation2DRenderSystem.d.ts +6 -0
  5. package/dist/animation/Animation2DRuntime.d.ts +6 -2
  6. package/dist/animation/AnimationTextRasterizer.d.ts +4 -0
  7. package/dist/animation/AnimationVisual2D.d.ts +58 -2
  8. package/dist/animation/interaction/InteractionRuntime.d.ts +83 -0
  9. package/dist/animation/interaction/runtime-types.d.ts +257 -0
  10. package/dist/animation.d.ts +3 -1
  11. package/dist/animation.js +551 -59
  12. package/dist/animation3d.js +1 -1
  13. package/dist/canvas-text.js +1 -1
  14. package/dist/chunks/2d-ui-animation-2d.generated-FpziAeGJ.js +4 -0
  15. package/dist/chunks/2d-ui-indexed-sprite.generated-DmKR0CC3.js +4 -0
  16. package/dist/chunks/Animation2DStateMachine-BHcA6Cu3.js +18212 -0
  17. package/dist/chunks/{AnimationStateMachineController-BIcAh-sT.js → AnimationStateMachineController-4sKaqh9E.js} +2 -2
  18. package/dist/chunks/AnimationVisual2D-8y3qp0_U.js +230 -0
  19. package/dist/chunks/{RenderSystem2DBase-DDT61dAu.js → RenderSystem2DBase-CF2s00vg.js} +2 -2
  20. package/dist/controls/VirtualJoystickControls.d.ts +46 -0
  21. package/dist/controls/contracts.d.ts +91 -0
  22. package/dist/controls.d.ts +2 -0
  23. package/dist/controls.js +358 -0
  24. package/dist/deformable-animation/runtime/DeformableMesh2DRuntimeExtension.d.ts +11 -0
  25. package/dist/deformable-animation/runtime/DeformableMesh2DSampler.d.ts +11 -0
  26. package/dist/deformable-animation.d.ts +5 -0
  27. package/dist/deformable-animation.js +316 -0
  28. package/dist/experimental-indexed-sprite.d.ts +5 -0
  29. package/dist/experimental-indexed-sprite.js +482 -0
  30. package/dist/gltf-worker-runtime.js +9 -9
  31. package/dist/hya-state-machine.js +3 -2
  32. package/dist/index.js +1 -1
  33. package/dist/indexed-sprite/AtlasLayout.d.ts +2 -0
  34. package/dist/indexed-sprite/IndexedSpriteRenderer.d.ts +23 -0
  35. package/dist/indexed-sprite/contracts.d.ts +89 -0
  36. package/dist/internal/2d-ui-shader-artifact.js +232 -23
  37. package/dist/ray-tracing/acceleration/bvh.d.ts +37 -0
  38. package/dist/ray-tracing/acceleration/index.d.ts +6 -0
  39. package/dist/ray-tracing/acceleration/pack.d.ts +152 -0
  40. package/dist/ray-tracing/acceleration/query.d.ts +4 -0
  41. package/dist/ray-tracing/acceleration/runtime.d.ts +30 -0
  42. package/dist/ray-tracing/acceleration/tlas.d.ts +4 -0
  43. package/dist/ray-tracing/acceleration/types.d.ts +165 -0
  44. package/dist/ray-tracing/denoise/index.d.ts +3 -0
  45. package/dist/ray-tracing/denoise/layout.d.ts +9 -0
  46. package/dist/ray-tracing/denoise/runtime.d.ts +23 -0
  47. package/dist/ray-tracing/denoise/types.d.ts +28 -0
  48. package/dist/ray-tracing/hybrid/contract.d.ts +2 -0
  49. package/dist/ray-tracing/hybrid/index.d.ts +5 -0
  50. package/dist/ray-tracing/hybrid/layout.d.ts +17 -0
  51. package/dist/ray-tracing/hybrid/plan.d.ts +2 -0
  52. package/dist/ray-tracing/hybrid/runtime.d.ts +31 -0
  53. package/dist/ray-tracing/hybrid/shaders/hybrid-artifact.generated.d.ts +625 -0
  54. package/dist/ray-tracing/hybrid/types.d.ts +144 -0
  55. package/dist/ray-tracing/lifecycle/device-recovery.d.ts +31 -0
  56. package/dist/ray-tracing/lifecycle/index.d.ts +2 -0
  57. package/dist/ray-tracing/lifecycle/types.d.ts +37 -0
  58. package/dist/ray-tracing/material/index.d.ts +2 -0
  59. package/dist/ray-tracing/material/pack.d.ts +5 -0
  60. package/dist/ray-tracing/material/types.d.ts +63 -0
  61. package/dist/ray-tracing/reference/index.d.ts +122 -0
  62. package/dist/ray-tracing/renderer/index.d.ts +7 -0
  63. package/dist/ray-tracing/renderer/layout.d.ts +20 -0
  64. package/dist/ray-tracing/renderer/plan.d.ts +2 -0
  65. package/dist/ray-tracing/renderer/reference.d.ts +17 -0
  66. package/dist/ray-tracing/renderer/runtime.d.ts +39 -0
  67. package/dist/ray-tracing/renderer/scene.d.ts +4 -0
  68. package/dist/ray-tracing/renderer/types.d.ts +115 -0
  69. package/dist/ray-tracing/sampling/index.d.ts +8 -0
  70. package/dist/ray-tracing/sampling/layout.d.ts +12 -0
  71. package/dist/ray-tracing/sampling/plan.d.ts +5 -0
  72. package/dist/ray-tracing/sampling/revision.d.ts +15 -0
  73. package/dist/ray-tracing/sampling/runtime.d.ts +44 -0
  74. package/dist/ray-tracing/sampling/sequence.d.ts +2 -0
  75. package/dist/ray-tracing/sampling/types.d.ts +96 -0
  76. package/dist/ray-tracing/scene/index.d.ts +41 -0
  77. package/dist/ray-tracing/traversal/index.d.ts +4 -0
  78. package/dist/ray-tracing/traversal/layout.d.ts +16 -0
  79. package/dist/ray-tracing/traversal/plan.d.ts +2 -0
  80. package/dist/ray-tracing/traversal/runtime.d.ts +25 -0
  81. package/dist/ray-tracing/traversal/types.d.ts +79 -0
  82. package/dist/ray-tracing/worker/client.d.ts +25 -0
  83. package/dist/ray-tracing/worker/index.d.ts +5 -0
  84. package/dist/ray-tracing/worker/runtime.d.ts +11 -0
  85. package/dist/ray-tracing/worker/serialization.d.ts +16 -0
  86. package/dist/ray-tracing/worker/source.d.ts +1 -0
  87. package/dist/ray-tracing/worker/types.d.ts +91 -0
  88. package/dist/ray-tracing.d.ts +17 -0
  89. package/dist/ray-tracing.js +7666 -0
  90. package/dist/spine.js +1 -1
  91. package/dist/tilemap.js +1 -1
  92. package/package.json +26 -23
  93. package/dist/animation/Animation2DStateMachineVisualRuntime.d.ts +0 -27
  94. package/dist/animation/Particle2DDescriptorAdapter.d.ts +0 -5
  95. package/dist/animation3d/Animation3DResource.d.ts +0 -28
  96. package/dist/animation3d/hya/HyaAnimation3DStateMachineRuntime.d.ts +0 -65
  97. package/dist/animation3d/hya/index.d.ts +0 -4
  98. package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.d.ts +0 -81
  99. package/dist/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.d.ts +0 -39
  100. package/dist/animation3d/runtime/integration/index.d.ts +0 -2
  101. package/dist/animation3d/runtime/mixer/Action.d.ts +0 -150
  102. package/dist/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.d.ts +0 -6
  103. package/dist/animation3d/runtime/mixer/Mixer.d.ts +0 -59
  104. package/dist/animation3d/runtime/mixer/PoseBuffer.d.ts +0 -1
  105. package/dist/animation3d/runtime/mixer/TrackSampler.d.ts +0 -24
  106. package/dist/animation3d/runtime/mixer/index.d.ts +0 -4
  107. package/dist/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.d.ts +0 -1
  108. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.d.ts +0 -1
  109. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineController.d.ts +0 -1
  110. package/dist/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.d.ts +0 -1
  111. package/dist/animation3d/runtime/state-machine/index.d.ts +0 -6
  112. package/dist/benchmark.d.ts +0 -30
  113. package/dist/chunks/2d-ui-animation-2d.generated-D4aHzFU2.js +0 -4
  114. package/dist/chunks/Animation2DStateMachine-9IonguNa.js +0 -3245
  115. package/dist/gltf/GltfAccessorReader.d.ts +0 -7
  116. package/dist/gltf/GltfDracoDecoder.d.ts +0 -4
  117. package/dist/gltf/GltfLoaderErrors.d.ts +0 -4
  118. package/dist/gltf/GltfMaterialDescriptor.d.ts +0 -23
  119. package/dist/gltf/GltfMaterialLoader.d.ts +0 -6
  120. package/dist/gltf-worker-runtime.d.ts +0 -7
  121. package/dist/plugins/pluginUtils.d.ts +0 -20
  122. package/dist/plugins.d.ts +0 -6
  123. package/dist/shaders/generated/2d-ui-artifact.generated.d.ts +0 -658
  124. package/dist/spine/Spine2DGpuRenderer.d.ts +0 -65
  125. package/dist/spine/Spine2DRuntime.d.ts +0 -103
  126. package/dist/spine/SpineAnimationBoneTimelines.d.ts +0 -8
  127. package/dist/spine/SpineFloatBuilder.d.ts +0 -15
  128. package/dist/spine/SpinePathConstraintSolver.d.ts +0 -32
  129. package/dist/spine/SpineSkeletonRuntime.d.ts +0 -185
  130. package/dist/spine/SpineSlotAnimation.d.ts +0 -17
  131. package/dist/spine/SpineTimelineRuntime.d.ts +0 -25
  132. package/dist/spine/SpineVertexBuilder.d.ts +0 -61
  133. package/dist/tilemap/Tilemap2DRenderer.d.ts +0 -24
  134. package/dist/utils/arrayAccess.d.ts +0 -4
  135. package/dist/utils/camera2d.d.ts +0 -12
  136. package/src/animation/Animation2DComponent.ts +0 -135
  137. package/src/animation/Animation2DExtensionRegistry.ts +0 -52
  138. package/src/animation/Animation2DRenderSystem.ts +0 -1026
  139. package/src/animation/Animation2DRuntime.ts +0 -1326
  140. package/src/animation/Animation2DStateMachine.ts +0 -39
  141. package/src/animation/Animation2DStateMachineComponent.ts +0 -228
  142. package/src/animation/Animation2DStateMachineRuntime.ts +0 -141
  143. package/src/animation/Animation2DStateMachineSystem.ts +0 -60
  144. package/src/animation/Animation2DStateMachineVisualRuntime.ts +0 -282
  145. package/src/animation/Animation2DSystem.ts +0 -72
  146. package/src/animation/AnimationAssetLoader.ts +0 -84
  147. package/src/animation/AnimationAudioClip.ts +0 -140
  148. package/src/animation/AnimationPathTessellator.ts +0 -691
  149. package/src/animation/AnimationTextRasterizer.ts +0 -340
  150. package/src/animation/AnimationVisual2D.ts +0 -153
  151. package/src/animation/HyaAnimation2DClipAdapter.ts +0 -845
  152. package/src/animation/Particle2DDescriptorAdapter.ts +0 -35
  153. package/src/animation/runtime/mixer/Action.ts +0 -876
  154. package/src/animation/runtime/mixer/Mixer.ts +0 -505
  155. package/src/animation/runtime/mixer/PoseBuffer.ts +0 -105
  156. package/src/animation/runtime/mixer/Sampler.ts +0 -252
  157. package/src/animation/runtime/mixer/Types.ts +0 -138
  158. package/src/animation/runtime/mixer/index.ts +0 -31
  159. package/src/animation-state-machine/AnimationStateMachine.ts +0 -146
  160. package/src/animation-state-machine/AnimationStateMachineChannels.ts +0 -116
  161. package/src/animation-state-machine/runtime/Animation2DStateMachineMixerAdapter.ts +0 -197
  162. package/src/animation-state-machine/runtime/AnimationStateMachineBlendTreeWeights.ts +0 -503
  163. package/src/animation-state-machine/runtime/AnimationStateMachineCompiler.ts +0 -763
  164. package/src/animation-state-machine/runtime/AnimationStateMachineController.ts +0 -987
  165. package/src/animation-state-machine/runtime/AnimationStateMachineMixerPort.ts +0 -54
  166. package/src/animation-state-machine/runtime/index.ts +0 -47
  167. package/src/animation.ts +0 -33
  168. package/src/animation3d/Animation3DAction.ts +0 -73
  169. package/src/animation3d/Animation3DBinding.ts +0 -116
  170. package/src/animation3d/Animation3DClip.ts +0 -22
  171. package/src/animation3d/Animation3DError.ts +0 -34
  172. package/src/animation3d/Animation3DMixer.ts +0 -82
  173. package/src/animation3d/Animation3DPose.ts +0 -100
  174. package/src/animation3d/Animation3DPoseBuffer.ts +0 -85
  175. package/src/animation3d/Animation3DResource.ts +0 -49
  176. package/src/animation3d/Animation3DStateMachine.ts +0 -126
  177. package/src/animation3d/Animation3DStateMachineRuntime.ts +0 -191
  178. package/src/animation3d/Animation3DTrack.ts +0 -29
  179. package/src/animation3d/hya/HyaAnimation3DRuntime.ts +0 -756
  180. package/src/animation3d/hya/HyaAnimation3DStateMachineRuntime.ts +0 -618
  181. package/src/animation3d/hya/HyaAnimation3DTypes.ts +0 -168
  182. package/src/animation3d/hya/index.ts +0 -10
  183. package/src/animation3d/index.ts +0 -76
  184. package/src/animation3d/runtime/integration/Animation3DStateMachineMixerAdapter.ts +0 -325
  185. package/src/animation3d/runtime/integration/Animation3DStateMachineMixerIntegration.ts +0 -158
  186. package/src/animation3d/runtime/integration/index.ts +0 -9
  187. package/src/animation3d/runtime/mixer/Action.ts +0 -833
  188. package/src/animation3d/runtime/mixer/Animation3DMixerRuntimeStore.ts +0 -26
  189. package/src/animation3d/runtime/mixer/Mixer.ts +0 -562
  190. package/src/animation3d/runtime/mixer/PoseBuffer.ts +0 -1
  191. package/src/animation3d/runtime/mixer/TrackSampler.ts +0 -399
  192. package/src/animation3d/runtime/mixer/index.ts +0 -8
  193. package/src/animation3d/runtime/state-machine/Animation3DBlendTreeWeights.ts +0 -4
  194. package/src/animation3d/runtime/state-machine/Animation3DStateMachineCompiler.ts +0 -1
  195. package/src/animation3d/runtime/state-machine/Animation3DStateMachineController.ts +0 -1
  196. package/src/animation3d/runtime/state-machine/Animation3DStateMachineMixerPort.ts +0 -1
  197. package/src/animation3d/runtime/state-machine/index.ts +0 -45
  198. package/src/animation3d.ts +0 -5
  199. package/src/benchmark.ts +0 -168
  200. package/src/canvas-text/CanvasText2DRenderSystem.ts +0 -292
  201. package/src/canvas-text/CanvasTextComponent.ts +0 -47
  202. package/src/canvas-text.ts +0 -4
  203. package/src/experimental-gltf-worker.ts +0 -18
  204. package/src/experimental-spine-worker.ts +0 -9
  205. package/src/gltf/GltfAccessorReader.ts +0 -271
  206. package/src/gltf/GltfAnimation3DAdapter.ts +0 -680
  207. package/src/gltf/GltfAnimationRuntime.ts +0 -392
  208. package/src/gltf/GltfAssetWorkerClient.ts +0 -136
  209. package/src/gltf/GltfCompatibilityReport.ts +0 -214
  210. package/src/gltf/GltfConservativeBounds.ts +0 -219
  211. package/src/gltf/GltfDracoDecoder.ts +0 -251
  212. package/src/gltf/GltfExtensionAdapter.ts +0 -340
  213. package/src/gltf/GltfLoaderContract.ts +0 -333
  214. package/src/gltf/GltfLoaderErrors.ts +0 -59
  215. package/src/gltf/GltfMaterialDescriptor.ts +0 -134
  216. package/src/gltf/GltfMaterialEncoder.ts +0 -173
  217. package/src/gltf/GltfMaterialLoader.ts +0 -293
  218. package/src/gltf/GltfModelComponent.ts +0 -115
  219. package/src/gltf/GltfModelSystem.ts +0 -208
  220. package/src/gltf/GltfPlugin.ts +0 -84
  221. package/src/gltf/GltfSchema.ts +0 -229
  222. package/src/gltf/GltfUvSemanticPlanner.ts +0 -166
  223. package/src/gltf/gltfLoader.ts +0 -910
  224. package/src/gltf-animation3d.ts +0 -10
  225. package/src/gltf-worker-runtime.ts +0 -10
  226. package/src/gltf.ts +0 -56
  227. package/src/grid/Grid2DComponent.ts +0 -69
  228. package/src/grid.ts +0 -2
  229. package/src/hya-state-machine.ts +0 -19
  230. package/src/index.ts +0 -2
  231. package/src/plugins/pluginUtils.ts +0 -50
  232. package/src/plugins.ts +0 -6
  233. package/src/shaders/generated/2d-ui-animation-2d.generated.wgsl +0 -242
  234. package/src/shaders/generated/2d-ui-artifact.generated.ts +0 -867
  235. package/src/shaders/generated/2d-ui-canvas-text-2d.generated.wgsl +0 -38
  236. package/src/shaders/generated/2d-ui-spine2d.generated.wgsl +0 -41
  237. package/src/shaders/generated/2d-ui-tilemap2d.generated.wgsl +0 -36
  238. package/src/spine/Spine2DComponent.ts +0 -168
  239. package/src/spine/Spine2DGpuRenderer.ts +0 -449
  240. package/src/spine/Spine2DRenderSystem.ts +0 -199
  241. package/src/spine/Spine2DRuntime.ts +0 -1529
  242. package/src/spine/SpineAnimationBoneTimelines.ts +0 -30
  243. package/src/spine/SpineAssetParser.ts +0 -20
  244. package/src/spine/SpineAssetWorkerClient.ts +0 -112
  245. package/src/spine/SpineAssetWorkerContract.ts +0 -4
  246. package/src/spine/SpineAtlasParser.ts +0 -127
  247. package/src/spine/SpineFloatBuilder.ts +0 -66
  248. package/src/spine/SpinePathConstraintSolver.ts +0 -582
  249. package/src/spine/SpinePlugin.ts +0 -76
  250. package/src/spine/SpineSkeletonRuntime.ts +0 -521
  251. package/src/spine/SpineSlotAnimation.ts +0 -195
  252. package/src/spine/SpineTimelineRuntime.ts +0 -453
  253. package/src/spine/SpineVertexBuilder.ts +0 -686
  254. package/src/spine.ts +0 -7
  255. package/src/tilemap/Tilemap2DComponent.ts +0 -232
  256. package/src/tilemap/Tilemap2DRenderSystem.ts +0 -58
  257. package/src/tilemap/Tilemap2DRenderer.ts +0 -289
  258. package/src/tilemap/TilemapPlugin.ts +0 -75
  259. package/src/tilemap.ts +0 -6
  260. package/src/tween/Tween2DComponent.ts +0 -83
  261. package/src/tween/Tween2DSystem.ts +0 -78
  262. package/src/tween.ts +0 -4
  263. package/src/types/earcut.d.ts +0 -7
  264. package/src/types/wgsl.d.ts +0 -4
  265. package/src/utils/RenderSystem2DBase.ts +0 -164
  266. package/src/utils/arrayAccess.ts +0 -17
  267. package/src/utils/camera2d.ts +0 -68
  268. package/src/utils/render2dGpu.ts +0 -132
  269. package/test/fixtures/gltf/animation-characterization.gltf +0 -344
package/dist/animation.js CHANGED
@@ -1,12 +1,13 @@
1
- import { parseAnimation, ANIMATION_MIME_TYPE } from '@haiyue/animation-spec';
1
+ import { parseAnimation, AnimationFormatError, ANIMATION_MIME_TYPE } from '@haiyue/animation-spec';
2
2
  import { Component, System } from '@haiyue/engine';
3
3
  import { UniqueCheckType, ComponentLifecycleFlags } from '@haiyue/engine/ecs';
4
- import { A as Animation2DRuntime, a as AnimationVisual2D } from './chunks/Animation2DStateMachine-9IonguNa.js';
5
- export { c as createAnimation2DStateMachineController, t as tessellateAnimationPath } from './chunks/Animation2DStateMachine-9IonguNa.js';
4
+ import { A as Animation2DRuntime } from './chunks/Animation2DStateMachine-BHcA6Cu3.js';
5
+ export { c as createAnimation2DStateMachineController, t as tessellateAnimationPath } from './chunks/Animation2DStateMachine-BHcA6Cu3.js';
6
6
  import { requireEngineDevice, getExtensionGPUResourceTracker, beginRenderCommandPass, cloneRenderPassDescriptor, alignUp4, estimateTextureBytes, getExtensionSharedRendererResource } from '@haiyue/engine/extension-authoring';
7
- import { R as RenderSystem2DBase, d as destroyCamera2DGpu, c as createCamera2DGpu, i as createCamera2DLayout } from './chunks/RenderSystem2DBase-DDT61dAu.js';
8
- import { A as Animation2dWgsl } from './chunks/2d-ui-animation-2d.generated-D4aHzFU2.js';
9
- export { A as AnimationStateMachineController, a as AnimationStateMachineValidationError, c as compileAnimationStateMachine, v as validateAnimationStateMachineDefinition } from './chunks/AnimationStateMachineController-BIcAh-sT.js';
7
+ import { R as RenderSystem2DBase, d as destroyCamera2DGpu, c as createCamera2DGpu, i as createCamera2DLayout } from './chunks/RenderSystem2DBase-CF2s00vg.js';
8
+ import { A as Animation2dWgsl } from './chunks/2d-ui-animation-2d.generated-FpziAeGJ.js';
9
+ import { A as AnimationVisual2D, c as createAnimation2DPipeline, a as animation2DCullingPipelineKey, b as animation2DCullingPrimitive } from './chunks/AnimationVisual2D-8y3qp0_U.js';
10
+ export { A as AnimationStateMachineController, b as AnimationStateMachineValidationError, a as compileAnimationStateMachine, d as validateAnimationStateMachineDefinition } from './chunks/AnimationStateMachineController-4sKaqh9E.js';
10
11
  import '@haiyue/engine/geometry';
11
12
  import '@haiyue/engine/components';
12
13
  import 'wgpu-matrix';
@@ -18,10 +19,13 @@ class Animation2DComponent extends Component {
18
19
  animation;
19
20
  playing;
20
21
  loop;
22
+ loopStartTime;
23
+ loopEndTime;
21
24
  speed;
22
25
  currentTime;
23
26
  completed = false;
24
27
  runtimeExtensions;
28
+ _nodeOverrides = new Map();
25
29
  _runtime = null;
26
30
  _needsApply = true;
27
31
  _forceParticleSeek = false;
@@ -30,6 +34,8 @@ class Animation2DComponent extends Component {
30
34
  this.animation = isParsed(source) ? source : parseAnimation(source);
31
35
  this.playing = options.autoplay ?? true;
32
36
  this.loop = options.loop ?? this.animation.endBehavior === 'loop';
37
+ this.loopStartTime = normalizeLoopStartTime(options.loopStartTime ?? 0, this.animation.duration);
38
+ this.loopEndTime = normalizeLoopEndTime(options.loopEndTime ?? this.animation.duration, this.loopStartTime, this.animation.duration);
33
39
  this.speed = finiteSpeed(options.speed ?? 1);
34
40
  this.currentTime = clampTime(options.startTime ?? 0, this.animation.duration);
35
41
  this.runtimeExtensions = options.runtimeExtensions;
@@ -73,6 +79,39 @@ class Animation2DComponent extends Component {
73
79
  this.speed = finiteSpeed(speed);
74
80
  return this;
75
81
  }
82
+ setNodeOverride(nodeId, override) {
83
+ if (!this.animation.nodes.some(node => node.id === nodeId))
84
+ throw new ReferenceError(`Animation node "${nodeId}" does not exist.`);
85
+ const position = override.position;
86
+ if (position && (position.length !== 2 || position.some(value => !Number.isFinite(value)))) {
87
+ throw new RangeError('Animation2D node override position must contain two finite values.');
88
+ }
89
+ if (override.opacity !== undefined && (!Number.isFinite(override.opacity) || override.opacity < 0 || override.opacity > 1)) {
90
+ throw new RangeError('Animation2D node override opacity must be within 0..1.');
91
+ }
92
+ if (override.enabled !== undefined && typeof override.enabled !== 'boolean') {
93
+ throw new TypeError('Animation2D node override enabled must be boolean.');
94
+ }
95
+ this._nodeOverrides.set(nodeId, Object.freeze({
96
+ ...(position ? { position: Object.freeze([position[0], position[1]]) } : {}),
97
+ ...(override.opacity === undefined ? {} : { opacity: override.opacity }),
98
+ ...(override.enabled === undefined ? {} : { enabled: override.enabled }),
99
+ }));
100
+ this._needsApply = true;
101
+ return this;
102
+ }
103
+ clearNodeOverride(nodeId) {
104
+ if (this._nodeOverrides.delete(nodeId))
105
+ this._needsApply = true;
106
+ return this;
107
+ }
108
+ clearNodeOverrides() {
109
+ if (this._nodeOverrides.size > 0) {
110
+ this._nodeOverrides.clear();
111
+ this._needsApply = true;
112
+ }
113
+ return this;
114
+ }
76
115
  onEntityRemoveComponent(_entity, component) {
77
116
  if (component === this)
78
117
  this._disposeRuntime();
@@ -84,11 +123,15 @@ class Animation2DComponent extends Component {
84
123
  const clone = new Animation2DComponent(this.animation, {
85
124
  autoplay: this.playing,
86
125
  loop: this.loop,
126
+ loopStartTime: this.loopStartTime,
127
+ loopEndTime: this.loopEndTime,
87
128
  speed: this.speed,
88
129
  startTime: this.currentTime,
89
130
  ...(this.runtimeExtensions ? { runtimeExtensions: this.runtimeExtensions } : {}),
90
131
  });
91
132
  clone.disabled = this.disabled;
133
+ for (const [nodeId, override] of this._nodeOverrides)
134
+ clone.setNodeOverride(nodeId, override);
92
135
  return clone;
93
136
  }
94
137
  _disposeRuntime() {
@@ -109,6 +152,254 @@ function clampTime(value, duration) {
109
152
  throw new RangeError('Animation2DComponent time must be finite.');
110
153
  return Math.max(0, Math.min(duration, value));
111
154
  }
155
+ function normalizeLoopStartTime(value, duration) {
156
+ if (!Number.isFinite(value) || value < 0 || value >= duration) {
157
+ throw new RangeError('Animation2DComponent loopStartTime must be finite, non-negative, and less than duration.');
158
+ }
159
+ return value;
160
+ }
161
+ function normalizeLoopEndTime(value, loopStartTime, duration) {
162
+ if (!Number.isFinite(value) || value <= loopStartTime || value > duration) {
163
+ throw new RangeError('Animation2DComponent loopEndTime must be finite, greater than loopStartTime, and no greater than duration.');
164
+ }
165
+ return value;
166
+ }
167
+
168
+ class InteractionRuntimeError extends Error {
169
+ code;
170
+ path;
171
+ name = 'InteractionRuntimeError';
172
+ constructor(code, path, message) {
173
+ super(`${message} (${path})`);
174
+ this.code = code;
175
+ this.path = path;
176
+ }
177
+ }
178
+
179
+ class InteractionRuntime {
180
+ document;
181
+ options;
182
+ targets;
183
+ listeners;
184
+ queue = [];
185
+ pointers = new Map();
186
+ captures = new Map();
187
+ focused;
188
+ sequence = 0;
189
+ transactionSequence = 0;
190
+ draining = false;
191
+ currentDepth = 0;
192
+ disposed = false;
193
+ maxEventQueue;
194
+ maxEventRecursion;
195
+ maxPointers;
196
+ maxStringBytes;
197
+ constructor(document, options = {}) {
198
+ this.document = document;
199
+ this.options = options;
200
+ this.targets = new Map(document.targets.map(target => [target.id, target]));
201
+ this.listeners = new Map();
202
+ for (const listener of document.listeners) {
203
+ const values = this.listeners.get(listener.target) ?? [];
204
+ values.push(listener);
205
+ this.listeners.set(listener.target, values);
206
+ }
207
+ this.maxEventQueue = options.maxEventQueue ?? document.limits?.maxEventQueue ?? 1_000_000;
208
+ this.maxEventRecursion = options.maxEventRecursion ?? document.limits?.maxEventRecursion ?? 64;
209
+ this.maxPointers = options.maxPointers ?? document.limits?.maxPointers ?? 1_024;
210
+ this.maxStringBytes = options.maxStringBytes ?? 4 * 1024 * 1024;
211
+ }
212
+ dispatchPointer(input) {
213
+ this.assertLive();
214
+ if (!Number.isSafeInteger(input.pointerId) || input.pointerId < 0 || input.point.some(value => !Number.isFinite(value)))
215
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_LIMIT', '$.pointer', 'invalid pointer input');
216
+ let state = this.pointers.get(input.pointerId);
217
+ if (!state) {
218
+ if (this.pointers.size >= this.maxPointers)
219
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_LIMIT', '$.pointers', 'pointer budget exceeded');
220
+ state = { point: input.point, hover: undefined, down: undefined, downPoint: undefined, dragging: false };
221
+ this.pointers.set(input.pointerId, state);
222
+ }
223
+ const hit = this.captures.get(input.pointerId) ?? this.hitTest(input.point), previousHover = state.hover;
224
+ state.point = input.point;
225
+ if (input.kind === 'move' && previousHover !== hit) {
226
+ if (previousHover)
227
+ this.enqueue({ kind: 'pointer-exit', target: previousHover, depth: 0, pointerId: input.pointerId, point: input.point });
228
+ if (hit)
229
+ this.enqueue({ kind: 'pointer-enter', target: hit, depth: 0, pointerId: input.pointerId, point: input.point });
230
+ state.hover = hit;
231
+ }
232
+ if (input.kind === 'down') {
233
+ state.down = hit;
234
+ state.downPoint = input.point;
235
+ state.dragging = false;
236
+ if (hit) {
237
+ this.enqueue({ kind: 'pointer-down', target: hit, depth: 0, pointerId: input.pointerId, point: input.point, button: input.button ?? 0 });
238
+ this.focusFromPointer(hit);
239
+ }
240
+ }
241
+ else if (input.kind === 'move') {
242
+ if (hit)
243
+ this.enqueue({ kind: 'pointer-move', target: hit, depth: 0, pointerId: input.pointerId, point: input.point });
244
+ if (state.down && state.downPoint) {
245
+ const distance = Math.hypot(input.point[0] - state.downPoint[0], input.point[1] - state.downPoint[1]);
246
+ if (!state.dragging && distance >= this.document.dragThreshold) {
247
+ state.dragging = true;
248
+ this.enqueue({ kind: 'drag-start', target: state.down, depth: 0, pointerId: input.pointerId, point: input.point });
249
+ }
250
+ if (state.dragging)
251
+ this.enqueue({ kind: 'drag', target: state.down, depth: 0, pointerId: input.pointerId, point: input.point });
252
+ }
253
+ }
254
+ else {
255
+ if (hit)
256
+ this.enqueue({ kind: 'pointer-up', target: hit, depth: 0, pointerId: input.pointerId, point: input.point, button: input.button ?? 0 });
257
+ if (state.down) {
258
+ if (state.dragging)
259
+ this.enqueue({ kind: 'drag-end', target: state.down, depth: 0, pointerId: input.pointerId, point: input.point });
260
+ else if (hit === state.down)
261
+ this.enqueue({ kind: 'click', target: state.down, depth: 0, pointerId: input.pointerId, point: input.point, button: input.button ?? 0 });
262
+ }
263
+ state.down = undefined;
264
+ state.downPoint = undefined;
265
+ state.dragging = false;
266
+ this.captures.delete(input.pointerId);
267
+ }
268
+ this.drain();
269
+ }
270
+ dispatchKeyboard(input) { this.assertLive(); if (!this.focused)
271
+ return; this.enqueue({ kind: 'keyboard', target: this.focused, depth: 0, key: input.key, keyPhase: input.phase, modifiers: input.modifiers ?? [] }); this.drain(); }
272
+ dispatchText(text) { this.assertLive(); this.stringInput(text, '$.text'); if (!this.focused)
273
+ return; this.enqueue({ kind: 'text-input', target: this.focused, depth: 0, text }); this.drain(); }
274
+ dispatchDataChange(binding, value, target = this.focused) { this.assertLive(); this.stringInput(binding, '$.binding'); if (!target)
275
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', binding, 'data change requires a target'); this.enqueue({ kind: 'data-change', target, depth: 0, binding, value }); this.drain(); }
276
+ dispatchGamepad(input) { this.assertLive(); if (!this.focused)
277
+ return; this.enqueue({ kind: 'gamepad', target: this.focused, depth: 0, gamepad: input }); this.drain(); }
278
+ dispatchSemanticAction(target, action) { this.assertLive(); this.target(target); this.enqueue({ kind: 'semantic-action', target, depth: 0, semanticAction: action }); this.drain(); }
279
+ enqueueReportedEvent(name, payload = null, target = this.focused) { this.assertLive(); this.stringInput(name, '$.event.name'); if (!target)
280
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', name, 'reported event requires a target'); this.enqueue({ kind: 'reported-event', target, depth: this.draining ? this.currentDepth + 1 : 0, name, payload }); if (!this.draining)
281
+ this.drain(); }
282
+ enqueueComponentEvent(target, name, payload = null) { this.assertLive(); this.stringInput(name, '$.componentEvent.name'); this.enqueue({ kind: 'component-event', target, depth: this.draining ? this.currentDepth + 1 : 0, name, payload }); if (!this.draining)
283
+ this.drain(); }
284
+ focus(target) { this.assertLive(); if (target !== undefined) {
285
+ const item = this.target(target);
286
+ if (!item.focusable || item.enabled === false)
287
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', target, 'target is not focusable');
288
+ } if (this.focused === target)
289
+ return; const previous = this.focused; this.focused = target; if (previous)
290
+ this.enqueue({ kind: 'blur', target: previous, depth: this.draining ? this.currentDepth + 1 : 0 }); if (target)
291
+ this.enqueue({ kind: 'focus', target, depth: this.draining ? this.currentDepth + 1 : 0 }); if (!this.draining)
292
+ this.drain(); }
293
+ focusNext(reverse = false) { const ordered = [...this.targets.values()].filter(target => target.focusable && target.enabled !== false).sort((a, b) => (a.tabIndex ?? 0) - (b.tabIndex ?? 0) || a.order - b.order || a.id.localeCompare(b.id)); if (ordered.length === 0) {
294
+ this.focus(undefined);
295
+ return undefined;
296
+ } const current = ordered.findIndex(target => target.id === this.focused), delta = reverse ? -1 : 1, index = (current + delta + ordered.length) % ordered.length; this.focus(ordered[index].id); return this.focused; }
297
+ hitTest(point) { this.assertLive(); return [...this.targets.values()].filter(target => target.enabled !== false && this.options.geometryPort?.visible?.(target.id) !== false).sort((a, b) => b.order - a.order || b.id.localeCompare(a.id)).find(target => this.containsTarget(target, point, true))?.id; }
298
+ dispose() { if (this.disposed)
299
+ return; this.disposed = true; this.queue.length = 0; this.pointers.clear(); this.captures.clear(); this.focused = undefined; this.options.actionPort?.dispose?.(); }
300
+ get trace() { return Object.freeze({ sequence: this.sequence, focused: this.focused ?? null, captures: Object.freeze([...this.captures].map(([pointer, target]) => Object.freeze({ pointer, target }))), pointers: this.pointers.size, queued: this.queue.length, disposed: this.disposed }); }
301
+ enqueue(event) { if (event.depth > this.maxEventRecursion)
302
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_LIMIT', '$.events', 'event recursion budget exceeded'); if (this.queue.length >= this.maxEventQueue)
303
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_LIMIT', '$.events', 'event queue budget exceeded'); this.target(event.target); this.queue.push(event); }
304
+ drain() { if (this.draining)
305
+ return; this.draining = true; try {
306
+ while (this.queue.length) {
307
+ const event = this.queue.shift();
308
+ this.currentDepth = event.depth;
309
+ this.route(event);
310
+ }
311
+ }
312
+ finally {
313
+ this.currentDepth = 0;
314
+ this.draining = false;
315
+ } }
316
+ route(event) { const path = this.path(event.target), transactionId = ++this.transactionSequence, snapshot = this.snapshot(); const actionPort = this.options.actionPort; actionPort?.begin(transactionId); try {
317
+ for (const current of path.slice(0, -1))
318
+ this.invokeListeners(current, 'capture', event, transactionId);
319
+ this.invokeListeners(event.target, 'target', event, transactionId);
320
+ for (const current of path.slice(0, -1).reverse())
321
+ this.invokeListeners(current, 'bubble', event, transactionId);
322
+ actionPort?.commit(transactionId);
323
+ }
324
+ catch (error) {
325
+ this.restore(snapshot);
326
+ actionPort?.rollback(transactionId);
327
+ throw error;
328
+ } }
329
+ invokeListeners(currentTarget, phase, event, transactionId) { const listeners = this.listeners.get(currentTarget) ?? []; for (const listener of listeners) {
330
+ if (listener.event !== event.kind || !listener.phases.includes(phase) || !this.matches(listener, event))
331
+ continue;
332
+ const routed = Object.freeze({ sequence: ++this.sequence, kind: event.kind, phase, target: event.target, currentTarget, ...(event.pointerId === undefined ? {} : { pointerId: event.pointerId }), ...(event.point === undefined ? {} : { point: event.point, localPoint: this.toLocal(currentTarget, event.point) }), ...(event.button === undefined ? {} : { button: event.button }), ...(event.key === undefined ? {} : { key: event.key }), ...(event.keyPhase === undefined ? {} : { keyPhase: event.keyPhase }), ...(event.modifiers === undefined ? {} : { modifiers: event.modifiers }), ...(event.text === undefined ? {} : { text: event.text }), ...(event.binding === undefined ? {} : { binding: event.binding }), ...(event.value === undefined ? {} : { value: event.value }), ...(event.gamepad === undefined ? {} : { gamepad: event.gamepad }), ...(event.semanticAction === undefined ? {} : { semanticAction: event.semanticAction }), ...(event.name === undefined ? {} : { name: event.name }), ...(event.payload === undefined ? {} : { payload: event.payload }) });
333
+ for (const action of listener.actions)
334
+ this.invokeAction(action, routed);
335
+ } }
336
+ invokeAction(action, event) { if (action.kind === 'pointer-capture') {
337
+ if (event.pointerId === undefined)
338
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', event.currentTarget, 'pointer capture requires a pointer event');
339
+ this.captures.set(event.pointerId, event.currentTarget);
340
+ }
341
+ else if (action.kind === 'pointer-release') {
342
+ if (event.pointerId !== undefined)
343
+ this.captures.delete(event.pointerId);
344
+ }
345
+ else if (action.kind === 'focus')
346
+ this.focus(action.target ?? event.currentTarget); const port = this.options.actionPort; if (!port && !['pointer-capture', 'pointer-release', 'focus'].includes(action.kind))
347
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', event.currentTarget, `action port is required for ${action.kind}`); port?.invoke(action, event); }
348
+ matches(listener, event) { if (listener.pointerButton !== undefined && listener.pointerButton !== event.button)
349
+ return false; if (listener.key !== undefined && listener.key !== event.key)
350
+ return false; if (listener.keyPhase !== undefined && listener.keyPhase !== event.keyPhase)
351
+ return false; if (listener.modifiers && !listener.modifiers.every(modifier => event.modifiers?.includes(modifier)))
352
+ return false; if (listener.gamepad && (!event.gamepad || (listener.gamepad.index !== undefined && listener.gamepad.index !== event.gamepad.index) || listener.gamepad.control !== event.gamepad.control || listener.gamepad.phase !== event.gamepad.phase))
353
+ return false; if (listener.semanticAction !== undefined && listener.semanticAction !== event.semanticAction)
354
+ return false; return true; }
355
+ containsTarget(target, point, clips) { const local = this.toLocal(target.id, point); if (clips && target.clips?.some(id => !this.containsTarget(this.target(id), point, false)))
356
+ return false; const area = target.hitArea, kind = area.kind; if (kind === 'rect') {
357
+ const [x, y, width, height] = area.rect;
358
+ return local[0] >= x && local[1] >= y && local[0] <= x + width && local[1] <= y + height;
359
+ } if (kind === 'ellipse') {
360
+ const center = area.center, radius = area.radius;
361
+ if (radius[0] === 0 || radius[1] === 0)
362
+ return false;
363
+ return ((local[0] - center[0]) / radius[0]) ** 2 + ((local[1] - center[1]) / radius[1]) ** 2 <= 1;
364
+ } if (kind === 'polygon')
365
+ return pointInPolygon(local, area.points); if (kind === 'geometry') {
366
+ const port = this.options.geometryPort?.containsGeometry;
367
+ if (!port)
368
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', target.id, 'geometry hit area requires a geometry port');
369
+ return port(area.port, target.id, local);
370
+ } return false; }
371
+ toLocal(id, point) { const chain = this.path(id); let matrix = [1, 0, 0, 1, 0, 0]; for (const targetId of chain) {
372
+ const target = this.target(targetId), authored = target.transform, dynamic = this.options.geometryPort?.matrix?.(targetId);
373
+ if (authored)
374
+ matrix = multiply(matrix, authored);
375
+ if (dynamic)
376
+ matrix = multiply(matrix, dynamic);
377
+ } return transform(inverse(matrix), point); }
378
+ path(id) { const result = []; let current = this.target(id); while (current) {
379
+ result.unshift(current.id);
380
+ current = current.parent ? this.target(current.parent) : undefined;
381
+ } return result; }
382
+ focusFromPointer(id) { let target = this.target(id); while (target && !target.focusable)
383
+ target = target.parent ? this.target(target.parent) : undefined; if (target?.focusable)
384
+ this.focus(target.id); }
385
+ target(id) { const target = this.targets.get(id); if (!target)
386
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', id, 'unknown interaction target'); return target; }
387
+ snapshot() { return { pointers: new Map([...this.pointers].map(([id, state]) => [id, { ...state }])), captures: new Map(this.captures), focus: this.focused, queue: [...this.queue] }; }
388
+ restore(snapshot) { this.pointers = snapshot.pointers; this.captures = snapshot.captures; this.focused = snapshot.focus; this.queue.splice(0, this.queue.length, ...snapshot.queue); }
389
+ stringInput(value, path) { if (typeof value !== 'string' || new TextEncoder().encode(value).length > this.maxStringBytes)
390
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_LIMIT', path, 'string input exceeds budget'); }
391
+ assertLive() { if (this.disposed)
392
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', '$', 'interaction runtime is disposed'); }
393
+ }
394
+ function multiply(a, b) { return [a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1], a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3], a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5]]; }
395
+ function inverse(value) { const determinant = value[0] * value[3] - value[1] * value[2]; if (Math.abs(determinant) < 1e-12)
396
+ throw new InteractionRuntimeError('E_INTERACTION_RUNTIME_PORT', '$.transform', 'interaction transform is singular'); return [value[3] / determinant, -value[1] / determinant, -value[2] / determinant, value[0] / determinant, (value[2] * value[5] - value[3] * value[4]) / determinant, (value[1] * value[4] - value[0] * value[5]) / determinant]; }
397
+ function transform(matrix, point) { return [matrix[0] * point[0] + matrix[2] * point[1] + matrix[4], matrix[1] * point[0] + matrix[3] * point[1] + matrix[5]]; }
398
+ function pointInPolygon(point, points) { let inside = false; for (let index = 0, previous = points.length - 1; index < points.length; previous = index++) {
399
+ const a = points[index], b = points[previous], crosses = a[1] > point[1] !== b[1] > point[1] && point[0] < (b[0] - a[0]) * (point[1] - a[1]) / (b[1] - a[1]) + a[0];
400
+ if (crosses)
401
+ inside = !inside;
402
+ } return inside; }
112
403
 
113
404
  class Animation2DSystem extends System {
114
405
  _assetManager;
@@ -149,12 +440,15 @@ class Animation2DSystem extends System {
149
440
  }
150
441
  if (component.completed && component.animation.endBehavior === 'destroy' && !component.playing)
151
442
  return;
152
- component._runtime ??= new Animation2DRuntime(entity, component.animation, component.runtimeExtensions, this._assetManager);
443
+ component._runtime ??= new Animation2DRuntime(entity, component.animation, component.runtimeExtensions, this._assetManager, component._nodeOverrides);
153
444
  if (component.playing && delta > 0 && component.speed > 0) {
154
445
  component.currentTime += delta * 0.001 * component.speed;
155
- if (component.currentTime >= component.animation.duration) {
446
+ const playbackEnd = component.loop ? component.loopEndTime : component.animation.duration;
447
+ if (component.currentTime >= playbackEnd) {
156
448
  if (component.loop) {
157
- component.currentTime %= component.animation.duration;
449
+ const loopDuration = component.loopEndTime - component.loopStartTime;
450
+ component.currentTime = component.loopStartTime
451
+ + ((component.currentTime - component.loopEndTime) % loopDuration);
158
452
  component._forceParticleSeek = true;
159
453
  }
160
454
  else {
@@ -178,9 +472,93 @@ class Animation2DSystem extends System {
178
472
  }
179
473
  }
180
474
 
475
+ /** 256 MiB of rgba8 mask targets before allocator/tracker overhead. */
476
+ const ANIMATION_2D_MAX_MASK_PIXELS = 64 * 1024 * 1024;
477
+ function animationMaskTargetKey(viewKey, sourceKey) {
478
+ return `${viewKey.length}:${viewKey}${sourceKey.length}:${sourceKey}`;
479
+ }
480
+ function animationMaskCompositeKey(viewKey, sourceKeys) {
481
+ return `${viewKey.length}:${viewKey}${sourceKeys.map(key => `${key.length}:${key}`).join('')}`;
482
+ }
483
+ /** Classifies every mask allocation limit before any GPU target is created. */
484
+ function assertAnimationMaskBudget(input) {
485
+ const basePath = `$runtime.views[${JSON.stringify(input.viewKey)}]`;
486
+ if (!Number.isSafeInteger(input.groupCount) || input.groupCount < 0) {
487
+ throw new AnimationFormatError('E_ANIMATION_INVALID_FORMAT', 'Mask group count must be a non-negative safe integer.', `${basePath}.maskGroups`);
488
+ }
489
+ if (input.groupCount > input.maxGroupCount) {
490
+ throw new AnimationFormatError('E_ANIMATION_LIMIT_EXCEEDED', `Mask group count ${input.groupCount} exceeds limit ${input.maxGroupCount}.`, `${basePath}.maskGroups`);
491
+ }
492
+ if (!Number.isSafeInteger(input.width) || input.width <= 0 || !Number.isSafeInteger(input.height) || input.height <= 0) {
493
+ throw new AnimationFormatError('E_ANIMATION_INVALID_FORMAT', 'Mask target dimensions must be positive safe integers.', `${basePath}.maskTexture`);
494
+ }
495
+ if (input.width > input.maxTextureDimension2D || input.height > input.maxTextureDimension2D) {
496
+ throw new AnimationFormatError('E_ANIMATION_LIMIT_EXCEEDED', `Mask target ${input.width}x${input.height} exceeds device maxTextureDimension2D ${input.maxTextureDimension2D}.`, `${basePath}.maskTexture`);
497
+ }
498
+ const pixels = input.width * input.height * input.groupCount;
499
+ const maxPixels = input.maxPixels ?? ANIMATION_2D_MAX_MASK_PIXELS;
500
+ if (!Number.isSafeInteger(pixels) || pixels > maxPixels) {
501
+ throw new AnimationFormatError('E_ANIMATION_LIMIT_EXCEEDED', `Mask target pixels ${pixels} exceed aggregate limit ${maxPixels}.`, `${basePath}.maskPixels`);
502
+ }
503
+ }
504
+
505
+ /** Official Cubism 5.2-and-earlier blend factors for premultiplied source colors. */
506
+ function animationBlendState(mode) {
507
+ if (mode === 'additive')
508
+ return {
509
+ color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
510
+ alpha: { srcFactor: 'zero', dstFactor: 'one', operation: 'add' },
511
+ };
512
+ if (mode === 'multiplicative')
513
+ return {
514
+ color: { srcFactor: 'dst', dstFactor: 'one-minus-src-alpha', operation: 'add' },
515
+ alpha: { srcFactor: 'zero', dstFactor: 'one', operation: 'add' },
516
+ };
517
+ if (mode === 'screen')
518
+ return {
519
+ color: { srcFactor: 'one', dstFactor: 'one-minus-src', operation: 'add' },
520
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
521
+ };
522
+ return {
523
+ color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
524
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
525
+ };
526
+ }
527
+ function animationBlendFragmentEntryPoint(mode, textureAlphaMode = 'straight') {
528
+ return textureAlphaMode === 'premultiplied' ? 'fs_main_premultiplied_texture' : 'fs_main';
529
+ }
530
+
181
531
  const SHADER = Animation2dWgsl;
532
+ const TEXT_MIPMAP_SHADER = /* wgsl */ `
533
+ struct VertexOutput {
534
+ @builtin(position) position: vec4f,
535
+ @location(0) uv: vec2f,
536
+ }
537
+
538
+ @group(0) @binding(0) var sourceTexture: texture_2d<f32>;
539
+ @group(0) @binding(1) var sourceSampler: sampler;
540
+
541
+ @vertex
542
+ fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
543
+ let positions = array(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
544
+ let uvs = array(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
545
+ var output: VertexOutput;
546
+ output.position = vec4f(positions[vertexIndex], 0.0, 1.0);
547
+ output.uv = uvs[vertexIndex];
548
+ return output;
549
+ }
550
+
551
+ @fragment
552
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
553
+ return textureSample(sourceTexture, sourceSampler, input.uv);
554
+ }
555
+ `;
182
556
  const MAX_COMPOSITE_LAYERS = 8;
183
- const COMPOSITE_PARAMS_OFFSET = 28;
557
+ const MULTIPLY_COLOR_OFFSET = 20;
558
+ const SCREEN_COLOR_OFFSET = 24;
559
+ const PARAMS_OFFSET = 28;
560
+ const UV_RECT_OFFSET = 32;
561
+ const COMPOSITE_PARAMS_OFFSET = 36;
184
562
  const COMPOSITE_EXPANSION_OFFSET = COMPOSITE_PARAMS_OFFSET + MAX_COMPOSITE_LAYERS * 4;
185
563
  const GRADIENT_PARAMS_OFFSET = COMPOSITE_EXPANSION_OFFSET + MAX_COMPOSITE_LAYERS;
186
564
  const GRADIENT_GEOMETRY_OFFSET = GRADIENT_PARAMS_OFFSET + 4;
@@ -193,7 +571,7 @@ const MAX_EFFECTS = 8;
193
571
  const OBJECT_FLOATS = EFFECT_DATA_OFFSET + MAX_EFFECTS * EFFECT_FLOATS;
194
572
  const OBJECT_BYTES = OBJECT_FLOATS * 4;
195
573
  function getAnimation2DSharedGpu(device, tracker) {
196
- return getExtensionSharedRendererResource(device, 'Animation2D.sharedGpu:v1', () => createAnimation2DSharedGpu(device, tracker));
574
+ return getExtensionSharedRendererResource(device, 'Animation2D.sharedGpu:v2', () => createAnimation2DSharedGpu(device, tracker));
197
575
  }
198
576
  function createAnimation2DSharedGpu(device, tracker) {
199
577
  const cameraLayout = createCamera2DLayout(device);
@@ -216,7 +594,21 @@ function createAnimation2DSharedGpu(device, tracker) {
216
594
  })),
217
595
  { binding: MAX_COMPOSITE_LAYERS, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
218
596
  ] });
219
- const sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
597
+ const sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear', mipmapFilter: 'linear' });
598
+ const mipmapSampler = device.createSampler({
599
+ magFilter: 'linear',
600
+ minFilter: 'linear',
601
+ addressModeU: 'clamp-to-edge',
602
+ addressModeV: 'clamp-to-edge',
603
+ });
604
+ const mipmapShader = device.createShaderModule({ label: 'Animation2D.textMipmapShader', code: TEXT_MIPMAP_SHADER });
605
+ const mipmapPipeline = device.createRenderPipeline({
606
+ label: 'Animation2D.textMipmapPipeline',
607
+ layout: 'auto',
608
+ vertex: { module: mipmapShader, entryPoint: 'vertexMain' },
609
+ fragment: { module: mipmapShader, entryPoint: 'fragmentMain', targets: [{ format: 'rgba8unorm' }] },
610
+ primitive: { topology: 'triangle-list' },
611
+ });
220
612
  const shader = device.createShaderModule({ label: 'Animation2D.shader', code: SHADER });
221
613
  const pipelineLayout = device.createPipelineLayout({
222
614
  bindGroupLayouts: [cameraLayout, objectLayout, textureLayout, compositeLayout],
@@ -260,6 +652,8 @@ function createAnimation2DSharedGpu(device, tracker) {
260
652
  textureLayout,
261
653
  compositeLayout,
262
654
  sampler,
655
+ mipmapSampler,
656
+ mipmapPipeline,
263
657
  shader,
264
658
  pipelineLayout,
265
659
  pipelines: new Map(),
@@ -285,6 +679,8 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
285
679
  textureLayout;
286
680
  compositeLayout;
287
681
  sampler;
682
+ mipmapSampler;
683
+ mipmapPipeline;
288
684
  whiteTexture;
289
685
  whiteBindGroup;
290
686
  emptyCompositeBindGroup;
@@ -295,6 +691,8 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
295
691
  pipelines;
296
692
  entityGpu = new Map();
297
693
  geometryGpu = new Map();
694
+ explicitUvIds = new WeakMap();
695
+ nextExplicitUvId = 1;
298
696
  textureBindGroups = new WeakMap();
299
697
  maskTargets = new Map();
300
698
  effectTargets = new Map();
@@ -371,6 +769,27 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
371
769
  }
372
770
  group.push(item);
373
771
  }
772
+ // Variant-expanded documents can retain many mutually exclusive mask
773
+ // sources. A completely transparent group contributes the transparent
774
+ // identity mask, so sharing the renderer's immutable transparent texture
775
+ // is exact and avoids allocating one full-viewport target per inactive
776
+ // state. Keep the group keys so alpha-inverted consumers still resolve the
777
+ // correct transparent input instead of being classified as unresolved.
778
+ const knownSourceKeys = new Set(this.sourceItems.keys());
779
+ let activeSourceGroupCount = 0;
780
+ for (const sources of this.sourceItems.values()) {
781
+ if (sources.some(source => !isFullyTransparent(source.visual)))
782
+ activeSourceGroupCount++;
783
+ }
784
+ const viewKey = context.view?.key ?? 'default';
785
+ assertAnimationMaskBudget({
786
+ groupCount: activeSourceGroupCount,
787
+ maxGroupCount: this.maxMaskTargets,
788
+ width: context.view?.target.width ?? this.engine.width,
789
+ height: context.view?.target.height ?? this.engine.height,
790
+ maxTextureDimension2D: context.device.limits.maxTextureDimension2D,
791
+ viewKey,
792
+ });
374
793
  const maskTargets = this.activeMaskTargets;
375
794
  maskTargets.clear();
376
795
  let maskPixels = 0;
@@ -382,19 +801,19 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
382
801
  let effectPixels = 0;
383
802
  for (const sourceKey of orderSourceGroups(this.sourceItems)) {
384
803
  const sources = this.sourceItems.get(sourceKey);
385
- if (maskCount >= this.maxMaskTargets)
386
- break;
387
- const targetKey = `${context.view?.key ?? 'default'}:${sourceKey}`;
804
+ if (sources.every(source => isFullyTransparent(source.visual)))
805
+ continue;
806
+ const targetKey = animationMaskTargetKey(viewKey, sourceKey);
388
807
  const target = this.getMaskTarget(targetKey, context, frame);
389
808
  maskPixels += target.width * target.height;
390
809
  maskCount++;
391
810
  let initialized = false;
392
811
  for (const source of sources) {
393
- const resolved = this.resolveCompositeBindGroup(source.visual, maskTargets, context.view?.key ?? 'default');
394
- if (!resolved) {
395
- droppedCompositeCount++;
812
+ if (isFullyTransparent(source.visual))
396
813
  continue;
397
- }
814
+ const resolved = this.resolveCompositeBindGroup(source.visual, maskTargets, knownSourceKeys, viewKey);
815
+ if (!resolved)
816
+ throw unresolvedCompositeError(source.visual, viewKey);
398
817
  compositeLayerCount += source.visual.compositeLayers.length;
399
818
  const effectTarget = source.visual.effects.length > 0 && !context.passEncoder
400
819
  ? this.renderEffectStack(source, context, resolved, frame)
@@ -412,7 +831,7 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
412
831
  this.drawEffectResult(pass, source.entity, effectTarget.texture);
413
832
  }
414
833
  else {
415
- pass.setPipeline(this.getPipeline('rgba8unorm', 1, false, false));
834
+ pass.setPipeline(this.getPipeline('rgba8unorm', 1, false, false, 'normal', source.visual.textureAlphaMode, source.visual.culling));
416
835
  this.draw(pass, source.entity, source.visual, context, resolved, true);
417
836
  }
418
837
  pass.end();
@@ -434,7 +853,7 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
434
853
  applyViewport(passEncoder, context);
435
854
  passEncoder.setBindGroup(0, this.cameraGpu.bindGroup);
436
855
  for (const entry of pending) {
437
- passEncoder.setPipeline(this.getPipeline(outputFormat, context.view?.sampleCount ?? this.engine.msaaSamples, true, context.view?.reverseZ ?? this.engine.reverseZ));
856
+ passEncoder.setPipeline(this.getPipeline(outputFormat, context.view?.sampleCount ?? this.engine.msaaSamples, true, context.view?.reverseZ ?? this.engine.reverseZ, entry.item.visual.blendMode, entry.item.visual.textureAlphaMode, entry.item.visual.culling));
438
857
  this.draw(passEncoder, entry.item.entity, entry.item.visual, context, entry.composite, false);
439
858
  visualCount++;
440
859
  }
@@ -449,13 +868,11 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
449
868
  };
450
869
  for (const item of this.items) {
451
870
  const { visual } = item;
452
- if (visual.sourceOnly)
453
- continue;
454
- const resolved = this.resolveCompositeBindGroup(visual, maskTargets, context.view?.key ?? 'default');
455
- if (!resolved) {
456
- droppedCompositeCount++;
871
+ if (visual.sourceOnly || isFullyTransparent(visual))
457
872
  continue;
458
- }
873
+ const resolved = this.resolveCompositeBindGroup(visual, maskTargets, knownSourceKeys, viewKey);
874
+ if (!resolved)
875
+ throw unresolvedCompositeError(visual, viewKey);
459
876
  compositeLayerCount += visual.compositeLayers.length;
460
877
  if (visual.effects.length > 0 && !context.passEncoder) {
461
878
  const effectTarget = this.renderEffectStack(item, context, resolved, frame);
@@ -517,6 +934,8 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
517
934
  this.textureLayout = shared.textureLayout;
518
935
  this.compositeLayout = shared.compositeLayout;
519
936
  this.sampler = shared.sampler;
937
+ this.mipmapSampler = shared.mipmapSampler;
938
+ this.mipmapPipeline = shared.mipmapPipeline;
520
939
  this.whiteTexture = shared.whiteTexture;
521
940
  this.whiteBindGroup = shared.whiteBindGroup;
522
941
  this.emptyCompositeBindGroup = shared.emptyCompositeBindGroup;
@@ -548,13 +967,13 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
548
967
  throw new Error('Animation2D ordered effects require an isolated render-pass descriptor.');
549
968
  return { passEncoder: context.encoder.beginRenderPass(cloneRenderPassDescriptor(context.descriptor, 'load')), ownsPass: true };
550
969
  }
551
- getPipeline(format, sampleCount, withDepth, reverseZ) {
970
+ getPipeline(format, sampleCount, withDepth, reverseZ, blendMode = 'normal', textureAlphaMode = 'straight', culling = false) {
552
971
  const depthFormat = withDepth ? this.engine.getDepthFormat(reverseZ) : 'none';
553
- const key = `visual:${format}:${sampleCount}:${depthFormat}:${reverseZ ? 1 : 0}`;
972
+ const key = `visual:${format}:${sampleCount}:${depthFormat}:${reverseZ ? 1 : 0}:${blendMode}:${textureAlphaMode}:${animation2DCullingPipelineKey(culling)}`;
554
973
  const cached = this.pipelines.get(key);
555
974
  if (cached)
556
975
  return cached;
557
- const pipeline = requireEngineDevice(this.engine).createRenderPipeline({
976
+ const pipeline = createAnimation2DPipeline(key, () => requireEngineDevice(this.engine).createRenderPipeline({
558
977
  label: `Animation2D.pipeline:${key}`,
559
978
  layout: this.pipelineLayout,
560
979
  vertex: { module: this.shader, entryPoint: 'vs_main', buffers: [{
@@ -564,18 +983,19 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
564
983
  { shaderLocation: 1, offset: 8, format: 'float32x2' },
565
984
  ],
566
985
  }] },
567
- fragment: { module: this.shader, entryPoint: 'fs_main', targets: [{ format, blend: {
568
- color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
569
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
570
- } }] },
571
- primitive: { topology: 'triangle-list', cullMode: 'none' },
986
+ fragment: {
987
+ module: this.shader,
988
+ entryPoint: animationBlendFragmentEntryPoint(blendMode, textureAlphaMode),
989
+ targets: [{ format, blend: animationBlendState(blendMode) }],
990
+ },
991
+ primitive: animation2DCullingPrimitive(culling),
572
992
  ...(withDepth ? { depthStencil: {
573
993
  format: depthFormat,
574
994
  depthWriteEnabled: false,
575
995
  depthCompare: 'always',
576
996
  } } : {}),
577
997
  multisample: { count: sampleCount },
578
- });
998
+ }));
579
999
  this.pipelines.set(key, pipeline);
580
1000
  return pipeline;
581
1001
  }
@@ -622,11 +1042,13 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
622
1042
  const object = this.getEntityGpu(entity);
623
1043
  object.data.set(this.getWorldMatrix2D(entity, context), 0);
624
1044
  object.data.set(visual.color, 16);
625
- object.data[20] = visual.compositeLayers.length;
626
- object.data[21] = outputMask ? 1 : 0;
627
- object.data[22] = Math.min(MAX_EFFECTS, visual.effects.length);
628
- object.data[23] = 0;
629
- object.data.set(visual.uvRect, 24);
1045
+ object.data.set(visual.multiplyColor, MULTIPLY_COLOR_OFFSET);
1046
+ object.data.set(visual.screenColor, SCREEN_COLOR_OFFSET);
1047
+ object.data[PARAMS_OFFSET] = visual.compositeLayers.length;
1048
+ object.data[PARAMS_OFFSET + 1] = outputMask ? 1 : 0;
1049
+ object.data[PARAMS_OFFSET + 2] = Math.min(MAX_EFFECTS, visual.effects.length);
1050
+ object.data[PARAMS_OFFSET + 3] = 0;
1051
+ object.data.set(visual.uvRect, UV_RECT_OFFSET);
630
1052
  object.data.fill(0, COMPOSITE_PARAMS_OFFSET);
631
1053
  for (let index = 0; index < visual.compositeLayers.length; index++) {
632
1054
  const layer = visual.compositeLayers[index];
@@ -686,7 +1108,7 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
686
1108
  }],
687
1109
  });
688
1110
  applyViewport(sourcePass, context);
689
- sourcePass.setPipeline(this.getPipeline('rgba8unorm', 1, false, false));
1111
+ sourcePass.setPipeline(this.getPipeline('rgba8unorm', 1, false, false, 'normal', item.visual.textureAlphaMode, item.visual.culling));
690
1112
  sourcePass.setBindGroup(0, this.cameraGpu.bindGroup);
691
1113
  this.draw(sourcePass, item.entity, item.visual, context, compositeBindGroup, false);
692
1114
  sourcePass.end();
@@ -732,6 +1154,7 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
732
1154
  externalVersion: -1,
733
1155
  externalWidth: 0,
734
1156
  externalHeight: 0,
1157
+ externalMipLevelCount: 1,
735
1158
  externalBindGroup: null,
736
1159
  };
737
1160
  });
@@ -762,22 +1185,26 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
762
1185
  retire();
763
1186
  }
764
1187
  }
1188
+ object.externalMipLevelCount = calculateMipLevelCount(size.width, size.height);
765
1189
  object.externalTexture = requireEngineDevice(this.engine).createTexture({
766
1190
  label: `Animation2D.text:${entity.id}`,
767
1191
  size: [size.width, size.height],
1192
+ mipLevelCount: object.externalMipLevelCount,
768
1193
  format: 'rgba8unorm',
769
1194
  // WebGPU copyExternalImageToTexture requires RENDER_ATTACHMENT in addition
770
1195
  // to COPY_DST for browser-backed image sources.
771
1196
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
772
1197
  });
773
- getExtensionGPUResourceTracker(this.engine)?.trackTexture(object.externalTexture, 'Animation2D.textTexture', size.width * size.height * 4);
1198
+ getExtensionGPUResourceTracker(this.engine)?.trackTexture(object.externalTexture, 'Animation2D.textTexture', estimateRgba8MipChainBytes(size.width, size.height, object.externalMipLevelCount));
774
1199
  object.externalBindGroup = this.createTextureBindGroup(object.externalTexture);
775
1200
  object.externalWidth = size.width;
776
1201
  object.externalHeight = size.height;
777
1202
  object.externalVersion = -1;
778
1203
  }
779
1204
  if (object.externalSource !== source || object.externalVersion !== material.textureVersion) {
780
- requireEngineDevice(this.engine).queue.copyExternalImageToTexture({ source }, { texture: object.externalTexture }, [size.width, size.height]);
1205
+ const device = requireEngineDevice(this.engine);
1206
+ device.queue.copyExternalImageToTexture({ source }, { texture: object.externalTexture, premultipliedAlpha: true }, [size.width, size.height]);
1207
+ this.generateTextMipChain(device, object.externalTexture, object.externalMipLevelCount, entity.id);
781
1208
  object.externalSource = source;
782
1209
  object.externalVersion = material.textureVersion;
783
1210
  }
@@ -785,14 +1212,44 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
785
1212
  }
786
1213
  return visual.requiresTexture ? this.transparentBindGroup : this.whiteBindGroup;
787
1214
  }
1215
+ generateTextMipChain(device, texture, mipLevelCount, entityId) {
1216
+ if (mipLevelCount <= 1)
1217
+ return;
1218
+ const encoder = device.createCommandEncoder({ label: `Animation2D.textMipmaps:${entityId}` });
1219
+ for (let level = 1; level < mipLevelCount; level++) {
1220
+ const bindGroup = device.createBindGroup({
1221
+ label: `Animation2D.textMipmapBindGroup:${entityId}:${level}`,
1222
+ layout: this.mipmapPipeline.getBindGroupLayout(0),
1223
+ entries: [
1224
+ { binding: 0, resource: texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 }) },
1225
+ { binding: 1, resource: this.mipmapSampler },
1226
+ ],
1227
+ });
1228
+ const pass = encoder.beginRenderPass({
1229
+ label: `Animation2D.textMipmapPass:${entityId}:${level}`,
1230
+ colorAttachments: [{
1231
+ view: texture.createView({ baseMipLevel: level, mipLevelCount: 1 }),
1232
+ loadOp: 'clear',
1233
+ storeOp: 'store',
1234
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1235
+ }],
1236
+ });
1237
+ pass.setPipeline(this.mipmapPipeline);
1238
+ pass.setBindGroup(0, bindGroup);
1239
+ pass.draw(3);
1240
+ pass.end();
1241
+ }
1242
+ device.queue.submit([encoder.finish()]);
1243
+ }
788
1244
  getGeometryGpu(visual) {
789
1245
  const geometry = visual.geometry;
790
- let gpu = this.geometryGpu.get(geometry.id);
1246
+ const key = this.geometryKey(visual);
1247
+ let gpu = this.geometryGpu.get(key);
791
1248
  if (gpu && gpu.version === geometry.version)
792
1249
  return gpu;
793
1250
  if (gpu)
794
1251
  this.destroyGeometryGpu(gpu);
795
- const vertices = buildVertices(geometry.positions);
1252
+ const vertices = buildVertices(geometry.positions, visual.uvs);
796
1253
  const device = requireEngineDevice(this.engine);
797
1254
  const vertexBuffer = device.createBuffer({ size: vertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
798
1255
  device.queue.writeBuffer(vertexBuffer, 0, vertices);
@@ -814,9 +1271,19 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
814
1271
  indexFormat: geometry.indices instanceof Uint32Array ? 'uint32' : 'uint16',
815
1272
  version: geometry.version,
816
1273
  };
817
- this.geometryGpu.set(geometry.id, gpu);
1274
+ this.geometryGpu.set(key, gpu);
818
1275
  return gpu;
819
1276
  }
1277
+ geometryKey(visual) {
1278
+ if (!visual.uvs)
1279
+ return `${visual.geometry.id}:derived`;
1280
+ let uvId = this.explicitUvIds.get(visual.uvs);
1281
+ if (uvId === undefined) {
1282
+ uvId = this.nextExplicitUvId++;
1283
+ this.explicitUvIds.set(visual.uvs, uvId);
1284
+ }
1285
+ return `${visual.geometry.id}:uv:${uvId}`;
1286
+ }
820
1287
  getTextureBindGroup(texture) {
821
1288
  let bindGroup = this.textureBindGroups.get(texture);
822
1289
  if (!bindGroup) {
@@ -831,20 +1298,28 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
831
1298
  { binding: 1, resource: this.sampler },
832
1299
  ] });
833
1300
  }
834
- resolveCompositeBindGroup(visual, targets, viewKey) {
1301
+ resolveCompositeBindGroup(visual, targets, knownSourceKeys, viewKey) {
835
1302
  if (visual.compositeLayers.length === 0)
836
1303
  return this.emptyCompositeBindGroup;
837
1304
  const resolved = [];
1305
+ const resolvedKeys = [];
838
1306
  for (const key of visual.compositeKeys) {
839
1307
  const target = targets.get(key);
840
- if (!target)
841
- return null;
842
- resolved.push(target);
1308
+ if (target) {
1309
+ resolved.push(target.texture);
1310
+ resolvedKeys.push(key);
1311
+ }
1312
+ else {
1313
+ if (!knownSourceKeys.has(key))
1314
+ return null;
1315
+ resolved.push(this.transparentTexture);
1316
+ resolvedKeys.push(`transparent:${key}`);
1317
+ }
843
1318
  }
844
- const key = `${viewKey}:${visual.compositeKeys.join('|')}`;
1319
+ const key = animationMaskCompositeKey(viewKey, resolvedKeys);
845
1320
  let bindGroup = this.compositeBindGroups.get(key);
846
1321
  if (!bindGroup) {
847
- bindGroup = this.createCompositeBindGroup(resolved.map(target => target.texture));
1322
+ bindGroup = this.createCompositeBindGroup(resolved);
848
1323
  this.compositeBindGroups.set(key, bindGroup);
849
1324
  }
850
1325
  return bindGroup;
@@ -946,7 +1421,7 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
946
1421
  sweepGeometryGpu() {
947
1422
  this.liveGeometryIds.clear();
948
1423
  for (const item of this.items)
949
- this.liveGeometryIds.add(item.visual.geometry.id);
1424
+ this.liveGeometryIds.add(this.geometryKey(item.visual));
950
1425
  for (const [id, gpu] of this.geometryGpu)
951
1426
  if (!this.liveGeometryIds.has(id)) {
952
1427
  this.destroyGeometryGpu(gpu);
@@ -980,6 +1455,9 @@ class Animation2DRenderSystem extends RenderSystem2DBase {
980
1455
  }
981
1456
  }
982
1457
  }
1458
+ function isFullyTransparent(visual) {
1459
+ return visual.color[3] <= 0 && visual.effects.length === 0;
1460
+ }
983
1461
  function writeBufferAligned(queue, buffer, source) {
984
1462
  if (source.byteLength % 4 === 0) {
985
1463
  queue.writeBuffer(buffer, 0, source);
@@ -1003,6 +1481,16 @@ function externalImageSize(source) {
1003
1481
  height: Math.max(1, isImage ? source.naturalHeight || source.height : source.height),
1004
1482
  };
1005
1483
  }
1484
+ function calculateMipLevelCount(width, height) {
1485
+ return Math.floor(Math.log2(Math.max(1, width, height))) + 1;
1486
+ }
1487
+ function estimateRgba8MipChainBytes(width, height, mipLevelCount) {
1488
+ let bytes = 0;
1489
+ for (let level = 0; level < mipLevelCount; level++) {
1490
+ bytes += Math.max(1, width >> level) * Math.max(1, height >> level) * 4;
1491
+ }
1492
+ return bytes;
1493
+ }
1006
1494
  function compareItems(a, b) {
1007
1495
  return a.visual.instanceId - b.visual.instanceId || a.visual.order - b.visual.order || a.entity.id - b.entity.id;
1008
1496
  }
@@ -1035,10 +1523,14 @@ function compositeOperationCode(operation) {
1035
1523
  }
1036
1524
  function effectKindCode(kind) {
1037
1525
  return kind === 'tint' ? 1 : kind === 'fill' ? 2 : kind === 'opacity' ? 3
1038
- : kind === 'color-matrix' ? 4 : kind === 'blur' ? 5 : 6;
1526
+ : kind === 'color-matrix' ? 4 : kind === 'blur' ? 5 : kind === 'drop-shadow' ? 6
1527
+ : kind === 'inner-feather' ? 7 : 8;
1039
1528
  }
1040
1529
  function visualNodeKey(visual) { return visual.nodeKey; }
1041
- function buildVertices(positions) {
1530
+ function unresolvedCompositeError(visual, viewKey) {
1531
+ return new AnimationFormatError('E_ANIMATION_INVALID_FORMAT', `Composite target for visual "${visual.nodeId}" could not be resolved in view "${viewKey}".`, `$runtime.visuals[${JSON.stringify(visual.nodeId)}].composite`);
1532
+ }
1533
+ function buildVertices(positions, explicitUvs) {
1042
1534
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1043
1535
  for (let i = 0; i < positions.length; i += 2) {
1044
1536
  const x = positions[i], y = positions[i + 1];
@@ -1053,8 +1545,8 @@ function buildVertices(positions) {
1053
1545
  const x = positions[source], y = positions[source + 1];
1054
1546
  out[target++] = x;
1055
1547
  out[target++] = y;
1056
- out[target++] = (x - minX) / width;
1057
- out[target++] = 1 - (y - minY) / height;
1548
+ out[target++] = explicitUvs?.[source] ?? (x - minX) / width;
1549
+ out[target++] = explicitUvs?.[source + 1] ?? 1 - (y - minY) / height;
1058
1550
  }
1059
1551
  return out;
1060
1552
  }
@@ -1167,5 +1659,5 @@ class Animation2DExtensionRegistry {
1167
1659
  }
1168
1660
  }
1169
1661
 
1170
- export { Animation2DComponent, Animation2DExtensionRegistry, Animation2DRenderSystem, Animation2DSystem, HAIYUE_ANIMATION_ASSET_TYPE, createAnimationAssetLoader };
1662
+ export { Animation2DComponent, Animation2DExtensionRegistry, Animation2DRenderSystem, Animation2DSystem, HAIYUE_ANIMATION_ASSET_TYPE, InteractionRuntime, createAnimationAssetLoader };
1171
1663
  //# sourceMappingURL=animation.js.map