@nodetool-ai/game-renderer 0.8.1 → 0.8.2

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 (273) hide show
  1. package/README.md +46 -0
  2. package/dist/.nodetool-build-stamp +1 -1
  3. package/dist/audio/mixer.d.ts +75 -0
  4. package/dist/audio/mixer.d.ts.map +1 -0
  5. package/dist/audio/mixer.js +345 -0
  6. package/dist/audio/mixer.js.map +1 -0
  7. package/dist/audio/player.d.ts +67 -0
  8. package/dist/audio/player.d.ts.map +1 -0
  9. package/dist/audio/player.js +337 -0
  10. package/dist/audio/player.js.map +1 -0
  11. package/dist/audio/spatial.d.ts +50 -0
  12. package/dist/audio/spatial.d.ts.map +1 -0
  13. package/dist/audio/spatial.js +166 -0
  14. package/dist/audio/spatial.js.map +1 -0
  15. package/dist/audio/voices.d.ts +11 -0
  16. package/dist/audio/voices.d.ts.map +1 -0
  17. package/dist/audio/voices.js +22 -0
  18. package/dist/audio/voices.js.map +1 -0
  19. package/dist/audio.d.ts +6 -38
  20. package/dist/audio.d.ts.map +1 -1
  21. package/dist/audio.js +3 -226
  22. package/dist/audio.js.map +1 -1
  23. package/dist/browser.d.ts +2 -1
  24. package/dist/browser.d.ts.map +1 -1
  25. package/dist/browser.js +5 -5
  26. package/dist/browser.js.map +1 -1
  27. package/dist/browser3d.d.ts +2 -0
  28. package/dist/browser3d.d.ts.map +1 -0
  29. package/dist/browser3d.js +2 -0
  30. package/dist/browser3d.js.map +1 -0
  31. package/dist/build.d.ts.map +1 -1
  32. package/dist/build.js +29 -19
  33. package/dist/build.js.map +1 -1
  34. package/dist/build3d.d.ts +11 -0
  35. package/dist/build3d.d.ts.map +1 -0
  36. package/dist/build3d.js +126 -0
  37. package/dist/build3d.js.map +1 -0
  38. package/dist/canvas2d.d.ts +12 -3
  39. package/dist/canvas2d.d.ts.map +1 -1
  40. package/dist/canvas2d.js +59 -12
  41. package/dist/canvas2d.js.map +1 -1
  42. package/dist/fixed-tick-host.d.ts +10 -0
  43. package/dist/fixed-tick-host.d.ts.map +1 -0
  44. package/dist/fixed-tick-host.js +29 -0
  45. package/dist/fixed-tick-host.js.map +1 -0
  46. package/dist/frame-budget.d.ts +34 -0
  47. package/dist/frame-budget.d.ts.map +1 -0
  48. package/dist/frame-budget.js +52 -0
  49. package/dist/frame-budget.js.map +1 -0
  50. package/dist/frame.d.ts +20 -1
  51. package/dist/frame.d.ts.map +1 -1
  52. package/dist/frame.js +69 -1
  53. package/dist/frame.js.map +1 -1
  54. package/dist/game3d-capture-page.js +5834 -0
  55. package/dist/game3d-player.js +6001 -0
  56. package/dist/imageDiff.d.ts +9 -0
  57. package/dist/imageDiff.d.ts.map +1 -0
  58. package/dist/imageDiff.js +30 -0
  59. package/dist/imageDiff.js.map +1 -0
  60. package/dist/index.d.ts +12 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +9 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/input-bindings.d.ts +76 -0
  65. package/dist/input-bindings.d.ts.map +1 -0
  66. package/dist/input-bindings.js +377 -0
  67. package/dist/input-bindings.js.map +1 -0
  68. package/dist/input.d.ts +1 -1
  69. package/dist/input.d.ts.map +1 -1
  70. package/dist/input.js +1 -14
  71. package/dist/input.js.map +1 -1
  72. package/dist/input3d.d.ts +3 -0
  73. package/dist/input3d.d.ts.map +1 -0
  74. package/dist/input3d.js +3 -0
  75. package/dist/input3d.js.map +1 -0
  76. package/dist/node.d.ts +5 -0
  77. package/dist/node.d.ts.map +1 -1
  78. package/dist/node.js +28 -11
  79. package/dist/node.js.map +1 -1
  80. package/dist/node3d.d.ts +29 -0
  81. package/dist/node3d.d.ts.map +1 -0
  82. package/dist/node3d.js +160 -0
  83. package/dist/node3d.js.map +1 -0
  84. package/dist/particles/curves.d.ts +16 -0
  85. package/dist/particles/curves.d.ts.map +1 -0
  86. package/dist/particles/curves.js +49 -0
  87. package/dist/particles/curves.js.map +1 -0
  88. package/dist/particles/index.d.ts +6 -0
  89. package/dist/particles/index.d.ts.map +1 -0
  90. package/dist/particles/index.js +6 -0
  91. package/dist/particles/index.js.map +1 -0
  92. package/dist/particles/random.d.ts +10 -0
  93. package/dist/particles/random.d.ts.map +1 -0
  94. package/dist/particles/random.js +26 -0
  95. package/dist/particles/random.js.map +1 -0
  96. package/dist/particles/render2d.d.ts +40 -0
  97. package/dist/particles/render2d.d.ts.map +1 -0
  98. package/dist/particles/render2d.js +70 -0
  99. package/dist/particles/render2d.js.map +1 -0
  100. package/dist/particles/shapes.d.ts +18 -0
  101. package/dist/particles/shapes.d.ts.map +1 -0
  102. package/dist/particles/shapes.js +92 -0
  103. package/dist/particles/shapes.js.map +1 -0
  104. package/dist/particles/simulator.d.ts +88 -0
  105. package/dist/particles/simulator.d.ts.map +1 -0
  106. package/dist/particles/simulator.js +371 -0
  107. package/dist/particles/simulator.js.map +1 -0
  108. package/dist/player-v1.js +2502 -0
  109. package/dist/preparation3d.d.ts +3 -0
  110. package/dist/preparation3d.d.ts.map +1 -0
  111. package/dist/preparation3d.js +3 -0
  112. package/dist/preparation3d.js.map +1 -0
  113. package/dist/renderPipeline.d.ts +12 -0
  114. package/dist/renderPipeline.d.ts.map +1 -0
  115. package/dist/renderPipeline.js +17 -0
  116. package/dist/renderPipeline.js.map +1 -0
  117. package/dist/renderer3d/animation/index.d.ts +11 -0
  118. package/dist/renderer3d/animation/index.d.ts.map +1 -0
  119. package/dist/renderer3d/animation/index.js +212 -0
  120. package/dist/renderer3d/animation/index.js.map +1 -0
  121. package/dist/renderer3d/assets/cache.d.ts +14 -0
  122. package/dist/renderer3d/assets/cache.d.ts.map +1 -0
  123. package/dist/renderer3d/assets/cache.js +74 -0
  124. package/dist/renderer3d/assets/cache.js.map +1 -0
  125. package/dist/renderer3d/assets/dispose.d.ts +4 -0
  126. package/dist/renderer3d/assets/dispose.d.ts.map +1 -0
  127. package/dist/renderer3d/assets/dispose.js +41 -0
  128. package/dist/renderer3d/assets/dispose.js.map +1 -0
  129. package/dist/renderer3d/assets/hdri-contract.d.ts +23 -0
  130. package/dist/renderer3d/assets/hdri-contract.d.ts.map +1 -0
  131. package/dist/renderer3d/assets/hdri-contract.js +2 -0
  132. package/dist/renderer3d/assets/hdri-contract.js.map +1 -0
  133. package/dist/renderer3d/assets/instances.d.ts +5 -0
  134. package/dist/renderer3d/assets/instances.d.ts.map +1 -0
  135. package/dist/renderer3d/assets/instances.js +86 -0
  136. package/dist/renderer3d/assets/instances.js.map +1 -0
  137. package/dist/renderer3d/capture-driver-types.d.ts +42 -0
  138. package/dist/renderer3d/capture-driver-types.d.ts.map +1 -0
  139. package/dist/renderer3d/capture-driver-types.js +2 -0
  140. package/dist/renderer3d/capture-driver-types.js.map +1 -0
  141. package/dist/renderer3d/capture-page.d.ts +36 -0
  142. package/dist/renderer3d/capture-page.d.ts.map +1 -0
  143. package/dist/renderer3d/capture-page.js +68 -0
  144. package/dist/renderer3d/capture-page.js.map +1 -0
  145. package/dist/renderer3d/culling.d.ts +10 -0
  146. package/dist/renderer3d/culling.d.ts.map +1 -0
  147. package/dist/renderer3d/culling.js +27 -0
  148. package/dist/renderer3d/culling.js.map +1 -0
  149. package/dist/renderer3d/environment/index.d.ts +4 -0
  150. package/dist/renderer3d/environment/index.d.ts.map +1 -0
  151. package/dist/renderer3d/environment/index.js +9 -0
  152. package/dist/renderer3d/environment/index.js.map +1 -0
  153. package/dist/renderer3d/environment/sky.d.ts +45 -0
  154. package/dist/renderer3d/environment/sky.d.ts.map +1 -0
  155. package/dist/renderer3d/environment/sky.js +221 -0
  156. package/dist/renderer3d/environment/sky.js.map +1 -0
  157. package/dist/renderer3d/hud/fonts.d.ts +13 -0
  158. package/dist/renderer3d/hud/fonts.d.ts.map +1 -0
  159. package/dist/renderer3d/hud/fonts.js +69 -0
  160. package/dist/renderer3d/hud/fonts.js.map +1 -0
  161. package/dist/renderer3d/hud/paint.d.ts +4 -0
  162. package/dist/renderer3d/hud/paint.d.ts.map +1 -0
  163. package/dist/renderer3d/hud/paint.js +22 -0
  164. package/dist/renderer3d/hud/paint.js.map +1 -0
  165. package/dist/renderer3d/import.d.ts +34 -0
  166. package/dist/renderer3d/import.d.ts.map +1 -0
  167. package/dist/renderer3d/import.js +408 -0
  168. package/dist/renderer3d/import.js.map +1 -0
  169. package/dist/renderer3d/index.d.ts +74 -0
  170. package/dist/renderer3d/index.d.ts.map +1 -0
  171. package/dist/renderer3d/index.js +338 -0
  172. package/dist/renderer3d/index.js.map +1 -0
  173. package/dist/renderer3d/lights/index.d.ts +7 -0
  174. package/dist/renderer3d/lights/index.d.ts.map +1 -0
  175. package/dist/renderer3d/lights/index.js +67 -0
  176. package/dist/renderer3d/lights/index.js.map +1 -0
  177. package/dist/renderer3d/materials/index.d.ts +4 -0
  178. package/dist/renderer3d/materials/index.d.ts.map +1 -0
  179. package/dist/renderer3d/materials/index.js +8 -0
  180. package/dist/renderer3d/materials/index.js.map +1 -0
  181. package/dist/renderer3d/passes/post-processing.d.ts +44 -0
  182. package/dist/renderer3d/passes/post-processing.d.ts.map +1 -0
  183. package/dist/renderer3d/passes/post-processing.js +213 -0
  184. package/dist/renderer3d/passes/post-processing.js.map +1 -0
  185. package/dist/renderer3d/pipeline.d.ts +15 -0
  186. package/dist/renderer3d/pipeline.d.ts.map +1 -0
  187. package/dist/renderer3d/pipeline.js +10 -0
  188. package/dist/renderer3d/pipeline.js.map +1 -0
  189. package/dist/renderer3d/playwright-contract.d.ts +3 -0
  190. package/dist/renderer3d/playwright-contract.d.ts.map +1 -0
  191. package/dist/renderer3d/playwright-contract.js +4 -0
  192. package/dist/renderer3d/playwright-contract.js.map +1 -0
  193. package/dist/renderer3d/preparation.d.ts +79 -0
  194. package/dist/renderer3d/preparation.d.ts.map +1 -0
  195. package/dist/renderer3d/preparation.js +458 -0
  196. package/dist/renderer3d/preparation.js.map +1 -0
  197. package/dist/renderer3d/scene-sync.d.ts +8 -0
  198. package/dist/renderer3d/scene-sync.d.ts.map +1 -0
  199. package/dist/renderer3d/scene-sync.js +96 -0
  200. package/dist/renderer3d/scene-sync.js.map +1 -0
  201. package/dist/renderer3d/shadows/cascades.d.ts +40 -0
  202. package/dist/renderer3d/shadows/cascades.d.ts.map +1 -0
  203. package/dist/renderer3d/shadows/cascades.js +244 -0
  204. package/dist/renderer3d/shadows/cascades.js.map +1 -0
  205. package/dist/renderer3d/shadows/index.d.ts +44 -0
  206. package/dist/renderer3d/shadows/index.d.ts.map +1 -0
  207. package/dist/renderer3d/shadows/index.js +129 -0
  208. package/dist/renderer3d/shadows/index.js.map +1 -0
  209. package/dist/renderer3d/types.d.ts +18 -0
  210. package/dist/renderer3d/types.d.ts.map +1 -0
  211. package/dist/renderer3d/types.js +2 -0
  212. package/dist/renderer3d/types.js.map +1 -0
  213. package/dist/smoke.d.ts +15 -0
  214. package/dist/smoke.d.ts.map +1 -0
  215. package/dist/smoke.js +138 -0
  216. package/dist/smoke.js.map +1 -0
  217. package/dist/standalone-player.d.ts +11 -1
  218. package/dist/standalone-player.d.ts.map +1 -1
  219. package/dist/standalone-player.js +67 -41
  220. package/dist/standalone-player.js.map +1 -1
  221. package/dist/standalone-player3d.d.ts +14 -0
  222. package/dist/standalone-player3d.d.ts.map +1 -0
  223. package/dist/standalone-player3d.js +255 -0
  224. package/dist/standalone-player3d.js.map +1 -0
  225. package/dist/touch-controls.d.ts +22 -8
  226. package/dist/touch-controls.d.ts.map +1 -1
  227. package/dist/touch-controls.js +88 -21
  228. package/dist/touch-controls.js.map +1 -1
  229. package/dist/ui/controller.d.ts +59 -0
  230. package/dist/ui/controller.d.ts.map +1 -0
  231. package/dist/ui/controller.js +201 -0
  232. package/dist/ui/controller.js.map +1 -0
  233. package/dist/ui/hud2d.d.ts +11 -0
  234. package/dist/ui/hud2d.d.ts.map +1 -0
  235. package/dist/ui/hud2d.js +25 -0
  236. package/dist/ui/hud2d.js.map +1 -0
  237. package/dist/ui/layout.d.ts +42 -0
  238. package/dist/ui/layout.d.ts.map +1 -0
  239. package/dist/ui/layout.js +182 -0
  240. package/dist/ui/layout.js.map +1 -0
  241. package/dist/ui/paint.d.ts +45 -0
  242. package/dist/ui/paint.d.ts.map +1 -0
  243. package/dist/ui/paint.js +121 -0
  244. package/dist/ui/paint.js.map +1 -0
  245. package/dist/webgpu/effects.d.ts +22 -0
  246. package/dist/webgpu/effects.d.ts.map +1 -0
  247. package/dist/webgpu/effects.js +69 -0
  248. package/dist/webgpu/effects.js.map +1 -0
  249. package/dist/webgpu/hud.d.ts +4 -0
  250. package/dist/webgpu/hud.d.ts.map +1 -0
  251. package/dist/webgpu/hud.js +27 -0
  252. package/dist/webgpu/hud.js.map +1 -0
  253. package/dist/webgpu/lighting.d.ts +4 -0
  254. package/dist/webgpu/lighting.d.ts.map +1 -0
  255. package/dist/webgpu/lighting.js +47 -0
  256. package/dist/webgpu/lighting.js.map +1 -0
  257. package/dist/webgpu/pipeline.d.ts +11 -0
  258. package/dist/webgpu/pipeline.d.ts.map +1 -0
  259. package/dist/webgpu/pipeline.js +11 -0
  260. package/dist/webgpu/pipeline.js.map +1 -0
  261. package/dist/webgpu/sprites.d.ts +6 -0
  262. package/dist/webgpu/sprites.d.ts.map +1 -0
  263. package/dist/webgpu/sprites.js +74 -0
  264. package/dist/webgpu/sprites.js.map +1 -0
  265. package/dist/webgpu/types.d.ts +14 -0
  266. package/dist/webgpu/types.d.ts.map +1 -0
  267. package/dist/webgpu/types.js +2 -0
  268. package/dist/webgpu/types.js.map +1 -0
  269. package/dist/webgpu.d.ts +4 -3
  270. package/dist/webgpu.d.ts.map +1 -1
  271. package/dist/webgpu.js +119 -279
  272. package/dist/webgpu.js.map +1 -1
  273. package/package.json +47 -10
