@guinetik/gcanvas 1.0.4 → 1.0.5

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 (193) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/animations.html +31 -0
  3. package/dist/basic.html +38 -0
  4. package/dist/baskara.html +31 -0
  5. package/dist/bezier.html +35 -0
  6. package/dist/beziersignature.html +29 -0
  7. package/dist/blackhole.html +28 -0
  8. package/dist/blob.html +35 -0
  9. package/dist/coordinates.html +698 -0
  10. package/dist/cube3d.html +23 -0
  11. package/dist/demos.css +303 -0
  12. package/dist/dino.html +42 -0
  13. package/dist/easing.html +28 -0
  14. package/dist/events.html +195 -0
  15. package/dist/fluent.html +647 -0
  16. package/dist/fluid-simple.html +22 -0
  17. package/dist/fluid.html +37 -0
  18. package/dist/fractals.html +36 -0
  19. package/dist/gameobjects.html +626 -0
  20. package/dist/gcanvas.es.js +517 -0
  21. package/dist/gcanvas.es.min.js +1 -1
  22. package/dist/gcanvas.umd.js +1 -1
  23. package/dist/gcanvas.umd.min.js +1 -1
  24. package/dist/genart.html +26 -0
  25. package/dist/gendream.html +26 -0
  26. package/dist/group.html +36 -0
  27. package/dist/home.html +587 -0
  28. package/dist/hyperbolic001.html +23 -0
  29. package/dist/hyperbolic002.html +23 -0
  30. package/dist/hyperbolic003.html +23 -0
  31. package/dist/hyperbolic004.html +23 -0
  32. package/dist/hyperbolic005.html +22 -0
  33. package/dist/index.html +398 -0
  34. package/dist/isometric.html +34 -0
  35. package/dist/js/animations.js +452 -0
  36. package/dist/js/basic.js +204 -0
  37. package/dist/js/baskara.js +751 -0
  38. package/dist/js/bezier.js +692 -0
  39. package/dist/js/beziersignature.js +241 -0
  40. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  41. package/dist/js/blackhole/blackhole.obj.js +318 -0
  42. package/dist/js/blackhole/index.js +409 -0
  43. package/dist/js/blackhole/particle.js +56 -0
  44. package/dist/js/blackhole/starfield.obj.js +218 -0
  45. package/dist/js/blob.js +2276 -0
  46. package/dist/js/coordinates.js +840 -0
  47. package/dist/js/cube3d.js +789 -0
  48. package/dist/js/dino.js +1420 -0
  49. package/dist/js/easing.js +477 -0
  50. package/dist/js/fluent.js +183 -0
  51. package/dist/js/fluid-simple.js +253 -0
  52. package/dist/js/fluid.js +527 -0
  53. package/dist/js/fractals.js +932 -0
  54. package/dist/js/fractalworker.js +93 -0
  55. package/dist/js/gameobjects.js +176 -0
  56. package/dist/js/genart.js +268 -0
  57. package/dist/js/gendream.js +209 -0
  58. package/dist/js/group.js +140 -0
  59. package/dist/js/hyperbolic001.js +310 -0
  60. package/dist/js/hyperbolic002.js +388 -0
  61. package/dist/js/hyperbolic003.js +319 -0
  62. package/dist/js/hyperbolic004.js +345 -0
  63. package/dist/js/hyperbolic005.js +340 -0
  64. package/dist/js/info-toggle.js +25 -0
  65. package/dist/js/isometric.js +863 -0
  66. package/dist/js/kerr.js +1547 -0
  67. package/dist/js/lavalamp.js +590 -0
  68. package/dist/js/layout.js +354 -0
  69. package/dist/js/mondrian.js +285 -0
  70. package/dist/js/opacity.js +275 -0
  71. package/dist/js/painter.js +484 -0
  72. package/dist/js/particles-showcase.js +514 -0
  73. package/dist/js/particles.js +299 -0
  74. package/dist/js/patterns.js +397 -0
  75. package/dist/js/penrose/artifact.js +69 -0
  76. package/dist/js/penrose/blackhole.js +121 -0
  77. package/dist/js/penrose/constants.js +73 -0
  78. package/dist/js/penrose/game.js +943 -0
  79. package/dist/js/penrose/lore.js +278 -0
  80. package/dist/js/penrose/penrosescene.js +892 -0
  81. package/dist/js/penrose/ship.js +216 -0
  82. package/dist/js/penrose/sounds.js +211 -0
  83. package/dist/js/penrose/voidparticle.js +55 -0
  84. package/dist/js/penrose/voidscene.js +258 -0
  85. package/dist/js/penrose/voidship.js +144 -0
  86. package/dist/js/penrose/wormhole.js +46 -0
  87. package/dist/js/pipeline.js +555 -0
  88. package/dist/js/plane3d.js +256 -0
  89. package/dist/js/platformer.js +1579 -0
  90. package/dist/js/scene.js +304 -0
  91. package/dist/js/scenes.js +320 -0
  92. package/dist/js/schrodinger.js +410 -0
  93. package/dist/js/schwarzschild.js +1015 -0
  94. package/dist/js/shapes.js +628 -0
  95. package/dist/js/space/alien.js +171 -0
  96. package/dist/js/space/boom.js +98 -0
  97. package/dist/js/space/boss.js +353 -0
  98. package/dist/js/space/buff.js +73 -0
  99. package/dist/js/space/bullet.js +102 -0
  100. package/dist/js/space/constants.js +85 -0
  101. package/dist/js/space/game.js +1884 -0
  102. package/dist/js/space/hud.js +112 -0
  103. package/dist/js/space/laserbeam.js +179 -0
  104. package/dist/js/space/lightning.js +277 -0
  105. package/dist/js/space/minion.js +192 -0
  106. package/dist/js/space/missile.js +212 -0
  107. package/dist/js/space/player.js +430 -0
  108. package/dist/js/space/powerup.js +90 -0
  109. package/dist/js/space/starfield.js +58 -0
  110. package/dist/js/space/starpower.js +90 -0
  111. package/dist/js/spacetime.js +559 -0
  112. package/dist/js/sphere3d.js +229 -0
  113. package/dist/js/sprite.js +473 -0
  114. package/dist/js/starfaux/config.js +118 -0
  115. package/dist/js/starfaux/enemy.js +353 -0
  116. package/dist/js/starfaux/hud.js +78 -0
  117. package/dist/js/starfaux/index.js +482 -0
  118. package/dist/js/starfaux/laser.js +182 -0
  119. package/dist/js/starfaux/player.js +468 -0
  120. package/dist/js/starfaux/terrain.js +560 -0
  121. package/dist/js/study001.js +275 -0
  122. package/dist/js/study002.js +366 -0
  123. package/dist/js/study003.js +331 -0
  124. package/dist/js/study004.js +389 -0
  125. package/dist/js/study005.js +209 -0
  126. package/dist/js/study006.js +194 -0
  127. package/dist/js/study007.js +192 -0
  128. package/dist/js/study008.js +413 -0
  129. package/dist/js/svgtween.js +204 -0
  130. package/dist/js/tde/accretiondisk.js +471 -0
  131. package/dist/js/tde/blackhole.js +219 -0
  132. package/dist/js/tde/blackholescene.js +209 -0
  133. package/dist/js/tde/config.js +59 -0
  134. package/dist/js/tde/index.js +820 -0
  135. package/dist/js/tde/jets.js +290 -0
  136. package/dist/js/tde/lensedstarfield.js +154 -0
  137. package/dist/js/tde/tdestar.js +297 -0
  138. package/dist/js/tde/tidalstream.js +372 -0
  139. package/dist/js/tde_old/blackhole.obj.js +354 -0
  140. package/dist/js/tde_old/debris.obj.js +791 -0
  141. package/dist/js/tde_old/flare.obj.js +239 -0
  142. package/dist/js/tde_old/index.js +448 -0
  143. package/dist/js/tde_old/star.obj.js +812 -0
  144. package/dist/js/tetris/config.js +157 -0
  145. package/dist/js/tetris/grid.js +286 -0
  146. package/dist/js/tetris/index.js +1195 -0
  147. package/dist/js/tetris/renderer.js +634 -0
  148. package/dist/js/tetris/tetrominos.js +280 -0
  149. package/dist/js/tiles.js +312 -0
  150. package/dist/js/tweendemo.js +79 -0
  151. package/dist/js/visibility.js +102 -0
  152. package/dist/kerr.html +28 -0
  153. package/dist/lavalamp.html +27 -0
  154. package/dist/layouts.html +37 -0
  155. package/dist/logo.svg +4 -0
  156. package/dist/loop.html +84 -0
  157. package/dist/mondrian.html +32 -0
  158. package/dist/og_image.png +0 -0
  159. package/dist/opacity.html +36 -0
  160. package/dist/painter.html +39 -0
  161. package/dist/particles-showcase.html +28 -0
  162. package/dist/particles.html +24 -0
  163. package/dist/patterns.html +33 -0
  164. package/dist/penrose-game.html +31 -0
  165. package/dist/pipeline.html +737 -0
  166. package/dist/plane3d.html +24 -0
  167. package/dist/platformer.html +43 -0
  168. package/dist/scene.html +33 -0
  169. package/dist/scenes.html +96 -0
  170. package/dist/schrodinger.html +27 -0
  171. package/dist/schwarzschild.html +27 -0
  172. package/dist/shapes.html +16 -0
  173. package/dist/space.html +85 -0
  174. package/dist/spacetime.html +27 -0
  175. package/dist/sphere3d.html +24 -0
  176. package/dist/sprite.html +18 -0
  177. package/dist/starfaux.html +22 -0
  178. package/dist/study001.html +23 -0
  179. package/dist/study002.html +23 -0
  180. package/dist/study003.html +23 -0
  181. package/dist/study004.html +23 -0
  182. package/dist/study005.html +22 -0
  183. package/dist/study006.html +24 -0
  184. package/dist/study007.html +24 -0
  185. package/dist/study008.html +22 -0
  186. package/dist/svgtween.html +29 -0
  187. package/dist/tde.html +28 -0
  188. package/dist/tetris3d.html +25 -0
  189. package/dist/tiles.html +28 -0
  190. package/dist/transforms.html +400 -0
  191. package/dist/tween.html +45 -0
  192. package/dist/visibility.html +33 -0
  193. package/package.json +1 -1
