@guinetik/gcanvas 1.0.0 → 1.0.2
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.
- package/demos/coordinates.html +698 -0
- package/demos/cube3d.html +23 -0
- package/demos/demos.css +17 -3
- package/demos/dino.html +42 -0
- package/demos/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/gameobjects.html +626 -0
- package/demos/index.html +19 -7
- package/demos/js/blob.js +18 -5
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -0
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/gameobjects.js +176 -0
- package/demos/js/plane3d.js +256 -0
- package/demos/js/platformer.js +1579 -0
- package/demos/js/sphere3d.js +229 -0
- package/demos/js/sprite.js +473 -0
- package/demos/js/tde/accretiondisk.js +65 -12
- package/demos/js/tde/blackholescene.js +2 -2
- package/demos/js/tde/config.js +2 -2
- package/demos/js/tde/index.js +152 -27
- package/demos/js/tde/lensedstarfield.js +32 -25
- package/demos/js/tde/tdestar.js +78 -98
- package/demos/js/tde/tidalstream.js +24 -8
- package/demos/plane3d.html +24 -0
- package/demos/platformer.html +43 -0
- package/demos/sphere3d.html +24 -0
- package/demos/sprite.html +18 -0
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/coordinate-system.md +384 -0
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +99 -0
- package/docs/getting-started/first-game.md +354 -354
- package/docs/getting-started/installation.md +175 -157
- package/docs/modules/collision/README.md +2 -2
- package/docs/modules/fluent/README.md +6 -6
- package/docs/modules/game/README.md +303 -303
- package/docs/modules/isometric-camera.md +2 -2
- package/docs/modules/isometric.md +1 -1
- package/docs/modules/painter/README.md +328 -328
- package/docs/modules/particle/README.md +3 -3
- package/docs/modules/shapes/README.md +221 -221
- package/docs/modules/shapes/base/euclidian.md +123 -123
- package/docs/modules/shapes/base/shape.md +262 -262
- package/docs/modules/shapes/base/transformable.md +243 -243
- package/docs/modules/state/README.md +2 -2
- package/docs/modules/util/README.md +1 -1
- package/docs/modules/util/camera3d.md +3 -3
- package/docs/modules/util/scene3d.md +1 -1
- package/package.json +3 -1
- package/readme.md +19 -5
- package/src/collision/collision.js +75 -0
- package/src/game/game.js +11 -5
- package/src/game/index.js +2 -1
- package/src/game/objects/index.js +3 -0
- package/src/game/objects/platformer-scene.js +411 -0
- package/src/game/objects/scene.js +14 -0
- package/src/game/objects/sprite.js +529 -0
- package/src/game/pipeline.js +20 -16
- package/src/game/systems/FluidSystem.js +835 -0
- package/src/game/systems/index.js +11 -0
- package/src/game/ui/button.js +39 -18
- package/src/game/ui/cursor.js +14 -0
- package/src/game/ui/fps.js +12 -4
- package/src/game/ui/index.js +2 -0
- package/src/game/ui/stepper.js +549 -0
- package/src/game/ui/theme.js +123 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- package/src/math/fluid.js +507 -0
- package/src/math/index.js +2 -0
- package/src/mixins/anchor.js +17 -7
- package/src/motion/tweenetik.js +16 -0
- package/src/shapes/cube3d.js +599 -0
- package/src/shapes/index.js +3 -0
- package/src/shapes/plane3d.js +687 -0
- package/src/shapes/sphere3d.js +75 -6
- package/src/util/camera2d.js +315 -0
- package/src/util/camera3d.js +218 -12
- package/src/util/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
- package/types/fluent.d.ts +361 -0
- package/types/game.d.ts +303 -0
- package/types/index.d.ts +144 -5
- package/types/math.d.ts +361 -0
- package/types/motion.d.ts +271 -0
- package/types/particle.d.ts +373 -0
- package/types/shapes.d.ts +107 -9
- package/types/util.d.ts +353 -0
- package/types/webgl.d.ts +109 -0
- package/disk_example.png +0 -0
- package/tde.png +0 -0
|
@@ -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 "../../src/index.js";
|
|
27
|
+
import { zoneTemperature } from "../../src/math/heat.js";
|
|
28
|
+
import { Easing } from "../../src/motion/easing.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
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gameobjects.html - Live demos for Shapes vs GameObjects documentation
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
Circle,
|
|
6
|
+
Game,
|
|
7
|
+
Group,
|
|
8
|
+
Painter,
|
|
9
|
+
Rectangle,
|
|
10
|
+
Scene,
|
|
11
|
+
ShapeGOFactory,
|
|
12
|
+
Sprite,
|
|
13
|
+
} from "../../src/index.js";
|
|
14
|
+
|
|
15
|
+
// ==================== Demo 1: Shapes Only ====================
|
|
16
|
+
// Even "shapes only" needs Painter initialized, so we use a minimal Game
|
|
17
|
+
class ShapesDemo extends Game {
|
|
18
|
+
constructor(canvas) {
|
|
19
|
+
super(canvas);
|
|
20
|
+
this.backgroundColor = "#050505";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init() {
|
|
24
|
+
super.init();
|
|
25
|
+
// Shapes are created and rendered in render() below
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
super.render(); // Clears canvas
|
|
30
|
+
|
|
31
|
+
const centerY = this.canvas.height / 2;
|
|
32
|
+
|
|
33
|
+
// Create shapes - spread across canvas
|
|
34
|
+
const circle = new Circle(35, { x: this.canvas.width * 0.2, y: centerY, color: "#0f0" });
|
|
35
|
+
const rect = new Rectangle({ x: this.canvas.width * 0.5, y: centerY, width: 80, height: 50, color: "#0ff" });
|
|
36
|
+
|
|
37
|
+
// Group with rotation
|
|
38
|
+
const group = new Group({ x: this.canvas.width * 0.8, y: centerY, rotation: Math.PI / 4 });
|
|
39
|
+
group.add(new Circle(20, { color: "#f0f" }));
|
|
40
|
+
group.add(new Rectangle({ y: 30, width: 40, height: 20, color: "#ff0" }));
|
|
41
|
+
|
|
42
|
+
// Render directly - calling render() on shapes, not using pipeline
|
|
43
|
+
circle.render();
|
|
44
|
+
rect.render();
|
|
45
|
+
group.render();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function initShapesDemo() {
|
|
50
|
+
const canvas = document.getElementById("shapes-canvas");
|
|
51
|
+
if (!canvas) return;
|
|
52
|
+
|
|
53
|
+
const rect = canvas.getBoundingClientRect();
|
|
54
|
+
canvas.width = rect.width;
|
|
55
|
+
canvas.height = rect.height;
|
|
56
|
+
|
|
57
|
+
const demo = new ShapesDemo(canvas);
|
|
58
|
+
demo.start();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ==================== Demo 2: GameObjects with Pipeline ====================
|
|
62
|
+
class GameObjectsDemo extends Game {
|
|
63
|
+
constructor(canvas) {
|
|
64
|
+
super(canvas);
|
|
65
|
+
this.backgroundColor = "#050505";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
init() {
|
|
69
|
+
super.init();
|
|
70
|
+
|
|
71
|
+
const centerX = this.canvas.width / 2;
|
|
72
|
+
const centerY = this.canvas.height / 2;
|
|
73
|
+
|
|
74
|
+
// Create a scene (GameObject container)
|
|
75
|
+
this.scene = new Scene(this, { x: centerX, y: centerY });
|
|
76
|
+
|
|
77
|
+
// Create a sprite with animation (GameObject)
|
|
78
|
+
this.player = new Sprite(this, { frameRate: 8, loop: true, autoPlay: true });
|
|
79
|
+
|
|
80
|
+
// Add frames - pulsing circle animation
|
|
81
|
+
const colors = ["#0f0", "#0ff", "#0f0", "#ff0", "#0f0"];
|
|
82
|
+
const sizes = [20, 25, 30, 25, 20];
|
|
83
|
+
colors.forEach((color, i) => {
|
|
84
|
+
this.player.addFrame(new Circle(sizes[i], { color, stroke: "#fff", lineWidth: 2 }));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Add to scene
|
|
88
|
+
this.scene.add(this.player);
|
|
89
|
+
|
|
90
|
+
// Add scene to pipeline - now it's managed
|
|
91
|
+
this.pipeline.add(this.scene);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function initGameObjectsDemo() {
|
|
96
|
+
const canvas = document.getElementById("gameobjects-canvas");
|
|
97
|
+
if (!canvas) return;
|
|
98
|
+
|
|
99
|
+
// Set actual canvas size
|
|
100
|
+
const rect = canvas.getBoundingClientRect();
|
|
101
|
+
canvas.width = rect.width;
|
|
102
|
+
canvas.height = rect.height;
|
|
103
|
+
|
|
104
|
+
const demo = new GameObjectsDemo(canvas);
|
|
105
|
+
demo.start();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ==================== Demo 3: Bridging Shape to GameObject ====================
|
|
109
|
+
class BridgingDemo extends Game {
|
|
110
|
+
constructor(canvas) {
|
|
111
|
+
super(canvas);
|
|
112
|
+
this.backgroundColor = "#050505";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
init() {
|
|
116
|
+
super.init();
|
|
117
|
+
|
|
118
|
+
const centerX = this.canvas.width / 2;
|
|
119
|
+
const centerY = this.canvas.height / 2;
|
|
120
|
+
|
|
121
|
+
// Create a Group of shapes (like a simple avatar)
|
|
122
|
+
const avatar = new Group();
|
|
123
|
+
avatar.add(new Circle(25, { y: -30, color: "#0f0", stroke: "#0a0", lineWidth: 2 })); // head
|
|
124
|
+
avatar.add(new Rectangle({ y: 20, width: 40, height: 50, color: "#0f0", stroke: "#0a0", lineWidth: 2 })); // body
|
|
125
|
+
|
|
126
|
+
// Wrap it as a GameObject so it can join the pipeline
|
|
127
|
+
const avatarGO = ShapeGOFactory.create(this, avatar, {
|
|
128
|
+
x: centerX,
|
|
129
|
+
y: centerY,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Now it's a proper GameObject - add to pipeline
|
|
133
|
+
this.pipeline.add(avatarGO);
|
|
134
|
+
|
|
135
|
+
// Add a second avatar to show multiple instances
|
|
136
|
+
const avatar2 = new Group();
|
|
137
|
+
avatar2.add(new Circle(20, { y: -25, color: "#0ff", stroke: "#0aa", lineWidth: 2 }));
|
|
138
|
+
avatar2.add(new Rectangle({ y: 15, width: 30, height: 40, color: "#0ff", stroke: "#0aa", lineWidth: 2 }));
|
|
139
|
+
|
|
140
|
+
const avatar2GO = ShapeGOFactory.create(this, avatar2, {
|
|
141
|
+
x: centerX + 100,
|
|
142
|
+
y: centerY,
|
|
143
|
+
});
|
|
144
|
+
this.pipeline.add(avatar2GO);
|
|
145
|
+
|
|
146
|
+
// Third one with rotation
|
|
147
|
+
const avatar3 = new Group({ rotation: 0.2 });
|
|
148
|
+
avatar3.add(new Circle(18, { y: -22, color: "#f0f", stroke: "#a0a", lineWidth: 2 }));
|
|
149
|
+
avatar3.add(new Rectangle({ y: 12, width: 25, height: 35, color: "#f0f", stroke: "#a0a", lineWidth: 2 }));
|
|
150
|
+
|
|
151
|
+
const avatar3GO = ShapeGOFactory.create(this, avatar3, {
|
|
152
|
+
x: centerX - 100,
|
|
153
|
+
y: centerY,
|
|
154
|
+
});
|
|
155
|
+
this.pipeline.add(avatar3GO);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function initBridgingDemo() {
|
|
160
|
+
const canvas = document.getElementById("bridging-canvas");
|
|
161
|
+
if (!canvas) return;
|
|
162
|
+
|
|
163
|
+
const rect = canvas.getBoundingClientRect();
|
|
164
|
+
canvas.width = rect.width;
|
|
165
|
+
canvas.height = rect.height;
|
|
166
|
+
|
|
167
|
+
const demo = new BridgingDemo(canvas);
|
|
168
|
+
demo.start();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ==================== Initialize ====================
|
|
172
|
+
window.addEventListener("load", () => {
|
|
173
|
+
initShapesDemo();
|
|
174
|
+
initGameObjectsDemo();
|
|
175
|
+
initBridgingDemo();
|
|
176
|
+
});
|