@@ -0,0 +1,70 @@
1
+ import { ParticleSimulator, particleSourcesFromFrame } from "./simulator.js";
2
+ /** Asset id of the built-in soft round dot that particles without a sprite draw with. */
3
+ export const PARTICLE_DOT_ASSET = "nodetool:particle-dot";
4
+ export const PARTICLE_DOT_SIZE = 32;
5
+ /**
6
+ * Visible particles the Canvas2D fallback draws. Canvas2D composites each particle separately, so it stops at a
7
+ * lower count than WebGPU, which draws every particle the simulator holds as one instanced batch per texture.
8
+ */
9
+ export const CANVAS2D_MAX_PARTICLES = 1024;
10
+ /** White RGBA pixels whose alpha falls smoothly from the centre to zero at the edge. */
11
+ export function particleDotPixels(size = PARTICLE_DOT_SIZE) {
12
+ const pixels = new Uint8ClampedArray(size * size * 4);
13
+ const half = size / 2;
14
+ for (let y = 0; y < size; y += 1) {
15
+ for (let x = 0; x < size; x += 1) {
16
+ const distance = Math.hypot(x + 0.5 - half, y + 0.5 - half) / half;
17
+ const falloff = Math.max(0, 1 - distance);
18
+ const offset = (y * size + x) * 4;
19
+ pixels[offset] = 255;
20
+ pixels[offset + 1] = 255;
21
+ pixels[offset + 2] = 255;
22
+ pixels[offset + 3] = Math.round(255 * falloff * falloff * (3 - 2 * falloff));
23
+ }
24
+ }
25
+ return pixels;
26
+ }
27
+ /** The sheet cell a particle shows at normalized age `life`. */
28
+ export function particleCell(sprite, life) {
29
+ const cells = sprite.columns * sprite.rows;
30
+ if (cells <= 1) {
31
+ return undefined;
32
+ }
33
+ const frames = Math.min(sprite.frameCount ?? cells, cells);
34
+ const steps = frames * sprite.cycles;
35
+ // Life 1 is the last moment of the last cycle, not the start of another.
36
+ const index = Math.min(steps - 1, Math.floor(Math.max(0, Math.min(1, life)) * steps)) % frames;
37
+ return { index, columns: sprite.columns, rows: sprite.rows };
38
+ }
39
+ /**
40
+ * Runs 2D particles beside a game session. Call `tick` after every session step with that step's frame and
41
+ * the session's presentation events, and pass the instance to the renderer. It reads the session and never
42
+ * writes to it, so particles stay out of snapshots and replay.
43
+ */
44
+ export class GameParticles2D {
45
+ simulator;
46
+ tickSeconds;
47
+ constructor(tickRate, maxSceneParticles) {
48
+ if (!(tickRate > 0)) {
49
+ throw new Error("Particle tick rate must be positive");
50
+ }
51
+ this.tickSeconds = 1 / tickRate;
52
+ this.simulator = new ParticleSimulator({ dimension: "2d", maxSceneParticles });
53
+ }
54
+ /** Live particles, for the frame budget. */
55
+ get count() {
56
+ return this.simulator.count;
57
+ }
58
+ tick(frame, events) {
59
+ this.simulator.sync(particleSourcesFromFrame(frame));
60
+ this.simulator.emit(events);
61
+ this.simulator.step(this.tickSeconds);
62
+ }
63
+ forEachParticle(visit) {
64
+ this.simulator.forEachParticle(visit);
65
+ }
66
+ clear() {
67
+ this.simulator.clear();
68
+ }
69
+ }
70
+ //# sourceMappingURL=render2d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render2d.js","sourceRoot":"","sources":["../../src/particles/render2d.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAqB,MAAM,gBAAgB,CAAC;AAOhG,yFAAyF;AACzF,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC;AAE1D,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAE3C,wFAAwF;AACxF,MAAM,UAAU,iBAAiB,CAAC,IAAI,GAAG,iBAAiB;IACxD,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;YACnE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC;YAC1C,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;YACrB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;YACzB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;YACzB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,GAAG,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AASD,gEAAgE;AAChE,MAAM,UAAU,YAAY,CAAC,MAA0B,EAAE,IAAY;IACnE,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC;IAC3C,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACf,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,KAAK,EAAE,KAAK,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,yEAAyE;IACzE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC;IAC/F,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACjB,SAAS,CAAoB;IACrB,WAAW,CAAS;IAErC,YAAY,QAAgB,EAAE,iBAA0B;QACtD,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,CAAC,GAAG,QAAQ,CAAC;QAChC,IAAI,CAAC,SAAS,GAAG,IAAI,iBAAiB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACjF,CAAC;IAED,4CAA4C;IAC5C,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,KAAsB,EAAE,MAAqD;QAChF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC,CAAC,CAAC;QACrD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC;IAED,eAAe,CAAC,KAAiD;QAC/D,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ import type { GameParticleShape } from "@nodetool-ai/protocol";
2
+ import type { ParticleRandom } from "./random.js";
3
+ export type ParticleDimension = "2d" | "3d";
4
+ /** Spawn position and unit direction in emitter space. */
5
+ export interface ParticleShapeSample {
6
+ px: number;
7
+ py: number;
8
+ pz: number;
9
+ dx: number;
10
+ dy: number;
11
+ dz: number;
12
+ }
13
+ /**
14
+ * Samples a spawn point and direction. Shapes lie in the XY plane except sphere, cone and box in 3D.
15
+ * Box, edge and cone emit along +Y. Point, circle and sphere emit outward.
16
+ */
17
+ export declare function sampleParticleShape(shape: GameParticleShape, dimension: ParticleDimension, random: ParticleRandom, out: ParticleShapeSample): ParticleShapeSample;
18
+ //# sourceMappingURL=shapes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shapes.d.ts","sourceRoot":"","sources":["../../src/particles/shapes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,0DAA0D;AAC1D,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,mBAAmB,GAAG,mBAAmB,CAqEjK"}
@@ -0,0 +1,92 @@
1
+ const DEGREES = Math.PI / 180;
2
+ /**
3
+ * Samples a spawn point and direction. Shapes lie in the XY plane except sphere, cone and box in 3D.
4
+ * Box, edge and cone emit along +Y. Point, circle and sphere emit outward.
5
+ */
6
+ export function sampleParticleShape(shape, dimension, random, out) {
7
+ out.px = 0;
8
+ out.py = 0;
9
+ out.pz = 0;
10
+ out.dx = 0;
11
+ out.dy = 1;
12
+ out.dz = 0;
13
+ const flat = dimension === "2d";
14
+ switch (shape.kind) {
15
+ case "point":
16
+ if (flat) {
17
+ circleDirection(random.next() * Math.PI * 2, out);
18
+ }
19
+ else {
20
+ sphereDirection(random, out);
21
+ }
22
+ break;
23
+ case "circle": {
24
+ const angle = random.next() * shape.arc * DEGREES;
25
+ const radius = shape.fromEdge ? shape.radius : shape.radius * Math.sqrt(random.next());
26
+ circleDirection(angle, out);
27
+ out.px = out.dx * radius;
28
+ out.py = out.dy * radius;
29
+ break;
30
+ }
31
+ case "sphere": {
32
+ if (flat) {
33
+ circleDirection(random.next() * Math.PI * 2, out);
34
+ }
35
+ else {
36
+ sphereDirection(random, out);
37
+ }
38
+ const unit = random.next();
39
+ const radius = shape.fromSurface ? shape.radius : shape.radius * (flat ? Math.sqrt(unit) : Math.cbrt(unit));
40
+ out.px = out.dx * radius;
41
+ out.py = out.dy * radius;
42
+ out.pz = out.dz * radius;
43
+ break;
44
+ }
45
+ case "cone": {
46
+ const spread = shape.angle * DEGREES;
47
+ if (flat) {
48
+ const angle = (random.next() * 2 - 1) * spread;
49
+ out.dx = -Math.sin(angle);
50
+ out.dy = Math.cos(angle);
51
+ out.px = (random.next() * 2 - 1) * shape.radius;
52
+ }
53
+ else {
54
+ // Uniform over the cap's solid angle, around +Y, from a disc of the given radius in XZ.
55
+ const cosine = 1 - random.next() * (1 - Math.cos(spread));
56
+ const sine = Math.sqrt(Math.max(0, 1 - cosine * cosine));
57
+ const turn = random.next() * Math.PI * 2;
58
+ out.dx = sine * Math.cos(turn);
59
+ out.dy = cosine;
60
+ out.dz = sine * Math.sin(turn);
61
+ const radius = shape.radius * Math.sqrt(random.next());
62
+ const baseTurn = random.next() * Math.PI * 2;
63
+ out.px = radius * Math.cos(baseTurn);
64
+ out.pz = radius * Math.sin(baseTurn);
65
+ }
66
+ break;
67
+ }
68
+ case "box":
69
+ out.px = (random.next() - 0.5) * shape.size.x;
70
+ out.py = (random.next() - 0.5) * shape.size.y;
71
+ out.pz = flat ? 0 : (random.next() - 0.5) * shape.size.z;
72
+ break;
73
+ case "edge":
74
+ out.px = (random.next() - 0.5) * shape.length;
75
+ break;
76
+ }
77
+ return out;
78
+ }
79
+ function circleDirection(angle, out) {
80
+ out.dx = Math.cos(angle);
81
+ out.dy = Math.sin(angle);
82
+ out.dz = 0;
83
+ }
84
+ function sphereDirection(random, out) {
85
+ const z = random.next() * 2 - 1;
86
+ const turn = random.next() * Math.PI * 2;
87
+ const ring = Math.sqrt(Math.max(0, 1 - z * z));
88
+ out.dx = ring * Math.cos(turn);
89
+ out.dy = ring * Math.sin(turn);
90
+ out.dz = z;
91
+ }
92
+ //# sourceMappingURL=shapes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shapes.js","sourceRoot":"","sources":["../../src/particles/shapes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import type { GameParticleEmitter, GameParticles, GameRenderFrame, GameRenderFrame3D } from "@nodetool-ai/protocol";
2
+ import { type ParticleDimension } from "./shapes.js";
3
+ export interface ParticleVector {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ readonly z: number;
7
+ }
8
+ /** Unit quaternion as `[x, y, z, w]`, matching `gameQuaternion3D`. */
9
+ export type ParticleQuaternion = readonly [number, number, number, number];
10
+ /** One entity's particles component and its current world transform. */
11
+ export interface ParticleEmitterSource {
12
+ readonly entityId: string;
13
+ readonly position: ParticleVector;
14
+ readonly rotation: ParticleQuaternion;
15
+ readonly particles: GameParticles;
16
+ }
17
+ export interface ParticleSimulatorOptions {
18
+ readonly dimension: ParticleDimension;
19
+ /** Live particles across every emitter. Emission stops at this cap until particles die. */
20
+ readonly maxSceneParticles?: number;
21
+ }
22
+ /** A live particle in world space. The object is reused between callbacks of one emitter, so copy what you keep. */
23
+ export interface ParticleView {
24
+ entityId: string;
25
+ emitterId: string;
26
+ /** The emitter definition, for render settings such as blend and sprite. */
27
+ emitter: GameParticleEmitter;
28
+ x: number;
29
+ y: number;
30
+ z: number;
31
+ size: number;
32
+ rotation: number;
33
+ r: number;
34
+ g: number;
35
+ b: number;
36
+ opacity: number;
37
+ /** Normalized age: 0 at birth, 1 at death. */
38
+ life: number;
39
+ }
40
+ export declare const DEFAULT_MAX_SCENE_PARTICLES = 8192;
41
+ /** Longest interval one `step` simulates, so a long pause cannot turn into thousands of emission cycles. */
42
+ export declare const MAX_PARTICLE_STEP_SECONDS = 0.25;
43
+ /**
44
+ * CPU particle simulation for the renderers. It is presentation state: it reads render frames and
45
+ * presentation events, owns its own random streams, and has no snapshot or path back into a session.
46
+ */
47
+ export declare class ParticleSimulator {
48
+ readonly dimension: ParticleDimension;
49
+ readonly maxSceneParticles: number;
50
+ private readonly instances;
51
+ /** Emitter ids of each entity in the last sync, in document order. */
52
+ private readonly emitterOrder;
53
+ private total;
54
+ private readonly sample;
55
+ private readonly color;
56
+ constructor(options: ParticleSimulatorOptions);
57
+ /** Live particles across every emitter. */
58
+ get count(): number;
59
+ /** Live particles of one emitter. */
60
+ countOf(entityId: string, emitterId: string): number;
61
+ /**
62
+ * Updates emitters from the current frame. An emitter missing from `sources` stops emitting. Its world-space
63
+ * particles finish their lives and its local-space particles disappear with the entity.
64
+ */
65
+ sync(sources: readonly ParticleEmitterSource[]): void;
66
+ /** Applies `particles` presentation events from a session. Other event kinds are ignored. */
67
+ emit(events: readonly {
68
+ readonly kind: string;
69
+ }[]): void;
70
+ /**
71
+ * Advances every particle, then emits new particles for the same interval. A step longer than
72
+ * {@link MAX_PARTICLE_STEP_SECONDS}, such as the first frame after a paused tab, is clamped to it.
73
+ */
74
+ step(seconds: number): void;
75
+ /** Visits every live particle in world space. */
76
+ forEachParticle(visit: (particle: Readonly<ParticleView>) => void): void;
77
+ /** Removes every emitter and particle. */
78
+ clear(): void;
79
+ private findInstance;
80
+ private advance;
81
+ /** Emits the rate and bursts that fall in [time, time + dt), wrapping looping cycles, plus pending counts. */
82
+ private schedule;
83
+ /** Spawns one particle at the emitter, or at `origin` (a world point) for a death emitter. */
84
+ private spawn;
85
+ }
86
+ /** Emitter sources from a 2D or 3D render frame. 2D rotation turns about Z. */
87
+ export declare function particleSourcesFromFrame(frame: GameRenderFrame | GameRenderFrame3D): ParticleEmitterSource[];
88
+ //# sourceMappingURL=simulator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simulator.d.ts","sourceRoot":"","sources":["../../src/particles/simulator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,371 @@
1
+ import { evaluateParticleCurve, evaluateParticleGradient, parseParticleColor, sampleParticleRange } from "./curves.js";
2
+ import { ParticleRandom, particleSeed } from "./random.js";
3
+ import { sampleParticleShape } from "./shapes.js";
4
+ export const DEFAULT_MAX_SCENE_PARTICLES = 8192;
5
+ /** Longest interval one `step` simulates, so a long pause cannot turn into thousands of emission cycles. */
6
+ export const MAX_PARTICLE_STEP_SECONDS = 0.25;
7
+ const STRIDE = 14;
8
+ const PX = 0, PY = 1, PZ = 2, VX = 3, VY = 4, VZ = 5, AGE = 6, LIFE = 7, SIZE = 8, ROT = 9, SPIN = 10, CR = 11, CG = 12, CB = 13;
9
+ const SAME_TIME = 1e-9;
10
+ const IDENTITY = [0, 0, 0, 1];
11
+ /**
12
+ * CPU particle simulation for the renderers. It is presentation state: it reads render frames and
13
+ * presentation events, owns its own random streams, and has no snapshot or path back into a session.
14
+ */
15
+ export class ParticleSimulator {
16
+ dimension;
17
+ maxSceneParticles;
18
+ instances = new Map();
19
+ /** Emitter ids of each entity in the last sync, in document order. */
20
+ emitterOrder = new Map();
21
+ total = 0;
22
+ sample = { px: 0, py: 0, pz: 0, dx: 0, dy: 1, dz: 0 };
23
+ color = { r: 0, g: 0, b: 0 };
24
+ constructor(options) {
25
+ this.dimension = options.dimension;
26
+ this.maxSceneParticles = options.maxSceneParticles ?? DEFAULT_MAX_SCENE_PARTICLES;
27
+ }
28
+ /** Live particles across every emitter. */
29
+ get count() {
30
+ return this.total;
31
+ }
32
+ /** Live particles of one emitter. */
33
+ countOf(entityId, emitterId) {
34
+ return this.instances.get(instanceKey(entityId, emitterId))?.count ?? 0;
35
+ }
36
+ /**
37
+ * Updates emitters from the current frame. An emitter missing from `sources` stops emitting. Its world-space
38
+ * particles finish their lives and its local-space particles disappear with the entity.
39
+ */
40
+ sync(sources) {
41
+ for (const instance of this.instances.values()) {
42
+ instance.attached = false;
43
+ }
44
+ this.emitterOrder.clear();
45
+ for (const source of sources) {
46
+ this.emitterOrder.set(source.entityId, source.particles.emitters.map((emitter) => emitter.id));
47
+ for (const definition of source.particles.emitters) {
48
+ const key = instanceKey(source.entityId, definition.id);
49
+ let instance = this.instances.get(key);
50
+ if (!instance) {
51
+ instance = {
52
+ entityId: source.entityId,
53
+ definition,
54
+ random: new ParticleRandom(particleSeed(source.entityId, definition.id)),
55
+ data: new Float64Array(definition.maxParticles * STRIDE),
56
+ count: 0,
57
+ time: 0,
58
+ playing: definition.playOnStart,
59
+ accumulator: 0,
60
+ pending: 0,
61
+ attached: true,
62
+ position: source.position,
63
+ rotation: source.rotation
64
+ };
65
+ this.instances.set(key, instance);
66
+ }
67
+ if (definition.maxParticles !== instance.definition.maxParticles) {
68
+ const data = new Float64Array(definition.maxParticles * STRIDE);
69
+ const kept = Math.min(instance.count, definition.maxParticles);
70
+ data.set(instance.data.subarray(0, kept * STRIDE));
71
+ this.total -= instance.count - kept;
72
+ instance.data = data;
73
+ instance.count = kept;
74
+ }
75
+ instance.definition = definition;
76
+ instance.position = source.position;
77
+ instance.rotation = source.rotation;
78
+ instance.attached = true;
79
+ }
80
+ }
81
+ for (const [key, instance] of this.instances) {
82
+ if (!instance.attached && (instance.definition.space === "local" || instance.count === 0)) {
83
+ this.total -= instance.count;
84
+ this.instances.delete(key);
85
+ }
86
+ }
87
+ }
88
+ /** Applies `particles` presentation events from a session. Other event kinds are ignored. */
89
+ emit(events) {
90
+ for (const event of events) {
91
+ if (!isParticleEmission(event)) {
92
+ continue;
93
+ }
94
+ const instance = this.findInstance(event.entityId, event.emitter);
95
+ if (!instance || !instance.attached) {
96
+ continue;
97
+ }
98
+ if (event.count === undefined) {
99
+ instance.time = 0;
100
+ instance.accumulator = 0;
101
+ instance.playing = true;
102
+ }
103
+ else {
104
+ instance.pending += event.count;
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * Advances every particle, then emits new particles for the same interval. A step longer than
110
+ * {@link MAX_PARTICLE_STEP_SECONDS}, such as the first frame after a paused tab, is clamped to it.
111
+ */
112
+ step(seconds) {
113
+ if (!(seconds > 0)) {
114
+ return;
115
+ }
116
+ const dt = Math.min(seconds, MAX_PARTICLE_STEP_SECONDS);
117
+ const deaths = [];
118
+ for (const instance of this.instances.values()) {
119
+ this.advance(instance, dt, deaths);
120
+ }
121
+ for (const death of deaths) {
122
+ const instance = this.instances.get(instanceKey(death.entityId, death.emitter));
123
+ if (instance && (instance.attached || instance.definition.space === "world")) {
124
+ for (let index = 0; index < death.count; index += 1) {
125
+ if (!this.spawn(instance, death)) {
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ for (const instance of this.instances.values()) {
132
+ if (instance.attached) {
133
+ this.schedule(instance, dt);
134
+ }
135
+ }
136
+ for (const [key, instance] of this.instances) {
137
+ if (!instance.attached && instance.count === 0) {
138
+ this.instances.delete(key);
139
+ }
140
+ }
141
+ }
142
+ /** Visits every live particle in world space. */
143
+ forEachParticle(visit) {
144
+ for (const instance of this.instances.values()) {
145
+ const definition = instance.definition;
146
+ const data = instance.data;
147
+ const view = { entityId: instance.entityId, emitterId: definition.id, emitter: definition,
148
+ x: 0, y: 0, z: 0, size: 0, rotation: 0, r: 0, g: 0, b: 0, opacity: 0, life: 0 };
149
+ for (let index = 0; index < instance.count; index += 1) {
150
+ const base = index * STRIDE;
151
+ const life = Math.min(1, data[base + AGE] / data[base + LIFE]);
152
+ if (definition.space === "local") {
153
+ const point = transformPoint(instance.rotation, instance.position, data[base + PX], data[base + PY], data[base + PZ]);
154
+ view.x = point.x;
155
+ view.y = point.y;
156
+ view.z = point.z;
157
+ }
158
+ else {
159
+ view.x = data[base + PX];
160
+ view.y = data[base + PY];
161
+ view.z = data[base + PZ];
162
+ }
163
+ view.size = data[base + SIZE] * (definition.sizeOverLifetime ? evaluateParticleCurve(definition.sizeOverLifetime, life) : 1);
164
+ view.rotation = data[base + ROT];
165
+ view.r = data[base + CR];
166
+ view.g = data[base + CG];
167
+ view.b = data[base + CB];
168
+ if (definition.colorOverLifetime) {
169
+ const tint = evaluateParticleGradient(definition.colorOverLifetime, life, this.color);
170
+ view.r *= tint.r;
171
+ view.g *= tint.g;
172
+ view.b *= tint.b;
173
+ }
174
+ view.opacity = definition.opacity * (definition.opacityOverLifetime ? evaluateParticleCurve(definition.opacityOverLifetime, life) : 1);
175
+ view.life = life;
176
+ visit(view);
177
+ }
178
+ }
179
+ }
180
+ /** Removes every emitter and particle. */
181
+ clear() {
182
+ this.instances.clear();
183
+ this.emitterOrder.clear();
184
+ this.total = 0;
185
+ }
186
+ findInstance(entityId, emitterId) {
187
+ if (emitterId !== undefined) {
188
+ return this.instances.get(instanceKey(entityId, emitterId));
189
+ }
190
+ // The first emitter in document order. Every emitter of a synced entity is attached.
191
+ const first = this.emitterOrder.get(entityId)?.[0];
192
+ return first === undefined ? undefined : this.instances.get(instanceKey(entityId, first));
193
+ }
194
+ advance(instance, dt, deaths) {
195
+ const definition = instance.definition;
196
+ const data = instance.data;
197
+ const flat = this.dimension === "2d";
198
+ const gx = definition.gravity.x, gy = definition.gravity.y, gz = flat ? 0 : definition.gravity.z;
199
+ const damping = definition.drag > 0 ? 1 / (1 + definition.drag * dt) : 1;
200
+ let index = 0;
201
+ while (index < instance.count) {
202
+ const base = index * STRIDE;
203
+ const age = data[base + AGE] + dt;
204
+ if (age >= data[base + LIFE]) {
205
+ if (definition.onDeath.length > 0) {
206
+ const point = definition.space === "local"
207
+ ? transformPoint(instance.rotation, instance.position, data[base + PX], data[base + PY], data[base + PZ])
208
+ : { x: data[base + PX], y: data[base + PY], z: data[base + PZ] };
209
+ for (const sub of definition.onDeath) {
210
+ deaths.push({ entityId: instance.entityId, emitter: sub.emitter, count: sub.count, ...point });
211
+ }
212
+ }
213
+ const last = (instance.count - 1) * STRIDE;
214
+ if (last !== base) {
215
+ data.copyWithin(base, last, last + STRIDE);
216
+ }
217
+ instance.count -= 1;
218
+ this.total -= 1;
219
+ continue;
220
+ }
221
+ data[base + AGE] = age;
222
+ data[base + VX] = (data[base + VX] + gx * dt) * damping;
223
+ data[base + VY] = (data[base + VY] + gy * dt) * damping;
224
+ data[base + VZ] = (data[base + VZ] + gz * dt) * damping;
225
+ const speed = definition.speedOverLifetime ? evaluateParticleCurve(definition.speedOverLifetime, age / data[base + LIFE]) : 1;
226
+ data[base + PX] += data[base + VX] * speed * dt;
227
+ data[base + PY] += data[base + VY] * speed * dt;
228
+ data[base + PZ] += data[base + VZ] * speed * dt;
229
+ data[base + ROT] += data[base + SPIN] * dt;
230
+ index += 1;
231
+ }
232
+ }
233
+ /** Emits the rate and bursts that fall in [time, time + dt), wrapping looping cycles, plus pending counts. */
234
+ schedule(instance, dt) {
235
+ const definition = instance.definition;
236
+ let emitted = instance.pending;
237
+ instance.pending = 0;
238
+ let remaining = dt;
239
+ while (instance.playing && remaining > 0) {
240
+ const start = instance.time;
241
+ // Subtracting the segment length itself ends the loop exactly, so float rounding cannot leave a sliver.
242
+ const length = Math.max(0, Math.min(remaining, definition.duration - start));
243
+ const end = start + length;
244
+ instance.accumulator += definition.rate * length;
245
+ const whole = Math.floor(instance.accumulator + SAME_TIME);
246
+ instance.accumulator = Math.max(0, instance.accumulator - whole);
247
+ emitted += whole;
248
+ for (const burst of definition.bursts) {
249
+ for (let cycle = 0; cycle < burst.cycles; cycle += 1) {
250
+ const at = burst.time + cycle * burst.interval;
251
+ if (at >= start - SAME_TIME && at < end - SAME_TIME) {
252
+ emitted += burst.count;
253
+ }
254
+ }
255
+ }
256
+ remaining -= length;
257
+ instance.time = end;
258
+ if (instance.time >= definition.duration - SAME_TIME) {
259
+ if (definition.loop) {
260
+ instance.time = 0;
261
+ }
262
+ else {
263
+ instance.playing = false;
264
+ }
265
+ }
266
+ }
267
+ for (let index = 0; index < emitted; index += 1) {
268
+ if (!this.spawn(instance)) {
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ /** Spawns one particle at the emitter, or at `origin` (a world point) for a death emitter. */
274
+ spawn(instance, origin) {
275
+ const definition = instance.definition;
276
+ if (instance.count >= definition.maxParticles || this.total >= this.maxSceneParticles) {
277
+ return false;
278
+ }
279
+ const random = instance.random;
280
+ const shape = sampleParticleShape(definition.shape, this.dimension, random, this.sample);
281
+ const lifetime = sampleParticleRange(definition.lifetime, random);
282
+ const speed = sampleParticleRange(definition.speed, random);
283
+ const size = sampleParticleRange(definition.size, random);
284
+ const rotation = sampleParticleRange(definition.rotation, random);
285
+ const spin = sampleParticleRange(definition.angularVelocity, random);
286
+ const color = this.color;
287
+ if (typeof definition.color === "string") {
288
+ parseParticleColor(definition.color, color);
289
+ }
290
+ else {
291
+ const mix = random.next();
292
+ const from = parseParticleColor(definition.color.min);
293
+ const to = parseParticleColor(definition.color.max);
294
+ color.r = from.r + (to.r - from.r) * mix;
295
+ color.g = from.g + (to.g - from.g) * mix;
296
+ color.b = from.b + (to.b - from.b) * mix;
297
+ }
298
+ let position;
299
+ let direction;
300
+ if (definition.space === "world") {
301
+ const offset = rotate(instance.rotation, shape.px, shape.py, shape.pz);
302
+ const anchor = origin ?? instance.position;
303
+ position = { x: anchor.x + offset.x, y: anchor.y + offset.y, z: anchor.z + offset.z };
304
+ direction = rotate(instance.rotation, shape.dx, shape.dy, shape.dz);
305
+ }
306
+ else {
307
+ const anchor = origin ? inverseTransformPoint(instance.rotation, instance.position, origin) : { x: 0, y: 0, z: 0 };
308
+ position = { x: anchor.x + shape.px, y: anchor.y + shape.py, z: anchor.z + shape.pz };
309
+ direction = { x: shape.dx, y: shape.dy, z: shape.dz };
310
+ }
311
+ const flat = this.dimension === "2d";
312
+ const base = instance.count * STRIDE;
313
+ const data = instance.data;
314
+ data[base + PX] = position.x;
315
+ data[base + PY] = position.y;
316
+ data[base + PZ] = flat ? 0 : position.z;
317
+ data[base + VX] = direction.x * speed;
318
+ data[base + VY] = direction.y * speed;
319
+ data[base + VZ] = flat ? 0 : direction.z * speed;
320
+ data[base + AGE] = 0;
321
+ data[base + LIFE] = lifetime;
322
+ data[base + SIZE] = size;
323
+ data[base + ROT] = rotation;
324
+ data[base + SPIN] = spin;
325
+ data[base + CR] = color.r;
326
+ data[base + CG] = color.g;
327
+ data[base + CB] = color.b;
328
+ instance.count += 1;
329
+ this.total += 1;
330
+ return true;
331
+ }
332
+ }
333
+ /** Emitter sources from a 2D or 3D render frame. 2D rotation turns about Z. */
334
+ export function particleSourcesFromFrame(frame) {
335
+ if ("dimension" in frame) {
336
+ return frame.entities.flatMap((entity) => entity.particles
337
+ ? [{ entityId: entity.entityId, position: entity.transform.position, rotation: entity.transform.rotation, particles: entity.particles }]
338
+ : []);
339
+ }
340
+ return (frame.particles ?? []).map((emitter) => ({
341
+ entityId: emitter.entityId,
342
+ position: { x: emitter.x, y: emitter.y, z: 0 },
343
+ rotation: emitter.rotation === 0 ? IDENTITY : [0, 0, Math.sin(emitter.rotation / 2), Math.cos(emitter.rotation / 2)],
344
+ particles: emitter.particles
345
+ }));
346
+ }
347
+ function isParticleEmission(event) {
348
+ return event.kind === "particles";
349
+ }
350
+ function instanceKey(entityId, emitterId) {
351
+ return `${entityId}\u0000${emitterId}`;
352
+ }
353
+ function rotate(q, x, y, z) {
354
+ const [qx, qy, qz, qw] = q;
355
+ const tx = 2 * (qy * z - qz * y);
356
+ const ty = 2 * (qz * x - qx * z);
357
+ const tz = 2 * (qx * y - qy * x);
358
+ return {
359
+ x: x + qw * tx + (qy * tz - qz * ty),
360
+ y: y + qw * ty + (qz * tx - qx * tz),
361
+ z: z + qw * tz + (qx * ty - qy * tx)
362
+ };
363
+ }
364
+ function transformPoint(q, origin, x, y, z) {
365
+ const turned = rotate(q, x, y, z);
366
+ return { x: origin.x + turned.x, y: origin.y + turned.y, z: origin.z + turned.z };
367
+ }
368
+ function inverseTransformPoint(q, origin, point) {
369
+ return rotate([-q[0], -q[1], -q[2], q[3]], point.x - origin.x, point.y - origin.y, point.z - origin.z);
370
+ }
371
+ //# sourceMappingURL=simulator.js.map