@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,514 @@
1
+ /**
2
+ * Particle System Showcase
3
+ *
4
+ * Organized display of particle effects in labeled stations:
5
+ * - Fountain (water spray with gravity)
6
+ * - Fire (rising flames with color gradient)
7
+ * - Snow (gentle falling flakes)
8
+ * - Confetti (burst on click)
9
+ */
10
+
11
+ import {
12
+ Game,
13
+ GameObject,
14
+ Scene3D,
15
+ Rectangle,
16
+ TextShape,
17
+ Painter,
18
+ Camera3D,
19
+ ParticleSystem,
20
+ ParticleEmitter,
21
+ Updaters,
22
+ FPSCounter,
23
+ Position,
24
+ } from "../../src/index.js";
25
+
26
+ const CONFIG = {
27
+ backgroundColor: "#0a0a12",
28
+ maxParticles: 20000, // Stress test: 20k particles
29
+
30
+ // Station layout (as ratios of screen size)
31
+ stationWidthRatio: 0.18, // 18% of screen width per station
32
+ stationHeightRatio: 0.45, // 45% of screen height
33
+ stationSpacingRatio: 0.03, // 3% of screen width between stations
34
+ minStationWidth: 120,
35
+ maxStationWidth: 220,
36
+ minStationHeight: 150,
37
+ maxStationHeight: 280,
38
+
39
+ // Fountain settings
40
+ fountain: {
41
+ rate: 150, // 4x more
42
+ velocity: { x: 0, y: -350, z: 0 },
43
+ velocitySpread: { x: 40, y: 20, z: 40 },
44
+ lifetime: { min: 2, max: 4 }, // Longer life
45
+ size: { min: 3, max: 6 },
46
+ color: { r: 100, g: 180, b: 255, a: 1 },
47
+ },
48
+
49
+ // Fire settings
50
+ fire: {
51
+ rate: 250, // Dense flames
52
+ velocity: { x: 0, y: -60, z: 0 },
53
+ velocitySpread: { x: 15, y: 10, z: 15 },
54
+ lifetime: { min: 1, max: 2 }, // Longer life
55
+ size: { min: 6, max: 12 },
56
+ },
57
+
58
+ // Snow settings
59
+ snow: {
60
+ rate: 80, // 5x more
61
+ velocity: { x: 0, y: 40, z: 0 },
62
+ velocitySpread: { x: 10, y: 5, z: 10 },
63
+ lifetime: { min: 4, max: 7 }, // Longer life
64
+ size: { min: 2, max: 4 },
65
+ color: { r: 255, g: 255, b: 255, a: 0.9 },
66
+ },
67
+
68
+ // Confetti burst settings
69
+ confetti: {
70
+ velocity: { x: 0, y: -150, z: 0 },
71
+ velocitySpread: { x: 100, y: 60, z: 100 },
72
+ lifetime: { min: 2, max: 4 }, // Longer life
73
+ size: { min: 5, max: 10 },
74
+ },
75
+ };
76
+
77
+ /**
78
+ * Station box GameObject for displaying a particle effect
79
+ */
80
+ class ParticleStation extends GameObject {
81
+ constructor(game, x, y, width, height, label) {
82
+ super(game, { x, y, width, height });
83
+ this.z = 0; // For Scene3D projection
84
+ this.label = label;
85
+
86
+ // Background box using Rectangle shape
87
+ this.bg = new Rectangle({
88
+ width,
89
+ height,
90
+ color: "rgba(255, 255, 255, 0.03)",
91
+ stroke: "rgba(255, 255, 255, 0.15)",
92
+ lineWidth: 1,
93
+ });
94
+
95
+ // Label using TextShape
96
+ this.labelText = new TextShape(label, {
97
+ y: height / 2 - 15,
98
+ font: "12px monospace",
99
+ color: "#00FF00",
100
+ align: "center",
101
+ baseline: "middle",
102
+ });
103
+ this.labelText.x = x;
104
+ this.bg.x = x;
105
+ this.bg.y = y;
106
+ }
107
+
108
+ /**
109
+ * Update station size and position
110
+ */
111
+ updateLayout(x, width, height) {
112
+ this.x = x;
113
+ this.width = width;
114
+ this.height = height;
115
+
116
+ // Update background rectangle
117
+ this.bg.width = width;
118
+ this.bg.height = height;
119
+ this.bg.x = x;
120
+ this.bg.y = this.y;
121
+
122
+ // Update label position
123
+ this.labelText.y = height / 2 - 15;
124
+ this.labelText.x = x;
125
+ }
126
+
127
+ // Get emitter spawn position (center-bottom of station)
128
+ getEmitterPosition() {
129
+ return {
130
+ x: this.x,
131
+ y: this.y + this.height / 2 - 40,
132
+ z: 0,
133
+ };
134
+ }
135
+
136
+ // Get top position for snow
137
+ getTopPosition() {
138
+ return {
139
+ x: this.x,
140
+ y: -this.height / 2 + 10,
141
+ z: 0,
142
+ };
143
+ }
144
+
145
+ // Get bottom position for fountain/fire
146
+ getBottomPosition() {
147
+ return {
148
+ x: this.x,
149
+ y: this.height / 2 - 10,
150
+ z: 0,
151
+ };
152
+ }
153
+
154
+ // Get center position for confetti
155
+ getCenterPosition() {
156
+ return { x: this.x, y: this.y, z: 0 };
157
+ }
158
+
159
+ render() {
160
+ super.render();
161
+ this.bg.render();
162
+ this.labelText.render();
163
+ }
164
+ }
165
+
166
+ class ParticlesShowcase extends Game {
167
+ constructor(canvas) {
168
+ super(canvas);
169
+ this.backgroundColor = CONFIG.backgroundColor;
170
+ this.enableFluidSize();
171
+ }
172
+
173
+ init() {
174
+ super.init();
175
+
176
+ // Calculate initial sizes
177
+ this.updateScaledSizes();
178
+
179
+ // Setup Camera3D (subtle rotation for depth)
180
+ this.camera = new Camera3D({
181
+ rotationX: 0.2,
182
+ rotationY: 0,
183
+ perspective: 800,
184
+ autoRotate: false,
185
+ });
186
+ this.camera.enableMouseControl(this.canvas);
187
+
188
+ // Create stations layout
189
+ this.createStations();
190
+
191
+ // Create particle system with Camera3D
192
+ this.particles = new ParticleSystem(this, {
193
+ camera: this.camera,
194
+ depthSort: true,
195
+ maxParticles: CONFIG.maxParticles,
196
+ blendMode: "screen",
197
+ updaters: [
198
+ Updaters.velocity,
199
+ Updaters.lifetime,
200
+ Updaters.gravity(120),
201
+ Updaters.fadeOut,
202
+ this.fireColorUpdater.bind(this),
203
+ Updaters.shrink(0.2),
204
+ ],
205
+ });
206
+
207
+ // Create emitters for each station
208
+ this.createEmitters();
209
+
210
+ this.pipeline.add(this.particles);
211
+
212
+ // Confetti emitter (for bursts, not continuous)
213
+ this.confettiEmitter = new ParticleEmitter({
214
+ position: { x: 0, y: 0, z: 0 },
215
+ velocity: CONFIG.confetti.velocity,
216
+ velocitySpread: CONFIG.confetti.velocitySpread,
217
+ lifetime: CONFIG.confetti.lifetime,
218
+ size: CONFIG.confetti.size,
219
+ color: { r: 255, g: 255, b: 0, a: 1 },
220
+ rate: 0,
221
+ shape: "triangle",
222
+ });
223
+
224
+ // Track emitter state
225
+ this.emittersActive = true;
226
+
227
+ // Click to burst confetti
228
+ this.canvas.addEventListener("click", (e) => this.handleClick(e));
229
+
230
+ // Space to toggle emitters
231
+ window.addEventListener("keydown", (e) => {
232
+ if (e.code === "Space") {
233
+ this.toggleEmitters();
234
+ }
235
+ });
236
+
237
+ // FPS counter anchored to bottom right
238
+ this.fpsCounter = new FPSCounter(this, {
239
+ anchor: Position.BOTTOM_RIGHT,
240
+ anchorOffsetX: 0,
241
+ anchorOffsetY: 0,
242
+ color: "#0f0",
243
+ });
244
+ this.pipeline.add(this.fpsCounter);
245
+ }
246
+
247
+ createStations() {
248
+ const labels = ["Fountain", "Fire", "Snow", "Confetti"];
249
+ const totalWidth =
250
+ labels.length * this.stationWidth +
251
+ (labels.length - 1) * this.stationSpacing;
252
+ const startX = -totalWidth / 2 + this.stationWidth / 2;
253
+
254
+ // Create Scene3D to hold stations with camera projection
255
+ this.stationScene = new Scene3D(this, {
256
+ x: this.width / 2,
257
+ y: this.height / 2,
258
+ camera: this.camera,
259
+ depthSort: false, // Stations are all at z=0
260
+ scaleByDepth: true,
261
+ });
262
+
263
+ this.stations = labels.map((label, i) => {
264
+ const x = startX + i * (this.stationWidth + this.stationSpacing);
265
+ const station = new ParticleStation(
266
+ this,
267
+ x,
268
+ 0,
269
+ this.stationWidth,
270
+ this.stationHeight,
271
+ label,
272
+ );
273
+ this.stationScene.add(station);
274
+ return station;
275
+ });
276
+
277
+ // Add to pipeline (will render before particles)
278
+ this.pipeline.add(this.stationScene);
279
+ }
280
+
281
+ createEmitters() {
282
+ // Fountain (station 0) - bottom of station, shoots up
283
+ const fountainStation = this.stations[0];
284
+ const fountainPos = fountainStation.getBottomPosition();
285
+ this.particles.addEmitter(
286
+ "fountain",
287
+ new ParticleEmitter({
288
+ position: { x: fountainPos.x, y: fountainPos.y + 20, z: 0 },
289
+ spread: { x: 5, y: 0, z: 5 },
290
+ velocity: CONFIG.fountain.velocity,
291
+ velocitySpread: CONFIG.fountain.velocitySpread,
292
+ lifetime: CONFIG.fountain.lifetime,
293
+ size: CONFIG.fountain.size,
294
+ color: CONFIG.fountain.color,
295
+ rate: CONFIG.fountain.rate,
296
+ shape: "circle",
297
+ }),
298
+ );
299
+
300
+ // Fire (station 1) - bottom of station, flames rise up
301
+ const fireStation = this.stations[1];
302
+ const firePos = fireStation.getBottomPosition();
303
+ this.particles.addEmitter(
304
+ "fire",
305
+ new ParticleEmitter({
306
+ position: { x: firePos.x, y: firePos.y, z: 0 },
307
+ spread: { x: 60, y: 0, z: 30 },
308
+ velocity: CONFIG.fire.velocity,
309
+ velocitySpread: { x: 40, y: 15, z: 40 },
310
+ lifetime: CONFIG.fire.lifetime,
311
+ size: CONFIG.fire.size,
312
+ color: { r: 255, g: 120, b: 40, a: 1 },
313
+ rate: CONFIG.fire.rate,
314
+ shape: "square",
315
+ }),
316
+ );
317
+
318
+ // Snow (station 2) - top of station, falls down
319
+ const snowStation = this.stations[2];
320
+ const snowPos = snowStation.getTopPosition();
321
+ this.particles.addEmitter(
322
+ "snow",
323
+ new ParticleEmitter({
324
+ position: { x: snowPos.x, y: snowPos.y - 10, z: 0 },
325
+ spread: { x: this.stationWidth * 0.4, y: 0, z: 30 },
326
+ velocity: CONFIG.snow.velocity,
327
+ velocitySpread: CONFIG.snow.velocitySpread,
328
+ lifetime: CONFIG.snow.lifetime,
329
+ size: CONFIG.snow.size,
330
+ color: CONFIG.snow.color,
331
+ rate: CONFIG.snow.rate,
332
+ shape: "circle",
333
+ }),
334
+ );
335
+ }
336
+
337
+ fireColorUpdater(p, dt) {
338
+ // Only apply to fire particles (squares)
339
+ if (p.shape !== "square") return;
340
+
341
+ const t = p.progress;
342
+ // Orange -> Red -> Dark
343
+ p.color.r = Math.floor(255 * (1 - t * 0.2));
344
+ p.color.g = Math.floor(120 * (1 - t * 0.9));
345
+ p.color.b = Math.floor(40 * (1 - t));
346
+ }
347
+
348
+ handleClick(e) {
349
+ // Confetti always spawns at its station center
350
+ const confettiStation = this.stations[3];
351
+ const pos = confettiStation.getCenterPosition();
352
+
353
+ this.confettiEmitter.position.x = pos.x;
354
+ this.confettiEmitter.position.y = pos.y;
355
+ this.confettiEmitter.position.z = pos.z;
356
+
357
+ // Randomize confetti colors
358
+ const colors = [
359
+ { r: 255, g: 80, b: 80 },
360
+ { r: 80, g: 255, b: 80 },
361
+ { r: 80, g: 80, b: 255 },
362
+ { r: 255, g: 255, b: 80 },
363
+ { r: 255, g: 80, b: 255 },
364
+ { r: 80, g: 255, b: 255 },
365
+ ];
366
+
367
+ // Burst with random colors
368
+ for (let i = 0; i < 40; i++) {
369
+ const color = colors[Math.floor(Math.random() * colors.length)];
370
+ this.confettiEmitter.color = { ...color, a: 1 };
371
+ this.particles.burst(1, this.confettiEmitter);
372
+ }
373
+ }
374
+
375
+ toggleEmitters() {
376
+ this.emittersActive = !this.emittersActive;
377
+
378
+ for (const emitter of this.particles.emitters.values()) {
379
+ emitter.active = this.emittersActive;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Calculate responsive station sizes based on screen dimensions
385
+ */
386
+ updateScaledSizes() {
387
+ // Calculate station dimensions from ratios with min/max bounds
388
+ this.stationWidth = Math.max(
389
+ CONFIG.minStationWidth,
390
+ Math.min(CONFIG.maxStationWidth, this.width * CONFIG.stationWidthRatio),
391
+ );
392
+ this.stationHeight = Math.max(
393
+ CONFIG.minStationHeight,
394
+ Math.min(CONFIG.maxStationHeight, this.height * CONFIG.stationHeightRatio),
395
+ );
396
+ this.stationSpacing = this.width * CONFIG.stationSpacingRatio;
397
+ }
398
+
399
+ /**
400
+ * Update station positions and sizes, then update emitters to match
401
+ */
402
+ updateStationLayout() {
403
+ if (!this.stations) return;
404
+
405
+ const totalWidth =
406
+ this.stations.length * this.stationWidth +
407
+ (this.stations.length - 1) * this.stationSpacing;
408
+ const startX = -totalWidth / 2 + this.stationWidth / 2;
409
+
410
+ // Update each station's position and size
411
+ for (let i = 0; i < this.stations.length; i++) {
412
+ const x = startX + i * (this.stationWidth + this.stationSpacing);
413
+ this.stations[i].updateLayout(x, this.stationWidth, this.stationHeight);
414
+ }
415
+
416
+ // Update emitter positions to follow stations
417
+ this.updateEmitterPositions();
418
+ }
419
+
420
+ /**
421
+ * Update emitter positions to match station positions
422
+ */
423
+ updateEmitterPositions() {
424
+ if (!this.particles || !this.stations) return;
425
+
426
+ // Fountain emitter
427
+ const fountainEmitter = this.particles.emitters.get("fountain");
428
+ if (fountainEmitter) {
429
+ const pos = this.stations[0].getBottomPosition();
430
+ fountainEmitter.position.x = pos.x;
431
+ fountainEmitter.position.y = pos.y + 20;
432
+ }
433
+
434
+ // Fire emitter
435
+ const fireEmitter = this.particles.emitters.get("fire");
436
+ if (fireEmitter) {
437
+ const pos = this.stations[1].getBottomPosition();
438
+ fireEmitter.position.x = pos.x;
439
+ fireEmitter.position.y = pos.y;
440
+ }
441
+
442
+ // Snow emitter
443
+ const snowEmitter = this.particles.emitters.get("snow");
444
+ if (snowEmitter) {
445
+ const pos = this.stations[2].getTopPosition();
446
+ snowEmitter.position.x = pos.x;
447
+ snowEmitter.position.y = pos.y - 10;
448
+ snowEmitter.spread.x = this.stationWidth * 0.4;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Handle window resize
454
+ */
455
+ onResize() {
456
+ this.updateScaledSizes();
457
+ this.updateStationLayout();
458
+ }
459
+
460
+ update(dt) {
461
+ super.update(dt);
462
+ this.camera.update(dt);
463
+
464
+ // Keep stationScene centered
465
+ this.stationScene.x = this.width / 2;
466
+ this.stationScene.y = this.height / 2;
467
+ }
468
+
469
+ render() {
470
+ super.render();
471
+ // Stations are rendered by Scene3D through the pipeline
472
+ // Draw HUD on top
473
+ this.drawHUD();
474
+ }
475
+
476
+ drawHUD() {
477
+ Painter.useCtx((ctx) => {
478
+ ctx.font = "11px monospace";
479
+ ctx.textAlign = "left";
480
+
481
+ // Particle count
482
+ ctx.fillStyle = "#666";
483
+ ctx.fillText(
484
+ `Particles: ${this.particles.particleCount} / ${CONFIG.maxParticles}`,
485
+ 15,
486
+ this.height - 45,
487
+ );
488
+ ctx.fillText(`Pool: ${this.particles.poolSize}`, 15, this.height - 30);
489
+
490
+ // Emitter status
491
+ ctx.fillStyle = this.emittersActive ? "#8a8" : "#a88";
492
+ ctx.fillText(
493
+ `Emitters: ${this.emittersActive ? "ON" : "OFF"} (Space)`,
494
+ 15,
495
+ this.height - 15,
496
+ );
497
+
498
+ // Instructions (bottom center)
499
+ ctx.textAlign = "center";
500
+ ctx.fillStyle = "#444";
501
+ ctx.fillText(
502
+ "click to burst confetti | drag to orbit",
503
+ this.width / 2,
504
+ this.height - 15,
505
+ );
506
+ });
507
+ }
508
+ }
509
+
510
+ window.addEventListener("load", () => {
511
+ const canvas = document.getElementById("game");
512
+ const demo = new ParticlesShowcase(canvas);
513
+ demo.start();
514
+ });