@guinetik/gcanvas 1.0.0 → 1.0.1

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 (68) hide show
  1. package/demos/fluid-simple.html +22 -0
  2. package/demos/fluid.html +37 -0
  3. package/demos/index.html +2 -0
  4. package/demos/js/blob.js +18 -5
  5. package/demos/js/fluid-simple.js +253 -0
  6. package/demos/js/fluid.js +527 -0
  7. package/demos/js/tde/accretiondisk.js +64 -11
  8. package/demos/js/tde/blackholescene.js +2 -2
  9. package/demos/js/tde/config.js +2 -2
  10. package/demos/js/tde/index.js +152 -27
  11. package/demos/js/tde/lensedstarfield.js +32 -25
  12. package/demos/js/tde/tdestar.js +78 -98
  13. package/demos/js/tde/tidalstream.js +23 -7
  14. package/docs/README.md +230 -222
  15. package/docs/api/FluidSystem.md +173 -0
  16. package/docs/concepts/architecture-overview.md +204 -204
  17. package/docs/concepts/rendering-pipeline.md +279 -279
  18. package/docs/concepts/two-layer-architecture.md +229 -229
  19. package/docs/fluid-dynamics.md +97 -0
  20. package/docs/getting-started/first-game.md +354 -354
  21. package/docs/getting-started/installation.md +175 -157
  22. package/docs/modules/collision/README.md +2 -2
  23. package/docs/modules/fluent/README.md +6 -6
  24. package/docs/modules/game/README.md +303 -303
  25. package/docs/modules/isometric-camera.md +2 -2
  26. package/docs/modules/isometric.md +1 -1
  27. package/docs/modules/painter/README.md +328 -328
  28. package/docs/modules/particle/README.md +3 -3
  29. package/docs/modules/shapes/README.md +221 -221
  30. package/docs/modules/shapes/base/euclidian.md +123 -123
  31. package/docs/modules/shapes/base/shape.md +262 -262
  32. package/docs/modules/shapes/base/transformable.md +243 -243
  33. package/docs/modules/state/README.md +2 -2
  34. package/docs/modules/util/README.md +1 -1
  35. package/docs/modules/util/camera3d.md +3 -3
  36. package/docs/modules/util/scene3d.md +1 -1
  37. package/package.json +3 -1
  38. package/readme.md +19 -5
  39. package/src/collision/collision.js +75 -0
  40. package/src/game/index.js +2 -1
  41. package/src/game/pipeline.js +3 -3
  42. package/src/game/systems/FluidSystem.js +835 -0
  43. package/src/game/systems/index.js +11 -0
  44. package/src/game/ui/button.js +39 -18
  45. package/src/game/ui/cursor.js +14 -0
  46. package/src/game/ui/fps.js +12 -4
  47. package/src/game/ui/index.js +2 -0
  48. package/src/game/ui/stepper.js +549 -0
  49. package/src/game/ui/theme.js +121 -0
  50. package/src/game/ui/togglebutton.js +9 -3
  51. package/src/game/ui/tooltip.js +11 -4
  52. package/src/math/fluid.js +507 -0
  53. package/src/math/index.js +2 -0
  54. package/src/mixins/anchor.js +17 -7
  55. package/src/motion/tweenetik.js +16 -0
  56. package/src/shapes/index.js +1 -0
  57. package/src/util/camera3d.js +218 -12
  58. package/types/fluent.d.ts +361 -0
  59. package/types/game.d.ts +303 -0
  60. package/types/index.d.ts +144 -5
  61. package/types/math.d.ts +361 -0
  62. package/types/motion.d.ts +271 -0
  63. package/types/particle.d.ts +373 -0
  64. package/types/shapes.d.ts +107 -9
  65. package/types/util.d.ts +353 -0
  66. package/types/webgl.d.ts +109 -0
  67. package/disk_example.png +0 -0
  68. package/tde.png +0 -0
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Fluid System Demo (Simplified)</title>
7
+ <link rel="stylesheet" href="demos.css" />
8
+ <script src="./js/info-toggle.js"></script>
9
+ </head>
10
+ <body>
11
+ <div id="info">
12
+ <strong>FluidSystem Demo</strong> — Simplified fluid simulation.<br/>
13
+ <span style="color:#CCC">
14
+ <li>Click to push particles, hover to attract</li>
15
+ <li>Switch between Liquid and Gas physics</li>
16
+ </span>
17
+ </div>
18
+ <canvas id="game"></canvas>
19
+ <script type="module" src="./js/fluid-simple.js"></script>
20
+ </body>
21
+ </html>
22
+
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Fluid & Gas Playground</title>
8
+ <link rel="stylesheet" href="demos.css" />
9
+ <script src="./js/info-toggle.js"></script>
10
+ </head>
11
+
12
+ <body>
13
+ <div id="info">
14
+ <strong>Fluid & Gas Playground</strong> — SPH-like liquid + lightweight gas using math-only helpers.<br />
15
+ <span style="color:#9ef">
16
+ <li>Move mouse to stir, hold click to push.</li>
17
+ <li>Press 1 for Liquid, 2 for Gas, Space toggles.</li>
18
+ <li>Gas mode: blue=cold (top), red=hot (bottom).</li>
19
+ <li>Drag the browser window to shake the bottle!</li>
20
+ </span>
21
+ </div>
22
+ <canvas id="game"></canvas>
23
+
24
+ <script type="module">
25
+ import { FluidGasGame } from "./js/fluid.js";
26
+ window.addEventListener("load", () => {
27
+ const canvas = document.getElementById("game");
28
+ const game = new FluidGasGame(canvas);
29
+ game.setFPS(60);
30
+ game.start();
31
+ });
32
+ </script>
33
+ </body>
34
+
35
+ </html>
36
+
37
+
package/demos/index.html CHANGED
@@ -219,6 +219,8 @@
219
219
  <a href="baskara.html" target="demo-frame">Root Dance</a>
