@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,322 @@
1
+ /**
2
+ * ParticleSystem - High-performance particle management GameObject
3
+ *
4
+ * Features:
5
+ * - Object pooling to minimize garbage collection
6
+ * - Composable updaters for physics and effects
7
+ * - Optional Camera3D integration with depth sorting
8
+ * - Multiple emitter support
9
+ * - Blend mode control
10
+ *
11
+ * @example
12
+ * const particles = new ParticleSystem(this, {
13
+ * camera: this.camera,
14
+ * depthSort: true,
15
+ * maxParticles: 3000,
16
+ * blendMode: "screen",
17
+ * updaters: [Updaters.velocity, Updaters.lifetime, Updaters.gravity(150)],
18
+ * });
19
+ * particles.addEmitter("fountain", new ParticleEmitter({ rate: 50 }));
20
+ * this.pipeline.add(particles);
21
+ */
22
+ import { GameObject } from "../game/objects/go.js";
23
+ import { Painter } from "../painter/painter.js";
24
+ import { Particle } from "./particle.js";
25
+ import { Updaters } from "./updaters.js";
26
+
27
+ export class ParticleSystem extends GameObject {
28
+ /**
29
+ * @param {Game} game - Game instance
30
+ * @param {Object} options - System configuration
31
+ * @param {number} [options.maxParticles=5000] - Maximum active particles
32
+ * @param {Camera3D} [options.camera] - Optional camera for 3D projection
33
+ * @param {boolean} [options.depthSort=false] - Enable depth sorting (requires camera)
34
+ * @param {string} [options.blendMode="source-over"] - Canvas blend mode
35
+ * @param {Function[]} [options.updaters] - Array of updater functions
36
+ * @param {boolean} [options.worldSpace=false] - Position particles in world space
37
+ */
38
+ constructor(game, options = {}) {
39
+ super(game, options);
40
+
41
+ // Particle storage
42
+ this.particles = [];
43
+ this.pool = [];
44
+ this.maxParticles = options.maxParticles ?? 5000;
45
+
46
+ // Emitters
47
+ this.emitters = new Map();
48
+
49
+ // Optional Camera3D for 3D projection
50
+ this.camera = options.camera ?? null;
51
+ this.depthSort = options.depthSort ?? false;
52
+
53
+ // Updaters (composable behaviors)
54
+ this.updaters = options.updaters ?? [
55
+ Updaters.velocity,
56
+ Updaters.lifetime,
57
+ ];
58
+
59
+ // Rendering
60
+ this.blendMode = options.blendMode ?? "source-over";
61
+ this.worldSpace = options.worldSpace ?? false;
62
+
63
+ // Stats
64
+ this._particleCount = 0;
65
+ }
66
+
67
+ /**
68
+ * Add an emitter to the system.
69
+ * @param {string} name - Emitter identifier
70
+ * @param {ParticleEmitter} emitter - Emitter instance
71
+ * @returns {ParticleSystem} this (for chaining)
72
+ */
73
+ addEmitter(name, emitter) {
74
+ this.emitters.set(name, emitter);
75
+ return this;
76
+ }
77
+
78
+ /**
79
+ * Remove an emitter from the system.
80
+ * @param {string} name - Emitter identifier
81
+ * @returns {ParticleSystem} this (for chaining)
82
+ */
83
+ removeEmitter(name) {
84
+ this.emitters.delete(name);
85
+ return this;
86
+ }
87
+
88
+ /**
89
+ * Get an emitter by name.
90
+ * @param {string} name - Emitter identifier
91
+ * @returns {ParticleEmitter|undefined}
92
+ */
93
+ getEmitter(name) {
94
+ return this.emitters.get(name);
95
+ }
96
+
97
+ /**
98
+ * Acquire a particle from pool or create new.
99
+ * @returns {Particle}
100
+ */
101
+ acquire() {
102
+ if (this.pool.length > 0) {
103
+ return this.pool.pop();
104
+ }
105
+ return new Particle();
106
+ }
107
+
108
+ /**
109
+ * Release a particle back to pool.
110
+ * @param {Particle} particle
111
+ */
112
+ release(particle) {
113
+ particle.reset();
114
+ this.pool.push(particle);
115
+ }
116
+
117
+ /**
118
+ * Emit particles using an emitter.
119
+ * @param {number} count - Number of particles to emit
120
+ * @param {ParticleEmitter} emitter - Emitter to use
121
+ */
122
+ emit(count, emitter) {
123
+ for (let i = 0; i < count && this.particles.length < this.maxParticles; i++) {
124
+ const p = this.acquire();
125
+ emitter.emit(p);
126
+ this.particles.push(p);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Burst spawn particles.
132
+ * @param {number} count - Number of particles
133
+ * @param {ParticleEmitter|string} emitterOrName - Emitter instance or name
134
+ */
135
+ burst(count, emitterOrName) {
136
+ const emitter = typeof emitterOrName === "string"
137
+ ? this.emitters.get(emitterOrName)
138
+ : emitterOrName;
139
+
140
+ if (emitter) {
141
+ this.emit(count, emitter);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Update all emitters and particles.
147
+ * @param {number} dt - Delta time in seconds
148
+ */
149
+ update(dt) {
150
+ super.update(dt);
151
+
152
+ // Update emitters and spawn particles
153
+ for (const emitter of this.emitters.values()) {
154
+ if (emitter.active) {
155
+ const count = emitter.update(dt);
156
+ this.emit(count, emitter);
157
+ }
158
+ }
159
+
160
+ // Update particles (iterate backwards for safe removal)
161
+ for (let i = this.particles.length - 1; i >= 0; i--) {
162
+ const p = this.particles[i];
163
+
164
+ // Apply all updaters
165
+ for (const updater of this.updaters) {
166
+ updater(p, dt, this);
167
+ }
168
+
169
+ // Remove dead particles
170
+ if (!p.alive) {
171
+ this.release(p);
172
+ this.particles.splice(i, 1);
173
+ }
174
+ }
175
+
176
+ this._particleCount = this.particles.length;
177
+ }
178
+
179
+ /**
180
+ * Render all particles.
181
+ */
182
+ render() {
183
+ super.render();
184
+
185
+ if (this.particles.length === 0) return;
186
+
187
+ if (this.camera && this.depthSort) {
188
+ this.renderWithDepthSort();
189
+ } else {
190
+ this.renderSimple();
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Simple 2D rendering (no depth sorting).
196
+ */
197
+ renderSimple() {
198
+ Painter.useCtx((ctx) => {
199
+ ctx.globalCompositeOperation = this.blendMode;
200
+
201
+ for (const p of this.particles) {
202
+ this.drawParticle(ctx, p, p.x, p.y, 1);
203
+ }
204
+
205
+ ctx.globalCompositeOperation = "source-over";
206
+ });
207
+ }
208
+
209
+ /**
210
+ * 3D rendering with Camera3D projection and depth sorting.
211
+ */
212
+ renderWithDepthSort() {
213
+ // Build render list with projections
214
+ const renderList = [];
215
+
216
+ for (const p of this.particles) {
217
+ const projected = this.camera.project(p.x, p.y, p.z);
218
+
219
+ // Cull particles behind camera
220
+ if (projected.z < -this.camera.perspective + 10) continue;
221
+
222
+ renderList.push({
223
+ p,
224
+ x: projected.x,
225
+ y: projected.y,
226
+ z: projected.z,
227
+ scale: projected.scale,
228
+ });
229
+ }
230
+
231
+ // Sort back to front
232
+ renderList.sort((a, b) => b.z - a.z);
233
+
234
+ // Draw all particles
235
+ Painter.useCtx((ctx) => {
236
+ ctx.globalCompositeOperation = this.blendMode;
237
+
238
+ // Translate to center if using camera (camera projects relative to origin)
239
+ // Only do this if NOT inside a Scene3D (which already handles projection/centering)
240
+ const isProjected = this.parent && this.parent.constructor.name === "Scene3D";
241
+
242
+ if (!this.worldSpace && !isProjected) {
243
+ ctx.save();
244
+ ctx.translate(this.game.width / 2, this.game.height / 2);
245
+ }
246
+
247
+ for (const item of renderList) {
248
+ this.drawParticle(ctx, item.p, item.x, item.y, item.scale);
249
+ }
250
+
251
+ if (!this.worldSpace && !isProjected) {
252
+ ctx.restore();
253
+ }
254
+
255
+ ctx.globalCompositeOperation = "source-over";
256
+ });
257
+ }
258
+
259
+ /**
260
+ * Draw a single particle.
261
+ * Override this method for custom particle rendering.
262
+ *
263
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
264
+ * @param {Particle} p - Particle to draw
265
+ * @param {number} x - Screen X position
266
+ * @param {number} y - Screen Y position
267
+ * @param {number} scale - Size scale factor (from perspective)
268
+ */
269
+ drawParticle(ctx, p, x, y, scale) {
270
+ const { r, g, b, a } = p.color;
271
+ const size = p.size * scale;
272
+
273
+ if (size < 0.5 || a <= 0) return;
274
+
275
+ ctx.fillStyle = `rgba(${Math.floor(r)},${Math.floor(g)},${Math.floor(b)},${a})`;
276
+
277
+ const shape = p.shape ?? "circle";
278
+ const half = size / 2;
279
+
280
+ ctx.beginPath();
281
+
282
+ if (shape === "circle") {
283
+ ctx.arc(x, y, half, 0, Math.PI * 2);
284
+ } else if (shape === "square") {
285
+ ctx.rect(x - half, y - half, size, size);
286
+ } else if (shape === "triangle") {
287
+ ctx.moveTo(x, y - half);
288
+ ctx.lineTo(x + half, y + half);
289
+ ctx.lineTo(x - half, y + half);
290
+ ctx.closePath();
291
+ }
292
+
293
+ ctx.fill();
294
+ }
295
+
296
+ /**
297
+ * Clear all particles and return them to pool.
298
+ */
299
+ clear() {
300
+ for (const p of this.particles) {
301
+ this.release(p);
302
+ }
303
+ this.particles = [];
304
+ this._particleCount = 0;
305
+ }
306
+
307
+ /**
308
+ * Get current particle count.
309
+ * @type {number}
310
+ */
311
+ get particleCount() {
312
+ return this._particleCount;
313
+ }
314
+
315
+ /**
316
+ * Get pool size (recycled particles ready for reuse).
317
+ * @type {number}
318
+ */
319
+ get poolSize() {
320
+ return this.pool.length;
321
+ }
322
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Particle - Lightweight data class for particle systems
3
+ *
4
+ * Plain object with no methods for performance (2500+ particles efficient).
5
+ * Uses object pooling via reset() to minimize garbage collection.
6
+ */
7
+ export class Particle {
8
+ constructor() {
9
+ // Position (3D capable)
10
+ this.x = 0;
11
+ this.y = 0;
12
+ this.z = 0;
13
+
14
+ // Velocity
15
+ this.vx = 0;
16
+ this.vy = 0;
17
+ this.vz = 0;
18
+
19
+ // Appearance
20
+ this.size = 1;
21
+ this.color = { r: 255, g: 255, b: 255, a: 1 };
22
+ this.shape = "circle"; // "circle", "square", "triangle"
23
+
24
+ // Lifecycle
25
+ this.age = 0;
26
+ this.lifetime = 1;
27
+ this.alive = true;
28
+
29
+ // Custom data (for domain-specific behaviors like orbital mechanics)
30
+ this.custom = {};
31
+ }
32
+
33
+ /**
34
+ * Reset all properties for object pooling.
35
+ * Called when particle is returned to pool.
36
+ */
37
+ reset() {
38
+ this.x = 0;
39
+ this.y = 0;
40
+ this.z = 0;
41
+
42
+ this.vx = 0;
43
+ this.vy = 0;
44
+ this.vz = 0;
45
+
46
+ this.size = 1;
47
+ this.color.r = 255;
48
+ this.color.g = 255;
49
+ this.color.b = 255;
50
+ this.color.a = 1;
51
+ this.shape = "circle";
52
+
53
+ this.age = 0;
54
+ this.lifetime = 1;
55
+ this.alive = true;
56
+
57
+ // Clear custom data
58
+ for (const key in this.custom) {
59
+ delete this.custom[key];
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Progress through lifetime (0 = just born, 1 = about to die).
65
+ * Useful for fade/shrink effects.
66
+ * @type {number}
67
+ */
68
+ get progress() {
69
+ return this.lifetime > 0 ? this.age / this.lifetime : 1;
70
+ }
71
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Updaters - Composable particle behavior functions
3
+ *
4
+ * Each updater is a function with signature: (particle, dt, system) => void
5
+ * Some updaters are factories that return the actual updater function.
6
+ *
7
+ * Usage:
8
+ * updaters: [Updaters.velocity, Updaters.lifetime, Updaters.gravity(200)]
9
+ */
10
+ export const Updaters = {
11
+ /**
12
+ * Apply velocity to position.
13
+ */
14
+ velocity: (p, dt) => {
15
+ p.x += p.vx * dt;
16
+ p.y += p.vy * dt;
17
+ p.z += p.vz * dt;
18
+ },
19
+
20
+ /**
21
+ * Track age and kill particle when lifetime exceeded.
22
+ */
23
+ lifetime: (p, dt) => {
24
+ p.age += dt;
25
+ if (p.age >= p.lifetime) {
26
+ p.alive = false;
27
+ }
28
+ },
29
+
30
+ /**
31
+ * Apply downward gravity.
32
+ * @param {number} [strength=200] - Gravity strength (pixels/second²)
33
+ * @returns {Function} Updater function
34
+ */
35
+ gravity: (strength = 200) => (p, dt) => {
36
+ p.vy += strength * dt;
37
+ },
38
+
39
+ /**
40
+ * Apply upward gravity (for rising particles like fire).
41
+ * @param {number} [strength=100] - Rise strength (pixels/second²)
42
+ * @returns {Function} Updater function
43
+ */
44
+ rise: (strength = 100) => (p, dt) => {
45
+ p.vy -= strength * dt;
46
+ },
47
+
48
+ /**
49
+ * Apply velocity damping/friction.
50
+ * @param {number} [factor=0.98] - Damping factor (0-1, lower = more friction)
51
+ * @returns {Function} Updater function
52
+ */
53
+ damping: (factor = 0.98) => (p, dt) => {
54
+ // Apply per-frame (not dt-based for simplicity)
55
+ p.vx *= factor;
56
+ p.vy *= factor;
57
+ p.vz *= factor;
58
+ },
59
+
60
+ /**
61
+ * Fade out alpha over lifetime.
62
+ */
63
+ fadeOut: (p, dt) => {
64
+ p.color.a = Math.max(0, 1 - p.progress);
65
+ },
66
+
67
+ /**
68
+ * Fade in then out (peak at 50% lifetime).
69
+ */
70
+ fadeInOut: (p, dt) => {
71
+ const t = p.progress;
72
+ p.color.a = t < 0.5 ? t * 2 : (1 - t) * 2;
73
+ },
74
+
75
+ /**
76
+ * Shrink size over lifetime.
77
+ * @param {number} [endScale=0] - Final size multiplier (0 = disappear)
78
+ * @returns {Function} Updater function
79
+ */
80
+ shrink: (endScale = 0) => {
81
+ // Store initial size on first call
82
+ return (p, dt) => {
83
+ if (p.custom._initialSize === undefined) {
84
+ p.custom._initialSize = p.size;
85
+ }
86
+ p.size = p.custom._initialSize * (1 - p.progress * (1 - endScale));
87
+ };
88
+ },
89
+
90
+ /**
91
+ * Grow size over lifetime.
92
+ * @param {number} [endScale=2] - Final size multiplier
93
+ * @returns {Function} Updater function
94
+ */
95
+ grow: (endScale = 2) => {
96
+ return (p, dt) => {
97
+ if (p.custom._initialSize === undefined) {
98
+ p.custom._initialSize = p.size;
99
+ }
100
+ p.size = p.custom._initialSize * (1 + p.progress * (endScale - 1));
101
+ };
102
+ },
103
+
104
+ /**
105
+ * Interpolate color over lifetime.
106
+ * @param {Object} startColor - Start color { r, g, b }
107
+ * @param {Object} endColor - End color { r, g, b }
108
+ * @returns {Function} Updater function
109
+ */
110
+ colorOverLife: (startColor, endColor) => (p, dt) => {
111
+ const t = p.progress;
112
+ p.color.r = Math.floor(startColor.r + (endColor.r - startColor.r) * t);
113
+ p.color.g = Math.floor(startColor.g + (endColor.g - startColor.g) * t);
114
+ p.color.b = Math.floor(startColor.b + (endColor.b - startColor.b) * t);
115
+ },
116
+
117
+ /**
118
+ * Add random wobble to velocity.
119
+ * @param {number} [strength=10] - Wobble strength
120
+ * @returns {Function} Updater function
121
+ */
122
+ wobble: (strength = 10) => (p, dt) => {
123
+ p.vx += (Math.random() - 0.5) * strength * dt;
124
+ p.vy += (Math.random() - 0.5) * strength * dt;
125
+ },
126
+
127
+ /**
128
+ * Bounce off screen boundaries.
129
+ * @param {Object} bounds - Boundary { left, right, top, bottom }
130
+ * @param {number} [bounce=0.8] - Bounce factor (0-1)
131
+ * @returns {Function} Updater function
132
+ */
133
+ bounds: (bounds, bounce = 0.8) => (p, dt) => {
134
+ if (p.x < bounds.left) {
135
+ p.x = bounds.left;
136
+ p.vx = Math.abs(p.vx) * bounce;
137
+ } else if (p.x > bounds.right) {
138
+ p.x = bounds.right;
139
+ p.vx = -Math.abs(p.vx) * bounce;
140
+ }
141
+
142
+ if (p.y < bounds.top) {
143
+ p.y = bounds.top;
144
+ p.vy = Math.abs(p.vy) * bounce;
145
+ } else if (p.y > bounds.bottom) {
146
+ p.y = bounds.bottom;
147
+ p.vy = -Math.abs(p.vy) * bounce;
148
+ }
149
+ },
150
+
151
+ /**
152
+ * Attract particles toward a point.
153
+ * @param {Object} target - Target position { x, y, z }
154
+ * @param {number} [strength=100] - Attraction strength
155
+ * @returns {Function} Updater function
156
+ */
157
+ attract: (target, strength = 100) => (p, dt) => {
158
+ const dx = target.x - p.x;
159
+ const dy = target.y - p.y;
160
+ const dz = (target.z ?? 0) - p.z;
161
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
162
+
163
+ if (dist > 1) {
164
+ const force = strength * dt / dist;
165
+ p.vx += dx * force;
166
+ p.vy += dy * force;
167
+ p.vz += dz * force;
168
+ }
169
+ },
170
+ };
@@ -0,0 +1,43 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ /**
5
+ * Arc - A circular arc (partial circle outline) without connecting to center.
6
+ */
7
+ export class Arc extends Shape {
8
+ /**
9
+ *
10
+ * @param {number} x
11
+ * @param {number} y
12
+ * @param {number} radius
13
+ * @param {number} startAngle - In radians
14
+ * @param {number} endAngle - In radians
15
+ * @param {object} options - Style options
16
+ */
17
+ constructor(radius, startAngle, endAngle, options = {}) {
18
+ super(options);
19
+ this.radius = radius;
20
+ this.startAngle = startAngle;
21
+ this.endAngle = endAngle;
22
+ }
23
+
24
+ draw() {
25
+ super.draw();
26
+ Painter.lines.beginPath();
27
+ Painter.shapes.arc(0, 0, this.radius, this.startAngle, this.endAngle, false);
28
+
29
+ if (this.stroke) {
30
+ Painter.colors.stroke(this.stroke, this.lineWidth);
31
+ }
32
+ }
33
+
34
+ getBounds() {
35
+ const r = this.radius;
36
+ return {
37
+ x: this.x,
38
+ y: this.y,
39
+ width: r * 2,
40
+ height: r * 2,
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,33 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+ export class Arrow extends Shape {
4
+ constructor(length, options = {}) {
5
+ super(options);
6
+ this.length = length;
7
+ }
8
+
9
+ draw() {
10
+ super.draw();
11
+ const halfW = this.width / 2;
12
+ const headLength = this.length * 0.4;
13
+ const shaftLength = this.length - headLength;
14
+
15
+ Painter.lines.beginPath();
16
+ Painter.lines.moveTo(-shaftLength / 2, -halfW);
17
+ Painter.lines.lineTo(shaftLength / 2, -halfW);
18
+ Painter.lines.lineTo(shaftLength / 2, -this.width);
19
+ Painter.lines.lineTo(this.length / 2, 0);
20
+ Painter.lines.lineTo(shaftLength / 2, this.width);
21
+ Painter.lines.lineTo(shaftLength / 2, halfW);
22
+ Painter.lines.lineTo(-shaftLength / 2, halfW);
23
+ Painter.lines.closePath();
24
+
25
+ if (this.color) {
26
+ Painter.colors.fill(this.color);
27
+ }
28
+
29
+ if (this.stroke) {
30
+ Painter.colors.stroke(this.stroke, this.lineWidth);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,42 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ /**
5
+ * BezierShape - A shape that renders any custom path using Painter.lines.path().
6
+ * Great for clouds, blobs, tails, ears, capes, bananas.
7
+ */
8
+ export class BezierShape extends Shape {
9
+ /**
10
+ *
11
+ * @param {number} x - Center X
12
+ * @param {number} y - Center Y
13
+ * @param {Array} path - An array of path commands [['M', x, y], ['C', ...], ['Z']]
14
+ * @param {object} options - color, stroke, etc
15
+ */
16
+ constructor(path = [], options = {}) {
17
+ //this.logger.log("new Bezier", options);
18
+ super(options);
19
+ this.path = path;
20
+ }
21
+
22
+ draw() {
23
+ super.draw();
24
+ Painter.lines.path(
25
+ this.path,
26
+ this.color,
27
+ this.stroke,
28
+ this.lineWidth
29
+ );
30
+ }
31
+
32
+ getBounds() {
33
+ // Not calculated from path (too complex); just approximate
34
+ const s = 50;
35
+ return {
36
+ x: this.x,
37
+ y: this.y,
38
+ width: s * 2,
39
+ height: s * 2,
40
+ };
41
+ }
42
+ }