@@ -0,0 +1,527 @@
1
+ /**
2
+ * Fluid & Gas Explorer Demo
3
+ *
4
+ * Advanced fluid simulation demo using FluidSystem with thermal physics.
5
+ * Demonstrates gas mode with heat zones, thermal convection, and temperature coloring.
6
+ *
7
+ * Features:
8
+ * - Smooth Particle Hydrodynamics for liquid behavior
9
+ * - Gas mode with thermal convection (hot rises, cold sinks)
10
+ * - Temperature-based coloring (blue=cold, red=hot)
11
+ * - Visual heat zone indicators
12
+ * - Mouse interaction (hover to stir, click to push)
13
+ */
14
+ import {
15
+ Game,
16
+ FluidSystem,
17
+ FPSCounter,
18
+ Painter,
19
+ Button,
20
+ ToggleButton,
21
+ Stepper,
22
+ Position,
23
+ Rectangle,
24
+ ShapeGOFactory,
25
+ HorizontalLayout,
26
+ } from "/gcanvas.es.min.js";
27
+ import { zoneTemperature } from "/gcanvas.es.min.js";
28
+ import { Easing } from "/gcanvas.es.min.js";
29
+
30
+ // Base particle size - all proportional values derive from this
31
+ const PARTICLE_SIZE = Math.PI * 10;
32
+
33
+ const CONFIG = {
34
+ particleSize: PARTICLE_SIZE,
35
+
36
+ sim: {
37
+ maxParticles: Math.floor(PARTICLE_SIZE * 11),
38
+ gravity: 200,
39
+ damping: 0.98,
40
+ bounce: 0.3,
41
+ maxSpeed: 400,
42
+ },
43
+ fluid: {
44
+ smoothingRadius: PARTICLE_SIZE * 2,
45
+ restDensity: 3.0,
46
+ pressureStiffness: 80,
47
+ nearPressureStiffness: 3,
48
+ viscosity: 0.005,
49
+ maxForce: 5000,
50
+ },
51
+ gas: {
52
+ interactionRadius: PARTICLE_SIZE * 4,
53
+ pressure: 150,
54
+ diffusion: 0.15,
55
+ drag: 0.02,
56
+ turbulence: 50,
57
+ buoyancy: 300,
58
+ sinking: 200,
59
+ repulsion: 300,
60
+ },
61
+ heat: {
62
+ enabled: true,
63
+ heatZone: 0.88,
64
+ coolZone: 0.25,
65
+ rate: 0.03,
66
+ heatMultiplier: 1.5,
67
+ coolMultiplier: 2.0,
68
+ middleMultiplier: 0.005,
69
+ transitionWidth: 0.08,
70
+ neutralTemp: 0.5,
71
+ deadZone: 0.15,
72
+ buoyancy: 300,
73
+ sinking: 200,
74
+ },
75
+ pointer: {
76
+ radius: PARTICLE_SIZE * 6,
77
+ push: 8000,
78
+ pull: 2000,
79
+ },
80
+ visuals: {
81
+ liquid: {
82
+ baseHue: 200,
83
+ hueRange: 40,
84
+ saturation: 75,
85
+ minLight: 50,
86
+ maxLight: 65,
87
+ },
88
+ gas: {
89
+ coldHue: 220,
90
+ hotHue: 0,
91
+ saturation: 85,
92
+ minLight: 45,
93
+ maxLight: 70,
94
+ },
95
+ alpha: 0.9,
96
+ },
97
+ ui: {
98
+ margin: 16,
99
+ width: 130,
100
+ height: 32,
101
+ spacing: 8,
102
+ },
103
+ container: {
104
+ marginX: 80,
105
+ marginY: 150,
106
+ strokeColor: "#22c55e",
107
+ strokeWidth: 2,
108
+ },
109
+ };
110
+
111
+ /**
112
+ * FluidGasGame - SPH-based fluid simulation demo using FluidSystem.
113
+ */
114
+ class FluidGasGame extends Game {
115
+ constructor(canvas) {
116
+ super(canvas);
117
+ this.backgroundColor = "#0a0e1a";
118
+ this.enableFluidSize();
119
+ this.pointer = { x: 0, y: 0, down: false };
120
+ this.mode = "liquid";
121
+ this.gravityOn = true;
122
+ }
123
+
124
+ init() {
125
+ super.init();
126
+ this.pointer.x = this.width * 0.5;
127
+ this.pointer.y = this.height * 0.5;
128
+
129
+ // Calculate container bounds
130
+ this._updateContainerBounds();
131
+
132
+ // Create container outline
133
+ const containerShape = new Rectangle({
134
+ width: this.bounds.w,
135
+ height: this.bounds.h,
136
+ stroke: CONFIG.container.strokeColor,
137
+ lineWidth: CONFIG.container.strokeWidth,
138
+ color: null,
139
+ });
140
+ this.containerRect = ShapeGOFactory.create(this, containerShape, {
141
+ x: this.bounds.x + this.bounds.w / 2,
142
+ y: this.bounds.y + this.bounds.h / 2,
143
+ });
144
+ this.pipeline.add(this.containerRect);
145
+
146
+ // Create FluidSystem - handles all physics internally
147
+ this.fluid = new FluidSystem(this, {
148
+ maxParticles: CONFIG.sim.maxParticles,
149
+ particleSize: CONFIG.particleSize,
150
+ bounds: this.bounds,
151
+ physics: "liquid",
152
+ gravity: CONFIG.sim.gravity,
153
+ damping: CONFIG.sim.damping,
154
+ bounce: CONFIG.sim.bounce,
155
+ maxSpeed: CONFIG.sim.maxSpeed,
156
+ fluid: CONFIG.fluid,
157
+ gas: CONFIG.gas,
158
+ heat: CONFIG.heat,
159
+ blendMode: "source-over",
160
+ });
161
+
162
+ // Spawn particles
163
+ this.fluid.spawn(CONFIG.sim.maxParticles);
164
+ this.pipeline.add(this.fluid);
165
+
166
+ // Build UI controls
167
+ this._buildUI();
168
+
169
+ // Input events
170
+ this.events.on("inputmove", (e) => {
171
+ this.pointer.x = e.x;
172
+ this.pointer.y = e.y;
173
+ });
174
+ this.events.on("inputdown", () => (this.pointer.down = true));
175
+ this.events.on("inputup", () => (this.pointer.down = false));
176
+ window.addEventListener("keydown", (e) => this._handleKey(e));
177
+
178
+ // Handle resize
179
+ this.onResize = () => this._handleResize();
180
+ }
181
+
182
+ /**
183
+ * Handle window resize
184
+ */
185
+ _handleResize() {
186
+ this._updateContainerBounds();
187
+
188
+ if (this.containerRect) {
189
+ this.containerRect.transform
190
+ .position(this.bounds.x + this.bounds.w / 2, this.bounds.y + this.bounds.h / 2)
191
+ .size(this.bounds.w, this.bounds.h);
192
+ }
193
+
194
+ if (this.fluid) {
195
+ this.fluid.setBounds(this.bounds);
196
+ }
197
+
198
+ if (this.buttonRow) this.buttonRow.markBoundsDirty();
199
+ if (this.stepperRow) this.stepperRow.markBoundsDirty();
200
+ }
201
+
202
+ update(dt) {
203
+ dt = Math.min(dt, 0.033);
204
+
205
+ // Update physics mode on FluidSystem
206
+ this.fluid.setPhysicsMode(this.mode);
207
+ this.fluid.gravityEnabled = this.gravityOn;
208
+
209
+ // Apply pointer forces
210
+ this._pointerForces(this.fluid.particles);
211
+
212
+ super.update(dt);
213
+
214
+ // Apply demo-specific coloring
215
+ this._applyColors(this.fluid.particles);
216
+ }
217
+
218
+ /**
219
+ * Render with optional heat zone visualization
220
+ */
221
+ render() {
222
+ super.render();
223
+
224
+ if (this.fluid.modeMix > 0.5 && this.bounds) {
225
+ this._drawHeatZones();
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Draw visual indicators for thermal zones
231
+ */
232
+ _drawHeatZones() {
233
+ const { heatZone, coolZone } = CONFIG.heat;
234
+ const { x, y, w, h } = this.bounds;
235
+ const ctx = this.ctx;
236
+
237
+ const coldZoneHeight = coolZone * h;
238
+ const hotZoneStart = heatZone * h;
239
+ const hotZoneHeight = h - hotZoneStart;
240
+
241
+ const alpha = Math.min(1, (this.fluid.modeMix - 0.5) * 4) * 0.25;
242
+
243
+ ctx.save();
244
+
245
+ // Cold zone at top
246
+ const coldGrad = ctx.createLinearGradient(x, y, x, y + coldZoneHeight);
247
+ coldGrad.addColorStop(0, `rgba(100, 150, 255, ${alpha})`);
248
+ coldGrad.addColorStop(1, `rgba(100, 150, 255, 0)`);
249
+ ctx.fillStyle = coldGrad;
250
+ ctx.fillRect(x, y, w, coldZoneHeight);
251
+
252
+ // Hot zone at bottom
253
+ const hotGrad = ctx.createLinearGradient(x, y + hotZoneStart, x, y + h);
254
+ hotGrad.addColorStop(0, `rgba(255, 100, 50, 0)`);
255
+ hotGrad.addColorStop(1, `rgba(255, 100, 50, ${alpha})`);
256
+ ctx.fillStyle = hotGrad;
257
+ ctx.fillRect(x, y + hotZoneStart, w, hotZoneHeight);
258
+
259
+ // Zone boundary lines
260
+ ctx.strokeStyle = `rgba(100, 150, 255, ${alpha * 0.8})`;
261
+ ctx.lineWidth = 1;
262
+ ctx.setLineDash([5, 5]);
263
+ ctx.beginPath();
264
+ ctx.moveTo(x, y + coldZoneHeight);
265
+ ctx.lineTo(x + w, y + coldZoneHeight);
266
+ ctx.stroke();
267
+
268
+ ctx.strokeStyle = `rgba(255, 100, 50, ${alpha * 0.8})`;
269
+ ctx.beginPath();
270
+ ctx.moveTo(x, y + hotZoneStart);
271
+ ctx.lineTo(x + w, y + hotZoneStart);
272
+ ctx.stroke();
273
+ ctx.setLineDash([]);
274
+
275
+ ctx.restore();
276
+ }
277
+
278
+ _buildUI() {
279
+ const { margin, width, height, spacing } = CONFIG.ui;
280
+ const buttonRowWidth = width * 3 + spacing * 2;
281
+ const buttonRowHeight = height;
282
+ const stepperWidth = 130;
283
+ const stepperHeight = 46;
284
+ const stepperRowWidth = stepperWidth * 4 - (spacing + 32);
285
+
286
+ // Button row
287
+ const buttonRow = new HorizontalLayout(this, {
288
+ width: buttonRowWidth,
289
+ height: buttonRowHeight,
290
+ spacing,
291
+ padding: 0,
292
+ anchor: Position.BOTTOM_LEFT,
293
+ margin: margin,
294
+ });
295
+
296
+ this.btnMode = new ToggleButton(this, {
297
+ width,
298
+ height,
299
+ text: "Mode: Liquid",
300
+ startToggled: false,
301
+ onToggle: (on) => {
302
+ this.mode = on ? "gas" : "liquid";
303
+ this.btnMode.text = on ? "Mode: Gas" : "Mode: Liquid";
304
+ },
305
+ });
306
+ buttonRow.add(this.btnMode);
307
+
308
+ this.btnGravity = new ToggleButton(this, {
309
+ width,
310
+ height,
311
+ text: "Gravity: On",
312
+ startToggled: true,
313
+ onToggle: (on) => {
314
+ this.gravityOn = on;
315
+ this.btnGravity.text = on ? "Gravity: On" : "Gravity: Off";
316
+ },
317
+ });
318
+ buttonRow.add(this.btnGravity);
319
+
320
+ this.btnReset = new Button(this, {
321
+ width,
322
+ height,
323
+ text: "Reset",
324
+ onClick: () => this.fluid.reset(),
325
+ });
326
+ buttonRow.add(this.btnReset);
327
+
328
+ // Stepper row
329
+ const stepperRow = new HorizontalLayout(this, {
330
+ width: stepperRowWidth,
331
+ height: stepperHeight,
332
+ spacing: spacing + 8,
333
+ padding: 0,
334
+ anchor: Position.BOTTOM_LEFT,
335
+ margin: margin,
336
+ anchorOffsetY: -(buttonRowHeight + spacing),
337
+ });
338
+
339
+ this.gravityStep = new Stepper(this, {
340
+ value: CONFIG.sim.gravity,
341
+ min: 0,
342
+ max: 500,
343
+ step: 25,
344
+ label: "Gravity",
345
+ valueWidth: 48,
346
+ buttonSize: 26,
347
+ height: 26,
348
+ onChange: (val) => {
349
+ this.fluid.config.gravity = val;
350
+ },
351
+ });
352
+ stepperRow.add(this.gravityStep);
353
+
354
+ this.viscosityStep = new Stepper(this, {
355
+ value: CONFIG.fluid.viscosity * 1000,
356
+ min: 0,
357
+ max: 100,
358
+ step: 5,
359
+ label: "Viscosity",
360
+ valueWidth: 48,
361
+ buttonSize: 26,
362
+ height: 26,
363
+ formatValue: (v) => (v / 1000).toFixed(2),
364
+ onChange: (val) => {
365
+ this.fluid.config.fluid.viscosity = val / 1000;
366
+ },
367
+ });
368
+ stepperRow.add(this.viscosityStep);
369
+
370
+ this.pressureStep = new Stepper(this, {
371
+ value: CONFIG.fluid.pressureStiffness,
372
+ min: 10,
373
+ max: 500,
374
+ step: 20,
375
+ label: "Pressure",
376
+ valueWidth: 48,
377
+ buttonSize: 26,
378
+ height: 26,
379
+ onChange: (val) => {
380
+ this.fluid.config.fluid.pressureStiffness = val;
381
+ },
382
+ });
383
+ stepperRow.add(this.pressureStep);
384
+
385
+ this.bounceStep = new Stepper(this, {
386
+ value: Math.round(CONFIG.sim.bounce * 100),
387
+ min: 0,
388
+ max: 100,
389
+ step: 5,
390
+ label: "Bounce",
391
+ valueWidth: 48,
392
+ buttonSize: 26,
393
+ height: 26,
394
+ formatValue: (v) => `${v}%`,
395
+ onChange: (val) => {
396
+ this.fluid.config.bounce = val / 100;
397
+ },
398
+ });
399
+ stepperRow.add(this.bounceStep);
400
+
401
+ this.pipeline.add(buttonRow);
402
+ this.pipeline.add(stepperRow);
403
+
404
+ this.buttonRow = buttonRow;
405
+ this.stepperRow = stepperRow;
406
+
407
+ this.pipeline.add(new FPSCounter(this, { anchor: "bottom-right" }));
408
+
409
+ buttonRow.markBoundsDirty();
410
+ stepperRow.markBoundsDirty();
411
+ }
412
+
413
+ /**
414
+ * Apply visual coloring based on mode and temperature
415
+ */
416
+ _applyColors(particles) {
417
+ const { liquid, gas, alpha } = CONFIG.visuals;
418
+ const maxSpeed = CONFIG.sim.maxSpeed;
419
+ const containerTop = this.bounds?.y || 0;
420
+ const containerHeight = this.bounds?.h || this.height || 1;
421
+
422
+ for (let i = 0; i < particles.length; i++) {
423
+ const p = particles[i];
424
+ const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
425
+ const speedNorm = Math.min(1, speed / maxSpeed);
426
+
427
+ let hue, saturation, light;
428
+
429
+ if (this.fluid.modeMix < 0.5) {
430
+ // LIQUID MODE: Blue water
431
+ hue = liquid.baseHue - speedNorm * liquid.hueRange;
432
+ saturation = liquid.saturation;
433
+ light = Easing.lerp(liquid.minLight, liquid.maxLight, 0.3 + speedNorm * 0.5);
434
+ } else {
435
+ // GAS MODE: Temperature-based coloring
436
+ const temp = p.custom.temperature ?? CONFIG.heat.neutralTemp;
437
+
438
+ // Blue -> Purple -> Magenta -> Red
439
+ if (temp < 0.33) {
440
+ hue = gas.coldHue + (280 - gas.coldHue) * (temp / 0.33);
441
+ } else if (temp < 0.66) {
442
+ hue = 280 + 40 * ((temp - 0.33) / 0.33);
443
+ } else {
444
+ hue = 320 + 40 * ((temp - 0.66) / 0.34);
445
+ if (hue >= 360) hue -= 360;
446
+ }
447
+ saturation = gas.saturation;
448
+ light = Easing.lerp(gas.minLight, gas.maxLight, 0.4 + temp * 0.4);
449
+ }
450
+
451
+ const [r, g, b] = Painter.colors.hslToRgb(hue, saturation, light);
452
+ p.color.r = r;
453
+ p.color.g = g;
454
+ p.color.b = b;
455
+ p.color.a = alpha;
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Apply pointer interaction forces
461
+ */
462
+ _pointerForces(particles) {
463
+ const { radius, push, pull } = CONFIG.pointer;
464
+ const r2 = radius * radius;
465
+ const mx = this.pointer.x;
466
+ const my = this.pointer.y;
467
+
468
+ for (let i = 0; i < particles.length; i++) {
469
+ const p = particles[i];
470
+ const dx = mx - p.x;
471
+ const dy = my - p.y;
472
+ const dist2 = dx * dx + dy * dy;
473
+ if (dist2 >= r2 || dist2 < 1) continue;
474
+
475
+ const dist = Math.sqrt(dist2);
476
+ const t = 1 - dist / radius;
477
+ const strength = (this.pointer.down ? -push : pull) * t * t;
478
+
479
+ // Apply directly to velocity (simpler than accumulating forces)
480
+ const dt = 0.016; // Approximate frame time
481
+ p.vx += (dx / dist) * strength * dt;
482
+ p.vy += (dy / dist) * strength * dt;
483
+ }
484
+ }
485
+
486
+ _updateContainerBounds() {
487
+ const { marginX, marginY } = CONFIG.container;
488
+ this.bounds = {
489
+ x: marginX,
490
+ y: marginY,
491
+ w: this.width - marginX * 2,
492
+ h: this.height - marginY * 2,
493
+ };
494
+ }
495
+
496
+ _handleKey(e) {
497
+ if (e.key === "1") {
498
+ this.mode = "liquid";
499
+ this.btnMode.toggle(false);
500
+ this.btnMode.text = "Mode: Liquid";
501
+ } else if (e.key === "2") {
502
+ this.mode = "gas";
503
+ this.btnMode.toggle(true);
504
+ this.btnMode.text = "Mode: Gas";
505
+ } else if (e.key === " ") {
506
+ e.preventDefault();
507
+ const newMode = this.mode === "liquid" ? "gas" : "liquid";
508
+ this.mode = newMode;
509
+ this.btnMode.toggle(newMode === "gas");
510
+ this.btnMode.text = newMode === "gas" ? "Mode: Gas" : "Mode: Liquid";
511
+ } else if (e.key === "r" || e.key === "R") {
512
+ this.fluid.reset();
513
+ } else if (e.key === "g" || e.key === "G") {
514
+ this.gravityOn = !this.gravityOn;
515
+ this.btnGravity.toggle(this.gravityOn);
516
+ this.btnGravity.text = this.gravityOn ? "Gravity: On" : "Gravity: Off";
517
+ }
518
+ }
519
+ }
520
+
521
+ export { FluidGasGame };
522
+
523
+ window.addEventListener("load", () => {
524
+ const canvas = document.getElementById("game");
525
+ const game = new FluidGasGame(canvas);
526
+ game.start();
527
+ });