@guinetik/gcanvas 1.0.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 (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ParticleEmitter } from "../../src/particle/emitter";
3
+ import { Particle } from "../../src/particle/particle";
4
+
5
+ describe("ParticleEmitter", () => {
6
+ describe("constructor", () => {
7
+ it("should initialize with default values", () => {
8
+ const emitter = new ParticleEmitter();
9
+
10
+ expect(emitter.rate).toBe(10);
11
+ expect(emitter.position).toEqual({ x: 0, y: 0, z: 0 });
12
+ expect(emitter.spread).toEqual({ x: 0, y: 0, z: 0 });
13
+ expect(emitter.velocity).toEqual({ x: 0, y: 0, z: 0 });
14
+ expect(emitter.velocitySpread).toEqual({ x: 0, y: 0, z: 0 });
15
+ expect(emitter.lifetime).toEqual({ min: 1, max: 2 });
16
+ expect(emitter.size).toEqual({ min: 1, max: 1 });
17
+ expect(emitter.color).toEqual({ r: 255, g: 255, b: 255, a: 1 });
18
+ expect(emitter.shape).toBe("circle");
19
+ expect(emitter.active).toBe(true);
20
+ });
21
+
22
+ it("should accept custom options", () => {
23
+ const emitter = new ParticleEmitter({
24
+ rate: 50,
25
+ position: { x: 100, y: 200 },
26
+ velocity: { y: -100 },
27
+ lifetime: { min: 0.5, max: 1.5 },
28
+ size: { min: 2, max: 5 },
29
+ color: { r: 255, g: 0, b: 0, a: 0.8 },
30
+ shape: "square",
31
+ active: false,
32
+ });
33
+
34
+ expect(emitter.rate).toBe(50);
35
+ expect(emitter.position).toEqual({ x: 100, y: 200, z: 0 });
36
+ expect(emitter.velocity).toEqual({ x: 0, y: -100, z: 0 });
37
+ expect(emitter.lifetime).toEqual({ min: 0.5, max: 1.5 });
38
+ expect(emitter.size).toEqual({ min: 2, max: 5 });
39
+ expect(emitter.color).toEqual({ r: 255, g: 0, b: 0, a: 0.8 });
40
+ expect(emitter.shape).toBe("square");
41
+ expect(emitter.active).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe("emit", () => {
46
+ it("should initialize particle with emitter settings", () => {
47
+ const emitter = new ParticleEmitter({
48
+ position: { x: 100, y: 200, z: 50 },
49
+ velocity: { x: 10, y: -20, z: 5 },
50
+ lifetime: { min: 2, max: 2 },
51
+ size: { min: 3, max: 3 },
52
+ color: { r: 128, g: 64, b: 32, a: 0.5 },
53
+ shape: "triangle",
54
+ });
55
+
56
+ const p = new Particle();
57
+ emitter.emit(p);
58
+
59
+ expect(p.x).toBe(100);
60
+ expect(p.y).toBe(200);
61
+ expect(p.z).toBe(50);
62
+ expect(p.vx).toBe(10);
63
+ expect(p.vy).toBe(-20);
64
+ expect(p.vz).toBe(5);
65
+ expect(p.lifetime).toBe(2);
66
+ expect(p.size).toBe(3);
67
+ expect(p.color).toEqual({ r: 128, g: 64, b: 32, a: 0.5 });
68
+ expect(p.shape).toBe("triangle");
69
+ expect(p.age).toBe(0);
70
+ expect(p.alive).toBe(true);
71
+ });
72
+
73
+ it("should apply position spread", () => {
74
+ const emitter = new ParticleEmitter({
75
+ position: { x: 0, y: 0, z: 0 },
76
+ spread: { x: 100, y: 100, z: 100 },
77
+ });
78
+
79
+ // Mock Math.random to return predictable values
80
+ const mockRandom = vi.spyOn(Math, "random");
81
+ mockRandom.mockReturnValue(0.5); // Mid-point = 0 spread
82
+
83
+ const p = new Particle();
84
+ emitter.emit(p);
85
+
86
+ // With Math.random() = 0.5, spread = (0.5 - 0.5) * 2 * spread = 0
87
+ expect(p.x).toBe(0);
88
+ expect(p.y).toBe(0);
89
+ expect(p.z).toBe(0);
90
+
91
+ mockRandom.mockRestore();
92
+ });
93
+
94
+ it("should apply velocity spread", () => {
95
+ const emitter = new ParticleEmitter({
96
+ velocity: { x: 100 },
97
+ velocitySpread: { x: 50 },
98
+ });
99
+
100
+ const mockRandom = vi.spyOn(Math, "random");
101
+ mockRandom.mockReturnValue(0); // (0 - 0.5) * 2 * 50 = -50
102
+
103
+ const p = new Particle();
104
+ emitter.emit(p);
105
+
106
+ expect(p.vx).toBe(50); // 100 + (-50)
107
+
108
+ mockRandom.mockRestore();
109
+ });
110
+
111
+ it("should randomize lifetime within range", () => {
112
+ const emitter = new ParticleEmitter({
113
+ lifetime: { min: 1, max: 3 },
114
+ });
115
+
116
+ const mockRandom = vi.spyOn(Math, "random");
117
+ mockRandom.mockReturnValue(0.5);
118
+
119
+ const p = new Particle();
120
+ emitter.emit(p);
121
+
122
+ expect(p.lifetime).toBe(2); // 1 + 0.5 * (3 - 1)
123
+
124
+ mockRandom.mockRestore();
125
+ });
126
+
127
+ it("should randomize size within range", () => {
128
+ const emitter = new ParticleEmitter({
129
+ size: { min: 2, max: 10 },
130
+ });
131
+
132
+ const mockRandom = vi.spyOn(Math, "random");
133
+ mockRandom.mockReturnValue(0.25);
134
+
135
+ const p = new Particle();
136
+ emitter.emit(p);
137
+
138
+ expect(p.size).toBe(4); // 2 + 0.25 * (10 - 2)
139
+
140
+ mockRandom.mockRestore();
141
+ });
142
+ });
143
+
144
+ describe("update", () => {
145
+ it("should return 0 when inactive", () => {
146
+ const emitter = new ParticleEmitter({ rate: 100, active: false });
147
+
148
+ expect(emitter.update(1)).toBe(0);
149
+ });
150
+
151
+ it("should return 0 when rate is 0", () => {
152
+ const emitter = new ParticleEmitter({ rate: 0 });
153
+
154
+ expect(emitter.update(1)).toBe(0);
155
+ });
156
+
157
+ it("should emit particles based on rate", () => {
158
+ const emitter = new ParticleEmitter({ rate: 10 }); // 10 particles/second
159
+
160
+ // 0.2 seconds should spawn 2 particles
161
+ const count = emitter.update(0.2);
162
+
163
+ expect(count).toBe(2);
164
+ });
165
+
166
+ it("should accumulate time between frames", () => {
167
+ const emitter = new ParticleEmitter({ rate: 10 }); // interval = 0.1s
168
+
169
+ // First frame: 0.05s (not enough for a particle)
170
+ expect(emitter.update(0.05)).toBe(0);
171
+
172
+ // Second frame: 0.05s more (total 0.1s = 1 particle)
173
+ expect(emitter.update(0.05)).toBe(1);
174
+ });
175
+
176
+ it("should handle high frame rates correctly", () => {
177
+ const emitter = new ParticleEmitter({ rate: 60 });
178
+
179
+ // One frame at 60fps = ~0.0167s
180
+ // At 60 particles/second, interval = 0.0167s, so 1 particle per frame
181
+ let total = 0;
182
+ for (let i = 0; i < 60; i++) {
183
+ total += emitter.update(1 / 60);
184
+ }
185
+
186
+ expect(total).toBe(60);
187
+ });
188
+ });
189
+
190
+ describe("reset", () => {
191
+ it("should reset the emission timer", () => {
192
+ const emitter = new ParticleEmitter({ rate: 10 });
193
+
194
+ // Accumulate some time
195
+ emitter.update(0.05);
196
+ expect(emitter._timer).toBeGreaterThan(0);
197
+
198
+ // Reset
199
+ emitter.reset();
200
+
201
+ expect(emitter._timer).toBe(0);
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,310 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ParticleSystem } from "../../src/particle/particle-system";
3
+ import { ParticleEmitter } from "../../src/particle/emitter";
4
+ import { Updaters } from "../../src/particle/updaters";
5
+ import { Particle } from "../../src/particle/particle";
6
+
7
+ // Mock the game object
8
+ const createMockGame = () => ({
9
+ width: 800,
10
+ height: 600,
11
+ });
12
+
13
+ describe("ParticleSystem", () => {
14
+ let mockGame;
15
+
16
+ beforeEach(() => {
17
+ mockGame = createMockGame();
18
+ });
19
+
20
+ describe("constructor", () => {
21
+ it("should initialize with default values", () => {
22
+ const system = new ParticleSystem(mockGame);
23
+
24
+ expect(system.particles).toEqual([]);
25
+ expect(system.pool).toEqual([]);
26
+ expect(system.maxParticles).toBe(5000);
27
+ expect(system.emitters).toBeInstanceOf(Map);
28
+ expect(system.emitters.size).toBe(0);
29
+ expect(system.camera).toBeNull();
30
+ expect(system.depthSort).toBe(false);
31
+ expect(system.blendMode).toBe("source-over");
32
+ expect(system.worldSpace).toBe(false);
33
+ expect(system.particleCount).toBe(0);
34
+ });
35
+
36
+ it("should accept custom options", () => {
37
+ const mockCamera = { project: vi.fn() };
38
+ const system = new ParticleSystem(mockGame, {
39
+ maxParticles: 1000,
40
+ camera: mockCamera,
41
+ depthSort: true,
42
+ blendMode: "screen",
43
+ worldSpace: true,
44
+ updaters: [Updaters.velocity],
45
+ });
46
+
47
+ expect(system.maxParticles).toBe(1000);
48
+ expect(system.camera).toBe(mockCamera);
49
+ expect(system.depthSort).toBe(true);
50
+ expect(system.blendMode).toBe("screen");
51
+ expect(system.worldSpace).toBe(true);
52
+ expect(system.updaters).toEqual([Updaters.velocity]);
53
+ });
54
+ });
55
+
56
+ describe("emitter management", () => {
57
+ it("should add emitters", () => {
58
+ const system = new ParticleSystem(mockGame);
59
+ const emitter = new ParticleEmitter({ rate: 10 });
60
+
61
+ system.addEmitter("fire", emitter);
62
+
63
+ expect(system.emitters.has("fire")).toBe(true);
64
+ expect(system.getEmitter("fire")).toBe(emitter);
65
+ });
66
+
67
+ it("should remove emitters", () => {
68
+ const system = new ParticleSystem(mockGame);
69
+ const emitter = new ParticleEmitter({ rate: 10 });
70
+
71
+ system.addEmitter("fire", emitter);
72
+ system.removeEmitter("fire");
73
+
74
+ expect(system.emitters.has("fire")).toBe(false);
75
+ });
76
+
77
+ it("should return undefined for non-existent emitter", () => {
78
+ const system = new ParticleSystem(mockGame);
79
+
80
+ expect(system.getEmitter("nonexistent")).toBeUndefined();
81
+ });
82
+
83
+ it("should support chaining on addEmitter/removeEmitter", () => {
84
+ const system = new ParticleSystem(mockGame);
85
+ const emitter = new ParticleEmitter();
86
+
87
+ const result1 = system.addEmitter("test", emitter);
88
+ const result2 = system.removeEmitter("test");
89
+
90
+ expect(result1).toBe(system);
91
+ expect(result2).toBe(system);
92
+ });
93
+ });
94
+
95
+ describe("object pooling", () => {
96
+ it("should acquire particles from pool when available", () => {
97
+ const system = new ParticleSystem(mockGame);
98
+
99
+ // Add particle to pool
100
+ const pooledParticle = new Particle();
101
+ system.pool.push(pooledParticle);
102
+
103
+ const acquired = system.acquire();
104
+
105
+ expect(acquired).toBe(pooledParticle);
106
+ expect(system.pool.length).toBe(0);
107
+ });
108
+
109
+ it("should create new particle when pool is empty", () => {
110
+ const system = new ParticleSystem(mockGame);
111
+
112
+ const acquired = system.acquire();
113
+
114
+ expect(acquired).toBeInstanceOf(Particle);
115
+ });
116
+
117
+ it("should release particles back to pool", () => {
118
+ const system = new ParticleSystem(mockGame);
119
+ const particle = new Particle();
120
+ particle.x = 100;
121
+ particle.y = 200;
122
+
123
+ system.release(particle);
124
+
125
+ expect(system.pool.length).toBe(1);
126
+ expect(system.pool[0]).toBe(particle);
127
+ // Particle should be reset
128
+ expect(particle.x).toBe(0);
129
+ expect(particle.y).toBe(0);
130
+ });
131
+ });
132
+
133
+ describe("emit", () => {
134
+ it("should emit particles using emitter", () => {
135
+ const system = new ParticleSystem(mockGame);
136
+ const emitter = new ParticleEmitter({
137
+ position: { x: 100, y: 200 },
138
+ });
139
+
140
+ system.emit(5, emitter);
141
+
142
+ expect(system.particles.length).toBe(5);
143
+ // particleCount is updated after update() call
144
+ });
145
+
146
+ it("should not exceed maxParticles", () => {
147
+ const system = new ParticleSystem(mockGame, { maxParticles: 3 });
148
+ const emitter = new ParticleEmitter();
149
+
150
+ system.emit(10, emitter);
151
+
152
+ expect(system.particles.length).toBe(3);
153
+ });
154
+ });
155
+
156
+ describe("burst", () => {
157
+ it("should burst spawn with emitter instance", () => {
158
+ const system = new ParticleSystem(mockGame);
159
+ const emitter = new ParticleEmitter();
160
+
161
+ system.burst(5, emitter);
162
+
163
+ expect(system.particles.length).toBe(5);
164
+ });
165
+
166
+ it("should burst spawn with emitter name", () => {
167
+ const system = new ParticleSystem(mockGame);
168
+ const emitter = new ParticleEmitter();
169
+ system.addEmitter("explosion", emitter);
170
+
171
+ system.burst(5, "explosion");
172
+
173
+ expect(system.particles.length).toBe(5);
174
+ });
175
+
176
+ it("should do nothing with invalid emitter name", () => {
177
+ const system = new ParticleSystem(mockGame);
178
+
179
+ system.burst(5, "nonexistent");
180
+
181
+ expect(system.particles.length).toBe(0);
182
+ });
183
+ });
184
+
185
+ describe("update", () => {
186
+ it("should update particles with all updaters", () => {
187
+ const customUpdater = vi.fn();
188
+ const system = new ParticleSystem(mockGame, {
189
+ updaters: [customUpdater],
190
+ });
191
+
192
+ const emitter = new ParticleEmitter({ lifetime: { min: 10, max: 10 } });
193
+ system.emit(2, emitter);
194
+
195
+ system.update(0.016);
196
+
197
+ expect(customUpdater).toHaveBeenCalledTimes(2);
198
+ });
199
+
200
+ it("should remove dead particles", () => {
201
+ const system = new ParticleSystem(mockGame, {
202
+ updaters: [Updaters.lifetime],
203
+ });
204
+
205
+ const emitter = new ParticleEmitter({
206
+ lifetime: { min: 0.1, max: 0.1 },
207
+ });
208
+ system.emit(3, emitter);
209
+
210
+ // Large time step to kill particles
211
+ system.update(1);
212
+
213
+ expect(system.particles.length).toBe(0);
214
+ });
215
+
216
+ it("should return dead particles to pool", () => {
217
+ const system = new ParticleSystem(mockGame, {
218
+ updaters: [Updaters.lifetime],
219
+ });
220
+
221
+ const emitter = new ParticleEmitter({
222
+ lifetime: { min: 0.1, max: 0.1 },
223
+ });
224
+ system.emit(3, emitter);
225
+
226
+ system.update(1);
227
+
228
+ expect(system.pool.length).toBe(3);
229
+ });
230
+
231
+ it("should spawn particles from active emitters", () => {
232
+ const system = new ParticleSystem(mockGame);
233
+ const emitter = new ParticleEmitter({
234
+ rate: 100, // 100 per second
235
+ lifetime: { min: 10, max: 10 },
236
+ });
237
+ system.addEmitter("continuous", emitter);
238
+
239
+ system.update(0.1); // 10 particles expected
240
+
241
+ expect(system.particles.length).toBe(10);
242
+ });
243
+
244
+ it("should not spawn from inactive emitters", () => {
245
+ const system = new ParticleSystem(mockGame);
246
+ const emitter = new ParticleEmitter({
247
+ rate: 100,
248
+ active: false,
249
+ });
250
+ system.addEmitter("disabled", emitter);
251
+
252
+ system.update(0.1);
253
+
254
+ expect(system.particles.length).toBe(0);
255
+ });
256
+ });
257
+
258
+ describe("clear", () => {
259
+ it("should remove all particles", () => {
260
+ const system = new ParticleSystem(mockGame);
261
+ const emitter = new ParticleEmitter();
262
+ system.emit(10, emitter);
263
+
264
+ system.clear();
265
+
266
+ expect(system.particles.length).toBe(0);
267
+ expect(system.particleCount).toBe(0);
268
+ });
269
+
270
+ it("should return particles to pool", () => {
271
+ const system = new ParticleSystem(mockGame);
272
+ const emitter = new ParticleEmitter();
273
+ system.emit(10, emitter);
274
+
275
+ system.clear();
276
+
277
+ expect(system.pool.length).toBe(10);
278
+ });
279
+ });
280
+
281
+ describe("stats", () => {
282
+ it("should track particle count", () => {
283
+ const system = new ParticleSystem(mockGame);
284
+ const emitter = new ParticleEmitter({
285
+ lifetime: { min: 10, max: 10 },
286
+ });
287
+
288
+ expect(system.particleCount).toBe(0);
289
+
290
+ system.emit(5, emitter);
291
+ system.update(0);
292
+
293
+ expect(system.particleCount).toBe(5);
294
+ });
295
+
296
+ it("should track pool size", () => {
297
+ const system = new ParticleSystem(mockGame);
298
+
299
+ expect(system.poolSize).toBe(0);
300
+
301
+ const emitter = new ParticleEmitter({
302
+ lifetime: { min: 0.01, max: 0.01 },
303
+ });
304
+ system.emit(5, emitter);
305
+ system.update(1); // Kill all
306
+
307
+ expect(system.poolSize).toBe(5);
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Particle } from "../../src/particle/particle";
3
+
4
+ describe("Particle", () => {
5
+ describe("constructor", () => {
6
+ it("should initialize with default values", () => {
7
+ const p = new Particle();
8
+
9
+ // Position
10
+ expect(p.x).toBe(0);
11
+ expect(p.y).toBe(0);
12
+ expect(p.z).toBe(0);
13
+
14
+ // Velocity
15
+ expect(p.vx).toBe(0);
16
+ expect(p.vy).toBe(0);
17
+ expect(p.vz).toBe(0);
18
+
19
+ // Appearance
20
+ expect(p.size).toBe(1);
21
+ expect(p.color).toEqual({ r: 255, g: 255, b: 255, a: 1 });
22
+ expect(p.shape).toBe("circle");
23
+
24
+ // Lifecycle
25
+ expect(p.age).toBe(0);
26
+ expect(p.lifetime).toBe(1);
27
+ expect(p.alive).toBe(true);
28
+
29
+ // Custom data
30
+ expect(p.custom).toEqual({});
31
+ });
32
+ });
33
+
34
+ describe("reset", () => {
35
+ it("should reset all properties to defaults", () => {
36
+ const p = new Particle();
37
+
38
+ // Modify all properties
39
+ p.x = 100;
40
+ p.y = 200;
41
+ p.z = 300;
42
+ p.vx = 10;
43
+ p.vy = 20;
44
+ p.vz = 30;
45
+ p.size = 5;
46
+ p.color = { r: 100, g: 50, b: 25, a: 0.5 };
47
+ p.shape = "square";
48
+ p.age = 2;
49
+ p.lifetime = 5;
50
+ p.alive = false;
51
+ p.custom.foo = "bar";
52
+
53
+ // Reset
54
+ p.reset();
55
+
56
+ // Verify defaults
57
+ expect(p.x).toBe(0);
58
+ expect(p.y).toBe(0);
59
+ expect(p.z).toBe(0);
60
+ expect(p.vx).toBe(0);
61
+ expect(p.vy).toBe(0);
62
+ expect(p.vz).toBe(0);
63
+ expect(p.size).toBe(1);
64
+ expect(p.color).toEqual({ r: 255, g: 255, b: 255, a: 1 });
65
+ expect(p.shape).toBe("circle");
66
+ expect(p.age).toBe(0);
67
+ expect(p.lifetime).toBe(1);
68
+ expect(p.alive).toBe(true);
69
+ expect(p.custom).toEqual({});
70
+ });
71
+
72
+ it("should clear custom data", () => {
73
+ const p = new Particle();
74
+ p.custom.key1 = "value1";
75
+ p.custom.key2 = "value2";
76
+
77
+ p.reset();
78
+
79
+ expect(Object.keys(p.custom).length).toBe(0);
80
+ });
81
+ });
82
+
83
+ describe("progress", () => {
84
+ it("should calculate progress as age/lifetime", () => {
85
+ const p = new Particle();
86
+ p.lifetime = 4;
87
+ p.age = 1;
88
+
89
+ expect(p.progress).toBe(0.25);
90
+ });
91
+
92
+ it("should return 0 at birth", () => {
93
+ const p = new Particle();
94
+ p.lifetime = 2;
95
+ p.age = 0;
96
+
97
+ expect(p.progress).toBe(0);
98
+ });
99
+
100
+ it("should return 1 at death", () => {
101
+ const p = new Particle();
102
+ p.lifetime = 2;
103
+ p.age = 2;
104
+
105
+ expect(p.progress).toBe(1);
106
+ });
107
+
108
+ it("should handle zero lifetime", () => {
109
+ const p = new Particle();
110
+ p.lifetime = 0;
111
+ p.age = 0;
112
+
113
+ expect(p.progress).toBe(1);
114
+ });
115
+ });
116
+ });