220
220
  <hr />
221
221
  <h2 style="margin-bottom: 0.3em">Math & Physics</h2>
222
+ <a href="fluid.html" target="demo-frame">Fluid Playground</a>
223
+ <a href="fluid-simple.html" target="demo-frame">Fluid System</a>
222
224
  <a href="schrodinger.html" target="demo-frame">Schrodinger Wave</a>
223
225
  <a href="spacetime.html" target="demo-frame">Spacetime Curvature</a>
224
226
  <a href="schwarzschild.html" target="demo-frame"
package/demos/js/blob.js CHANGED
@@ -243,6 +243,7 @@ class BlobScene extends Scene {
243
243
  // Set initial blob size (smaller)
244
244
  this.blobPhysics.baseRadius = CONFIG.startRadius;
245
245
  this.blobPhysics.currentRadius = CONFIG.startRadius;
246
+ this.blobPhysics.healthyRadius = CONFIG.startRadius; // Track healthy size before hunger effects
246
247
 
247
248
  // Control points around the blob (in polar coordinates for easy animation)
248
249
  this.blobPoints = [];
@@ -1095,11 +1096,16 @@ class BlobScene extends Scene {
1095
1096
  this.gameState.lastEatTime = 0;
1096
1097
  this.gameState.isHungry = false;
1097
1098
 
1098
- // Grow the blob
1099
+ // Grow the blob - restore to healthy radius plus growth
1099
1100
  const physics = this.blobPhysics;
1100
- const newRadius = Math.min(CONFIG.maxRadius, physics.baseRadius + CONFIG.growthPerCollect);
1101
+ // Ensure healthyRadius is initialized
1102
+ if (physics.healthyRadius === undefined) {
1103
+ physics.healthyRadius = physics.baseRadius;
1104
+ }
1105
+ const newRadius = Math.min(CONFIG.maxRadius, physics.healthyRadius + CONFIG.growthPerCollect);
1101
1106
  physics.baseRadius = newRadius;
1102
1107
  physics.currentRadius = newRadius;
1108
+ physics.healthyRadius = newRadius; // Update healthy radius to new size
1103
1109
 
1104
1110
  // Also give energy boost
1105
1111
  physics.energy = Math.min(1, physics.energy + 0.15);
@@ -1150,10 +1156,14 @@ class BlobScene extends Scene {
1150
1156
  const wasHungry = this.gameState.isHungry;
1151
1157
  this.gameState.isHungry = this.gameState.lastEatTime > hungerThreshold;
1152
1158
 
1153
- // If just became hungry, show warning
1159
+ // If just became hungry, show warning and capture healthy radius
1154
1160
  if (this.gameState.isHungry && !wasHungry) {
1155
1161
  this.showFloatingText("HUNGRY!", this.game.width / 2, this.game.height / 2);
1156
1162
  this.playHungryWarning();
1163
+ // Capture the current radius as the healthy radius when hunger starts
1164
+ if (physics.healthyRadius === undefined || physics.healthyRadius < physics.baseRadius) {
1165
+ physics.healthyRadius = physics.baseRadius;
1166
+ }
1157
1167
  }
1158
1168
 
1159
1169
  // If hungry, shrink and lose points
@@ -1169,7 +1179,7 @@ class BlobScene extends Scene {
1169
1179
  const hungerMultiplier = 1 + Math.min(hungerDuration / 2, 1);
1170
1180
  const shrinkAmount = shrinkRate * hungerMultiplier * dt;
1171
1181
 
1172
- // Apply shrinking
1182
+ // Apply shrinking from healthy radius (but don't modify healthyRadius)
1173
1183
  const newRadius = Math.max(CONFIG.minRadius, physics.baseRadius - shrinkAmount);
1174
1184
  if (newRadius < physics.baseRadius) {
1175
1185
  // Calculate score penalty
@@ -1177,7 +1187,7 @@ class BlobScene extends Scene {
1177
1187
  const scorePenalty = Math.ceil(radiusLost * CONFIG.shrinkScorePenalty);
1178
1188
  this.gameState.score = Math.max(0, this.gameState.score - scorePenalty);
1179
1189
 
1180
- // Apply size reduction
1190
+ // Apply size reduction (healthyRadius stays unchanged)
1181
1191
  physics.baseRadius = newRadius;
1182
1192
  physics.currentRadius = newRadius;
1183
1193
  }
@@ -1288,6 +1298,7 @@ class BlobScene extends Scene {
1288
1298
  this.collectionParticles = [];
1289
1299
  this.blobPhysics.baseRadius = CONFIG.startRadius;
1290
1300
  this.blobPhysics.currentRadius = CONFIG.startRadius;
1301
+ this.blobPhysics.healthyRadius = CONFIG.startRadius;
1291
1302
  // Reset color to normal
1292
1303
  this.blobPhysics.currentColor = [...this.blobPhysics.baseColor];
1293
1304
  // Reset pop sound scale
@@ -1357,6 +1368,7 @@ class BlobScene extends Scene {
1357
1368
  // Reset physics
1358
1369
  physics.baseRadius = CONFIG.startRadius;
1359
1370
  physics.currentRadius = CONFIG.startRadius;
1371
+ physics.healthyRadius = CONFIG.startRadius;
1360
1372
  physics.currentX = this.game.width / 2;
1361
1373
  physics.currentY = this.game.height / 2;
1362
1374
  physics.vx = 0;
@@ -2190,6 +2202,7 @@ class BlobUIScene extends Scene {
2190
2202
  // Reset physics - use CONFIG for starting size
2191
2203
  physics.baseRadius = CONFIG.startRadius;
2192
2204
  physics.currentRadius = CONFIG.startRadius;
2205
+ physics.healthyRadius = CONFIG.startRadius;
2193
2206
  physics.energy = 1.0;
2194
2207
  physics.baseColor = [64, 180, 255];
2195
2208
  physics.vx = 0;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Simplified Fluid Demo using FluidSystem
3
+ *
4
+ * Demonstrates how the FluidSystem class dramatically reduces
5
+ * the boilerplate needed for fluid simulations.
6
+ */
7
+ import {
8
+ Game,
9
+ FPSCounter,
10
+ FluidSystem,
11
+ applyAnchor,
12
+ Position,
13
+ Button,
14
+ ToggleButton,
15
+ HorizontalLayout,
16
+ } from "../../src/index.js";
17
+
18
+ const PARTICLE_SIZE = 32;
19
+
20
+ const CONFIG = {
21
+ particleSize: PARTICLE_SIZE,
22
+ maxParticles: 500,
23
+ gravity: 200,
24
+ container: {
25
+ marginX: 80,
26
+ marginY: 200,
27
+ strokeColor: "#22c55e",
28
+ strokeWidth: 2,
29
+ },
30
+ pointer: {
31
+ radius: PARTICLE_SIZE * 4,
32
+ push: 3000,
33
+ pull: 600,
34
+ },
35
+ ui: {
36
+ margin: 12,
37
+ width: 130,
38
+ height: 32,
39
+ spacing: 6,
40
+ },
41
+ };
42
+
43
+ /**
44
+ * Simplified fluid demo using FluidSystem.
45
+ */
46
+ class FluidSimpleDemo extends Game {
47
+ constructor(canvas) {
48
+ super(canvas);
49
+ this.backgroundColor = "#0f172a";
50
+ this.enableFluidSize();
51
+ this.pointer = { x: 0, y: 0, down: false };
52
+ }
53
+
54
+ init() {
55
+ super.init();
56
+
57
+ // Create container bounds
58
+ this._updateBounds();
59
+
60
+ // Create FluidSystem - ALL the physics is handled internally!
61
+ this.fluid = new FluidSystem(this, {
62
+ maxParticles: CONFIG.maxParticles,
63
+ particleSize: CONFIG.particleSize,
64
+ width: this.bounds.w,
65
+ height: this.bounds.h,
66
+ bounds: this.bounds,
67
+ physics: "liquid",
68
+ debug: true,
69
+ debugColor: CONFIG.container.strokeColor,
70
+ gravity: CONFIG.gravity,
71
+ particleColor: { r: 80, g: 180, b: 255, a: 0.9 },
72
+ });
73
+
74
+ // Spawn particles
75
+ this.fluid.spawn(CONFIG.maxParticles);
76
+
77
+ this.pipeline.add(this.fluid);
78
+
79
+ // Build UI controls
80
+ this._buildUI();
81
+
82
+ // FPS counter
83
+ this.pipeline.add(
84
+ new FPSCounter(this, { color: "#6af", anchor: "top-right" })
85
+ );
86
+
87
+ // Mouse interaction
88
+ this._setupInteraction();
89
+ }
90
+
91
+ /**
92
+ * Build the UI control bar.
93
+ */
94
+ _buildUI() {
95
+ const { margin, width, height, spacing } = CONFIG.ui;
96
+
97
+ // Create horizontal layout for buttons, anchored to bottom left
98
+ const uiPanel = new HorizontalLayout(this, {
99
+ width: width * 3 + spacing * 2,
100
+ height: height,
101
+ spacing,
102
+ padding: 0,
103
+ align: "center",
104
+ });
105
+ applyAnchor(uiPanel, {
106
+ anchor: Position.BOTTOM_LEFT,
107
+ anchorMargin: margin,
108
+ });
109
+
110
+ this.btnMode = new ToggleButton(this, {
111
+ width,
112
+ height,
113
+ text: "Mode: Liquid",
114
+ startToggled: false,
115
+ onToggle: (on) => {
116
+ this.fluid.setPhysicsMode(on ? "gas" : "liquid");
117
+ this.btnMode.text = on ? "Mode: Gas" : "Mode: Liquid";
118
+ },
119
+ });
120
+ uiPanel.add(this.btnMode);
121
+
122
+ this.btnGravity = new ToggleButton(this, {
123
+ width,
124
+ height,
125
+ text: "Gravity: On",
126
+ startToggled: true,
127
+ onToggle: (on) => {
128
+ this.fluid.gravityEnabled = on;
129
+ this.btnGravity.text = on ? "Gravity: On" : "Gravity: Off";
130
+ },
131
+ });
132
+ uiPanel.add(this.btnGravity);
133
+
134
+ this.btnReset = new Button(this, {
135
+ width,
136
+ height,
137
+ text: "Reset",
138
+ onClick: () => this.fluid.reset(),
139
+ });
140
+ uiPanel.add(this.btnReset);
141
+
142
+ this.pipeline.add(uiPanel);
143
+ }
144
+
145
+ _updateBounds() {
146
+ const { marginX, marginY } = CONFIG.container;
147
+ this.bounds = {
148
+ x: marginX,
149
+ y: marginY,
150
+ w: this.width - marginX * 2,
151
+ h: this.height - marginY * 2,
152
+ };
153
+ }
154
+
155
+ onResize() {
156
+ this._updateBounds();
157
+ if (this.fluid) {
158
+ this.fluid.setBounds(this.bounds);
159
+ }
160
+ }
161
+
162
+ _setupInteraction() {
163
+ // Track pointer position and state
164
+ this.events.on("inputmove", (e) => {
165
+ this.pointer.x = e.x;
166
+ this.pointer.y = e.y;
167
+ });
168
+ this.events.on("inputdown", () => (this.pointer.down = true));
169
+ this.events.on("inputup", () => (this.pointer.down = false));
170
+ }
171
+
172
+ /**
173
+ * Apply continuous pointer forces to particles.
174
+ * Push when clicking, gentle pull when hovering.
175
+ * @param {number} dt - Delta time
176
+ */
177
+ _applyPointerForces(dt) {
178
+ const { radius, push, pull } = CONFIG.pointer;
179
+ const r2 = radius * radius;
180
+ const mx = this.pointer.x;
181
+ const my = this.pointer.y;
182
+
183
+ for (const p of this.fluid.particles) {
184
+ const dx = mx - p.x;
185
+ const dy = my - p.y;
186
+ const dist2 = dx * dx + dy * dy;
187
+
188
+ if (dist2 >= r2 || dist2 < 1) continue;
189
+
190
+ const dist = Math.sqrt(dist2);
191
+ const t = 1 - dist / radius;
192
+ const strength = (this.pointer.down ? push : -pull) * t * t;
193
+
194
+ p.vx += (dx / dist) * strength * dt;
195
+ p.vy += (dy / dist) * strength * dt;
196
+ }
197
+ }
198
+
199
+ update(dt) {
200
+ // Apply pointer forces continuously
201
+ this._applyPointerForces(dt);
202
+
203
+ super.update(dt);
204
+ this._colorByVelocity();
205
+ }
206
+
207
+ /**
208
+ * Color particles by velocity (Sebastian Lague style).
209
+ */
210
+ _colorByVelocity() {
211
+ const maxSpeed = 300;
212
+
213
+ for (const p of this.fluid.particles) {
214
+ const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
215
+ const t = Math.min(1, speed / maxSpeed);
216
+
217
+ // Blue (slow) -> Cyan -> Green -> Yellow -> Orange (fast)
218
+ const hue = 210 - t * 180;
219
+ const sat = 80;
220
+ const light = 50 + t * 15;
221
+
222
+ // Simple HSL to RGB approximation
223
+ const c = ((1 - Math.abs((2 * light) / 100 - 1)) * sat) / 100;
224
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
225
+ const m = light / 100 - c / 2;
226
+
227
+ let r, g, b;
228
+ if (hue < 60) {
229
+ r = c; g = x; b = 0;
230
+ } else if (hue < 120) {
231
+ r = x; g = c; b = 0;
232
+ } else if (hue < 180) {
233
+ r = 0; g = c; b = x;
234
+ } else if (hue < 240) {
235
+ r = 0; g = x; b = c;
236
+ } else if (hue < 300) {
237
+ r = x; g = 0; b = c;
238
+ } else {
239
+ r = c; g = 0; b = x;
240
+ }
241
+
242
+ p.color.r = Math.round((r + m) * 255);
243
+ p.color.g = Math.round((g + m) * 255);
244
+ p.color.b = Math.round((b + m) * 255);
245
+ }
246
+ }
247
+ }
248
+
249
+ window.addEventListener("load", () => {
250
+ const canvas = document.getElementById("game");
251
+ const demo = new FluidSimpleDemo(canvas);
252
+ demo.start();
253
+ });