@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.
- package/demos/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/index.html +2 -0
- package/demos/js/blob.js +18 -5
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/tde/accretiondisk.js +64 -11
- 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 +23 -7
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +97 -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/index.js +2 -1
- package/src/game/pipeline.js +3 -3
- 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 +121 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- 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/index.js +1 -0
- package/src/util/camera3d.js +218 -12
- 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
package/src/game/ui/tooltip.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { GameObject } from "../objects/go.js";
|
|
2
2
|
import { Rectangle, TextShape, Group } from "../../shapes/index.js";
|
|
3
|
+
import { UI_THEME } from "./theme.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Tooltip
|
|
6
7
|
*
|
|
7
8
|
* A GameObject that displays text near the cursor when shown.
|
|
8
9
|
* Supports multiline text with automatic word wrapping.
|
|
10
|
+
*
|
|
11
|
+
* Theme: Terminal × Vercel aesthetic
|
|
12
|
+
* - Dark translucent background
|
|
13
|
+
* - Subtle green border glow
|
|
14
|
+
* - Neon green monospace text
|
|
9
15
|
*
|
|
10
16
|
* Usage:
|
|
11
17
|
* const tooltip = new Tooltip(game, { ... });
|
|
@@ -34,10 +40,11 @@ export class Tooltip extends GameObject {
|
|
|
34
40
|
constructor(game, options = {}) {
|
|
35
41
|
super(game, { ...options, zIndex: 9999 }); // Always on top
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
43
|
+
// Terminal × Vercel theme defaults
|
|
44
|
+
this.font = options.font || UI_THEME.fonts.small;
|
|
45
|
+
this.textColor = options.textColor || UI_THEME.tooltip.text;
|
|
46
|
+
this.bgColor = options.bgColor || UI_THEME.tooltip.bg;
|
|
47
|
+
this.borderColor = options.borderColor || UI_THEME.tooltip.border;
|
|
41
48
|
this.padding = options.padding ?? 8;
|
|
42
49
|
this.offsetX = options.offsetX ?? 15;
|
|
43
50
|
this.offsetY = options.offsetY ?? 15;
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluid and Gas Dynamics (pure math utilities)
|
|
3
|
+
*
|
|
4
|
+
* Provides stateless helpers for 2D Smooth Particle Hydrodynamics (SPH) and a
|
|
5
|
+
* simplified gas model (diffusion/advection). Designed to be called by
|
|
6
|
+
* consumers (e.g., Game subclasses) that own particles; this module never
|
|
7
|
+
* mutates inputs.
|
|
8
|
+
*
|
|
9
|
+
* Key design points:
|
|
10
|
+
* - Pure functions (no hidden state); callers decide how to apply results.
|
|
11
|
+
* - Works on any particle-like objects with { x, y, vx, vy, size?, custom? }.
|
|
12
|
+
* - Configurable kernels, stiffness, viscosity, diffusion, and buoyancy.
|
|
13
|
+
* - Optional temperature coupling (pairs well with `src/math/heat.js`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Easing } from "../motion/easing.js";
|
|
17
|
+
|
|
18
|
+
const CONFIG = {
|
|
19
|
+
kernel: {
|
|
20
|
+
smoothingRadius: 28, // pixels
|
|
21
|
+
},
|
|
22
|
+
fluid: {
|
|
23
|
+
restDensity: 1.1,
|
|
24
|
+
particleMass: 1,
|
|
25
|
+
pressureStiffness: 1800,
|
|
26
|
+
nearPressureStiffness: 2.5, // Near pressure multiplier for stacking
|
|
27
|
+
viscosity: 0.18,
|
|
28
|
+
surfaceTension: 0,
|
|
29
|
+
maxForce: 6000,
|
|
30
|
+
},
|
|
31
|
+
gas: {
|
|
32
|
+
interactionRadius: 34,
|
|
33
|
+
pressure: 12,
|
|
34
|
+
diffusion: 0.08,
|
|
35
|
+
drag: 0.04,
|
|
36
|
+
buoyancy: 260,
|
|
37
|
+
neutralTemperature: 0.5,
|
|
38
|
+
turbulence: 16,
|
|
39
|
+
},
|
|
40
|
+
external: {
|
|
41
|
+
gravity: { x: 0, y: 820 }, // pixels/s²
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const EPS = 0.0001;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Deep-ish merge (one level) of config objects without mutating defaults.
|
|
49
|
+
* @param {Object} overrides
|
|
50
|
+
* @returns {Object}
|
|
51
|
+
*/
|
|
52
|
+
function mergeConfig(overrides = {}) {
|
|
53
|
+
return {
|
|
54
|
+
kernel: { ...CONFIG.kernel, ...(overrides.kernel || {}) },
|
|
55
|
+
fluid: { ...CONFIG.fluid, ...(overrides.fluid || {}) },
|
|
56
|
+
gas: { ...CONFIG.gas, ...(overrides.gas || {}) },
|
|
57
|
+
external: { ...CONFIG.external, ...(overrides.external || {}) },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* SPH kernel helpers (2D).
|
|
63
|
+
* Includes dual-density kernels for proper liquid stacking behavior.
|
|
64
|
+
*/
|
|
65
|
+
export const Kernels = {
|
|
66
|
+
/**
|
|
67
|
+
* Poly6 kernel (used for viscosity).
|
|
68
|
+
* @param {number} rSquared
|
|
69
|
+
* @param {number} h
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
poly6(rSquared, h) {
|
|
73
|
+
const h2 = h * h;
|
|
74
|
+
if (rSquared >= h2) return 0;
|
|
75
|
+
const factor = 4 / (Math.PI * Math.pow(h, 8));
|
|
76
|
+
const term = h2 - rSquared;
|
|
77
|
+
return factor * term * term * term;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Spiky kernel pow2 (used for regular density).
|
|
82
|
+
* @param {number} r - distance
|
|
83
|
+
* @param {number} h - smoothing radius
|
|
84
|
+
* @returns {number}
|
|
85
|
+
*/
|
|
86
|
+
spikyPow2(r, h) {
|
|
87
|
+
if (r >= h) return 0;
|
|
88
|
+
const factor = 6 / (Math.PI * Math.pow(h, 4));
|
|
89
|
+
const term = h - r;
|
|
90
|
+
return factor * term * term;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Spiky kernel pow3 (used for near density - sharper falloff).
|
|
95
|
+
* @param {number} r - distance
|
|
96
|
+
* @param {number} h - smoothing radius
|
|
97
|
+
* @returns {number}
|
|
98
|
+
*/
|
|
99
|
+
spikyPow3(r, h) {
|
|
100
|
+
if (r >= h) return 0;
|
|
101
|
+
const factor = 10 / (Math.PI * Math.pow(h, 5));
|
|
102
|
+
const term = h - r;
|
|
103
|
+
return factor * term * term * term;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Derivative of Spiky pow2 (used for pressure gradient).
|
|
108
|
+
* @param {number} r
|
|
109
|
+
* @param {number} h
|
|
110
|
+
* @returns {number}
|
|
111
|
+
*/
|
|
112
|
+
spikyPow2Derivative(r, h) {
|
|
113
|
+
if (r === 0 || r >= h) return 0;
|
|
114
|
+
const factor = -12 / (Math.PI * Math.pow(h, 4));
|
|
115
|
+
return factor * (h - r);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Derivative of Spiky pow3 (used for near pressure gradient).
|
|
120
|
+
* @param {number} r
|
|
121
|
+
* @param {number} h
|
|
122
|
+
* @returns {number}
|
|
123
|
+
*/
|
|
124
|
+
spikyPow3Derivative(r, h) {
|
|
125
|
+
if (r === 0 || r >= h) return 0;
|
|
126
|
+
const factor = -30 / (Math.PI * Math.pow(h, 5));
|
|
127
|
+
const term = h - r;
|
|
128
|
+
return factor * term * term;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Spiky kernel gradient magnitude (legacy, used for pressure).
|
|
133
|
+
* @param {number} r
|
|
134
|
+
* @param {number} h
|
|
135
|
+
* @returns {number}
|
|
136
|
+
*/
|
|
137
|
+
spikyGradient(r, h) {
|
|
138
|
+
if (r === 0 || r >= h) return 0;
|
|
139
|
+
const factor = -30 / (Math.PI * Math.pow(h, 5));
|
|
140
|
+
const term = (h - r) * (h - r);
|
|
141
|
+
return factor * term;
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Viscosity kernel laplacian (used for viscosity force).
|
|
146
|
+
* @param {number} r
|
|
147
|
+
* @param {number} h
|
|
148
|
+
* @returns {number}
|
|
149
|
+
*/
|
|
150
|
+
viscosityLaplacian(r, h) {
|
|
151
|
+
if (r >= h) return 0;
|
|
152
|
+
const factor = 40 / (Math.PI * Math.pow(h, 5));
|
|
153
|
+
return factor * (h - r);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Compute SPH densities for all particles (both regular and near density).
|
|
159
|
+
* Uses dual-density approach for proper liquid stacking.
|
|
160
|
+
* @param {Array<Object>} particles
|
|
161
|
+
* @param {Object} [overrides]
|
|
162
|
+
* @param {Object} [spatialHash]
|
|
163
|
+
* @returns {{ densities: Float32Array, nearDensities: Float32Array }}
|
|
164
|
+
*/
|
|
165
|
+
export function computeDensities(particles, overrides = {}, spatialHash) {
|
|
166
|
+
const cfg = mergeConfig(overrides);
|
|
167
|
+
const h = cfg.kernel.smoothingRadius;
|
|
168
|
+
const h2 = h * h;
|
|
169
|
+
const hash = spatialHash ?? buildSpatialHash(particles, h);
|
|
170
|
+
const n = particles.length;
|
|
171
|
+
const densities = new Float32Array(n);
|
|
172
|
+
const nearDensities = new Float32Array(n);
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < n; i++) {
|
|
175
|
+
// Self contribution
|
|
176
|
+
densities[i] = Kernels.spikyPow2(0, h);
|
|
177
|
+
nearDensities[i] = Kernels.spikyPow3(0, h);
|
|
178
|
+
|
|
179
|
+
forEachNeighbor(i, particles, hash, h2, (j, dx, dy, r2) => {
|
|
180
|
+
const r = Math.sqrt(r2);
|
|
181
|
+
densities[i] += Kernels.spikyPow2(r, h);
|
|
182
|
+
nearDensities[i] += Kernels.spikyPow3(r, h);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { densities, nearDensities };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compute pressures from densities (both regular and near pressure).
|
|
191
|
+
* @param {Float32Array|number[]} densities
|
|
192
|
+
* @param {Float32Array|number[]} nearDensities
|
|
193
|
+
* @param {Object} [overrides]
|
|
194
|
+
* @returns {{ pressures: Float32Array, nearPressures: Float32Array }}
|
|
195
|
+
*/
|
|
196
|
+
export function computePressures(densities, nearDensities, overrides = {}) {
|
|
197
|
+
const cfg = mergeConfig(overrides);
|
|
198
|
+
const n = densities.length;
|
|
199
|
+
const pressures = new Float32Array(n);
|
|
200
|
+
const nearPressures = new Float32Array(n);
|
|
201
|
+
const { pressureStiffness, nearPressureStiffness, restDensity } = cfg.fluid;
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < n; i++) {
|
|
204
|
+
// Regular pressure: pushes particles apart when density > rest density
|
|
205
|
+
pressures[i] = (densities[i] - restDensity) * pressureStiffness;
|
|
206
|
+
// Near pressure: always positive, prevents complete overlap
|
|
207
|
+
nearPressures[i] = nearDensities[i] * nearPressureStiffness;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { pressures, nearPressures };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Compute SPH pressure + viscosity forces using dual-density relaxation.
|
|
215
|
+
* This approach uses both regular pressure and near pressure for proper
|
|
216
|
+
* liquid stacking behavior (like the Unity reference implementation).
|
|
217
|
+
*
|
|
218
|
+
* @param {Array<Object>} particles
|
|
219
|
+
* @param {Object} [overrides]
|
|
220
|
+
* @returns {{ forces: Array<{x:number,y:number}>, densities: Float32Array, pressures: Float32Array }}
|
|
221
|
+
*/
|
|
222
|
+
export function computeFluidForces(particles, overrides = {}) {
|
|
223
|
+
const cfg = mergeConfig(overrides);
|
|
224
|
+
const h = cfg.kernel.smoothingRadius;
|
|
225
|
+
const h2 = h * h;
|
|
226
|
+
const n = particles.length;
|
|
227
|
+
|
|
228
|
+
const hash = buildSpatialHash(particles, h);
|
|
229
|
+
const { densities, nearDensities } = computeDensities(particles, cfg, hash);
|
|
230
|
+
const { pressures, nearPressures } = computePressures(densities, nearDensities, cfg);
|
|
231
|
+
|
|
232
|
+
const forces = new Array(n);
|
|
233
|
+
for (let i = 0; i < n; i++) {
|
|
234
|
+
forces[i] = { x: 0, y: 0 };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < n; i++) {
|
|
238
|
+
const pi = particles[i];
|
|
239
|
+
const densityI = Math.max(densities[i], EPS);
|
|
240
|
+
|
|
241
|
+
forEachNeighbor(i, particles, hash, h2, (j, dx, dy, r2) => {
|
|
242
|
+
if (j <= i) return;
|
|
243
|
+
const r = Math.sqrt(r2);
|
|
244
|
+
if (r < EPS || r >= h) return;
|
|
245
|
+
|
|
246
|
+
const pj = particles[j];
|
|
247
|
+
|
|
248
|
+
// Direction from i to j (for repulsion)
|
|
249
|
+
const invR = 1 / r;
|
|
250
|
+
const dirX = -dx * invR; // Flip: dx is (pi.x - pj.x), we want (pj - pi)
|
|
251
|
+
const dirY = -dy * invR;
|
|
252
|
+
|
|
253
|
+
// Neighbor densities
|
|
254
|
+
const densityJ = Math.max(densities[j], EPS);
|
|
255
|
+
const nearDensityJ = Math.max(nearDensities[j], EPS);
|
|
256
|
+
|
|
257
|
+
// Shared pressures (average between particles)
|
|
258
|
+
const sharedPressure = (pressures[i] + pressures[j]) * 0.5;
|
|
259
|
+
const sharedNearPressure = (nearPressures[i] + nearPressures[j]) * 0.5;
|
|
260
|
+
|
|
261
|
+
// Kernel derivatives (these are negative, pointing inward)
|
|
262
|
+
const densityGrad = Kernels.spikyPow2Derivative(r, h);
|
|
263
|
+
const nearDensityGrad = Kernels.spikyPow3Derivative(r, h);
|
|
264
|
+
|
|
265
|
+
// Pressure forces - divide by respective densities (like Unity reference)
|
|
266
|
+
// The negative gradient * positive direction = repulsion force
|
|
267
|
+
const pressureForceMag = sharedPressure * densityGrad / densityJ;
|
|
268
|
+
const nearPressureForceMag = sharedNearPressure * nearDensityGrad / nearDensityJ;
|
|
269
|
+
const totalForce = pressureForceMag + nearPressureForceMag;
|
|
270
|
+
|
|
271
|
+
const fx = dirX * totalForce;
|
|
272
|
+
const fy = dirY * totalForce;
|
|
273
|
+
|
|
274
|
+
// Apply equal and opposite forces
|
|
275
|
+
forces[i].x += fx;
|
|
276
|
+
forces[i].y += fy;
|
|
277
|
+
forces[j].x -= fx;
|
|
278
|
+
forces[j].y -= fy;
|
|
279
|
+
|
|
280
|
+
// Viscosity force using Poly6 kernel
|
|
281
|
+
const viscKernel = Kernels.poly6(r2, h);
|
|
282
|
+
const viscStrength = cfg.fluid.viscosity * viscKernel;
|
|
283
|
+
const dvx = (pj.vx - pi.vx) * viscStrength;
|
|
284
|
+
const dvy = (pj.vy - pi.vy) * viscStrength;
|
|
285
|
+
forces[i].x += dvx;
|
|
286
|
+
forces[i].y += dvy;
|
|
287
|
+
forces[j].x -= dvx;
|
|
288
|
+
forces[j].y -= dvy;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
clampForces(forces, cfg.fluid.maxForce);
|
|
293
|
+
return { forces, densities, pressures };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Compute buoyancy from temperature differentials (hot rises, cold sinks).
|
|
298
|
+
* Temperature can live in `p.custom.temperature` or `p.temperature`.
|
|
299
|
+
* @param {Array<Object>} particles
|
|
300
|
+
* @param {Object} [overrides]
|
|
301
|
+
* @returns {Array<{x:number,y:number}>}
|
|
302
|
+
*/
|
|
303
|
+
export function computeThermalBuoyancy(particles, overrides = {}) {
|
|
304
|
+
const cfg = mergeConfig(overrides);
|
|
305
|
+
const forces = new Array(particles.length);
|
|
306
|
+
const neutral = cfg.gas.neutralTemperature;
|
|
307
|
+
const strength = cfg.gas.buoyancy;
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < particles.length; i++) {
|
|
310
|
+
const p = particles[i];
|
|
311
|
+
const temperature =
|
|
312
|
+
p.temperature ?? p.custom?.temperature ?? cfg.gas.neutralTemperature;
|
|
313
|
+
const lift = (temperature - neutral) * strength;
|
|
314
|
+
forces[i] = { x: 0, y: -lift };
|
|
315
|
+
}
|
|
316
|
+
return forces;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Simplified gas model: diffusion + mild pressure + buoyancy + turbulence.
|
|
321
|
+
* @param {Array<Object>} particles
|
|
322
|
+
* @param {Object} [overrides]
|
|
323
|
+
* @returns {{ forces: Array<{x:number,y:number}> }}
|
|
324
|
+
*/
|
|
325
|
+
export function computeGasForces(particles, overrides = {}) {
|
|
326
|
+
const cfg = mergeConfig(overrides);
|
|
327
|
+
const rMax = cfg.gas.interactionRadius;
|
|
328
|
+
const rMax2 = rMax * rMax;
|
|
329
|
+
const n = particles.length;
|
|
330
|
+
const hash = buildSpatialHash(particles, rMax);
|
|
331
|
+
const forces = new Array(n);
|
|
332
|
+
for (let i = 0; i < n; i++) {
|
|
333
|
+
forces[i] = { x: 0, y: 0 };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (let i = 0; i < n; i++) {
|
|
337
|
+
const pi = particles[i];
|
|
338
|
+
const massI = resolveMass(pi, cfg);
|
|
339
|
+
const tempI = pi.temperature ?? pi.custom?.temperature ?? cfg.gas.neutralTemperature;
|
|
340
|
+
|
|
341
|
+
forEachNeighbor(i, particles, hash, rMax2, (j, dx, dy, r2) => {
|
|
342
|
+
if (j <= i) return;
|
|
343
|
+
if (r2 === 0) return;
|
|
344
|
+
const r = Math.sqrt(r2);
|
|
345
|
+
const invR = 1 / r;
|
|
346
|
+
const pj = particles[j];
|
|
347
|
+
const massJ = resolveMass(pj, cfg);
|
|
348
|
+
const tempJ = pj.temperature ?? pj.custom?.temperature ?? cfg.gas.neutralTemperature;
|
|
349
|
+
|
|
350
|
+
const pressure = cfg.gas.pressure * (1 - r / rMax);
|
|
351
|
+
const fx = dx * invR * pressure;
|
|
352
|
+
const fy = dy * invR * pressure;
|
|
353
|
+
forces[i].x += fx;
|
|
354
|
+
forces[i].y += fy;
|
|
355
|
+
forces[j].x -= fx;
|
|
356
|
+
forces[j].y -= fy;
|
|
357
|
+
|
|
358
|
+
const diffusion = cfg.gas.diffusion;
|
|
359
|
+
const dvx = (pj.vx - pi.vx) * diffusion;
|
|
360
|
+
const dvy = (pj.vy - pi.vy) * diffusion;
|
|
361
|
+
forces[i].x += dvx * massJ;
|
|
362
|
+
forces[i].y += dvy * massJ;
|
|
363
|
+
forces[j].x -= dvx * massI;
|
|
364
|
+
forces[j].y -= dvy * massI;
|
|
365
|
+
|
|
366
|
+
const tempDelta = tempI - tempJ;
|
|
367
|
+
const buoyancyPush = tempDelta * cfg.gas.buoyancy * 0.5;
|
|
368
|
+
forces[i].y -= buoyancyPush;
|
|
369
|
+
forces[j].y += buoyancyPush;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Turbulence + drag
|
|
374
|
+
for (let i = 0; i < n; i++) {
|
|
375
|
+
const p = particles[i];
|
|
376
|
+
const drag = cfg.gas.drag;
|
|
377
|
+
forces[i].x += -p.vx * drag;
|
|
378
|
+
forces[i].y += -p.vy * drag;
|
|
379
|
+
forces[i].x += (Math.random() - 0.5) * cfg.gas.turbulence;
|
|
380
|
+
forces[i].y += (Math.random() - 0.5) * cfg.gas.turbulence;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
clampForces(forces, cfg.fluid.maxForce);
|
|
384
|
+
return { forces };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Apply Euler integration in a pure way (returns next state, does not mutate).
|
|
389
|
+
* @param {Array<Object>} particles
|
|
390
|
+
* @param {Array<{x:number,y:number}>} forces
|
|
391
|
+
* @param {number} dt
|
|
392
|
+
* @param {Object} [overrides]
|
|
393
|
+
* @returns {Array<{ x:number, y:number, vx:number, vy:number }>}
|
|
394
|
+
*/
|
|
395
|
+
export function integrateEuler(particles, forces, dt, overrides = {}) {
|
|
396
|
+
const cfg = mergeConfig(overrides);
|
|
397
|
+
const next = new Array(particles.length);
|
|
398
|
+
for (let i = 0; i < particles.length; i++) {
|
|
399
|
+
const p = particles[i];
|
|
400
|
+
const f = forces[i];
|
|
401
|
+
const mass = resolveMass(p, cfg);
|
|
402
|
+
const ax = f.x / mass + cfg.external.gravity.x;
|
|
403
|
+
const ay = f.y / mass + cfg.external.gravity.y;
|
|
404
|
+
|
|
405
|
+
const vx = p.vx + ax * dt;
|
|
406
|
+
const vy = p.vy + ay * dt;
|
|
407
|
+
next[i] = {
|
|
408
|
+
x: p.x + vx * dt,
|
|
409
|
+
y: p.y + vy * dt,
|
|
410
|
+
vx,
|
|
411
|
+
vy,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return next;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Utility to mix (lerp) between two force fields (e.g., liquid vs gas).
|
|
419
|
+
* @param {Array<{x:number,y:number}>} a
|
|
420
|
+
* @param {Array<{x:number,y:number}>} b
|
|
421
|
+
* @param {number} t - 0..1
|
|
422
|
+
* @returns {Array<{x:number,y:number}>}
|
|
423
|
+
*/
|
|
424
|
+
export function blendForces(a, b, t) {
|
|
425
|
+
const n = Math.min(a.length, b.length);
|
|
426
|
+
const out = new Array(n);
|
|
427
|
+
for (let i = 0; i < n; i++) {
|
|
428
|
+
out[i] = {
|
|
429
|
+
x: Easing.lerp(a[i].x, b[i].x, t),
|
|
430
|
+
y: Easing.lerp(a[i].y, b[i].y, t),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Convenience factory to grab defaults (safe copy).
|
|
438
|
+
* @returns {Object}
|
|
439
|
+
*/
|
|
440
|
+
export function getDefaultFluidConfig() {
|
|
441
|
+
return mergeConfig();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function resolveMass(p, cfg) {
|
|
445
|
+
if (p.custom && typeof p.custom.mass === "number") return p.custom.mass;
|
|
446
|
+
if (p.mass) return p.mass;
|
|
447
|
+
if (p.size) return p.size * 0.5 + cfg.fluid.particleMass;
|
|
448
|
+
return cfg.fluid.particleMass;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function clampForces(forces, maxForce) {
|
|
452
|
+
const maxForce2 = maxForce * maxForce;
|
|
453
|
+
for (let i = 0; i < forces.length; i++) {
|
|
454
|
+
const f = forces[i];
|
|
455
|
+
const mag2 = f.x * f.x + f.y * f.y;
|
|
456
|
+
if (mag2 > maxForce2) {
|
|
457
|
+
const inv = 1 / Math.sqrt(mag2);
|
|
458
|
+
f.x *= maxForce * inv;
|
|
459
|
+
f.y *= maxForce * inv;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function buildSpatialHash(particles, radius) {
|
|
465
|
+
const cellSize = radius;
|
|
466
|
+
const buckets = new Map();
|
|
467
|
+
for (let i = 0; i < particles.length; i++) {
|
|
468
|
+
const p = particles[i];
|
|
469
|
+
const cx = Math.floor(p.x / cellSize);
|
|
470
|
+
const cy = Math.floor(p.y / cellSize);
|
|
471
|
+
const key = `${cx},${cy}`;
|
|
472
|
+
let bucket = buckets.get(key);
|
|
473
|
+
if (!bucket) {
|
|
474
|
+
bucket = [];
|
|
475
|
+
buckets.set(key, bucket);
|
|
476
|
+
}
|
|
477
|
+
bucket.push(i);
|
|
478
|
+
}
|
|
479
|
+
return { cellSize, buckets };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function forEachNeighbor(i, particles, hash, radiusSquared, cb) {
|
|
483
|
+
const p = particles[i];
|
|
484
|
+
const cellSize = hash.cellSize;
|
|
485
|
+
const cx = Math.floor(p.x / cellSize);
|
|
486
|
+
const cy = Math.floor(p.y / cellSize);
|
|
487
|
+
|
|
488
|
+
for (let ox = -1; ox <= 1; ox++) {
|
|
489
|
+
for (let oy = -1; oy <= 1; oy++) {
|
|
490
|
+
const key = `${cx + ox},${cy + oy}`;
|
|
491
|
+
const bucket = hash.buckets.get(key);
|
|
492
|
+
if (!bucket) continue;
|
|
493
|
+
for (let k = 0; k < bucket.length; k++) {
|
|
494
|
+
const j = bucket[k];
|
|
495
|
+
if (j === i) continue;
|
|
496
|
+
const pj = particles[j];
|
|
497
|
+
const dx = p.x - pj.x;
|
|
498
|
+
const dy = p.y - pj.y;
|
|
499
|
+
const r2 = dx * dx + dy * dy;
|
|
500
|
+
if (r2 < radiusSquared) {
|
|
501
|
+
cb(j, dx, dy, r2);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
package/src/math/index.js
CHANGED
|
@@ -4,9 +4,11 @@ export {Fractals} from "./fractal.js";
|
|
|
4
4
|
export {Patterns} from "./patterns.js";
|
|
5
5
|
export {Noise} from "./noise.js";
|
|
6
6
|
export {Tensor} from "./tensor.js";
|
|
7
|
+
export {generatePenroseTilingPixels} from "./penrose.js";
|
|
7
8
|
|
|
8
9
|
// Physics modules
|
|
9
10
|
export * from "./gr.js";
|
|
10
11
|
export * from "./orbital.js";
|
|
11
12
|
export * from "./quantum.js";
|
|
12
13
|
export * from "./heat.js";
|
|
14
|
+
export * from "./fluid.js";
|
package/src/mixins/anchor.js
CHANGED
|
@@ -83,23 +83,33 @@ export function applyAnchor(go, options = {}) {
|
|
|
83
83
|
go._anchor.offsetY
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
|
-
// Apply the calculated position
|
|
86
|
+
// Apply the calculated position using Transform API
|
|
87
|
+
let newX, newY;
|
|
88
|
+
|
|
87
89
|
if (go.parent && !isPipelineRoot(go)) {
|
|
88
90
|
// If object has a parent AND is not directly in the pipeline
|
|
89
91
|
if (relativeObj === go.parent) {
|
|
90
92
|
// If anchored relative to parent, use local coordinates
|
|
91
93
|
// (parent position is already accounted for in rendering)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
newX = position.x - relativeObj.x;
|
|
95
|
+
newY = position.y - relativeObj.y;
|
|
94
96
|
} else {
|
|
95
97
|
// If anchored to something else or absolutely, convert to local coordinates
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
newX = position.x - go.parent.x;
|
|
99
|
+
newY = position.y - go.parent.y;
|
|
98
100
|
}
|
|
99
101
|
} else {
|
|
100
102
|
// No parent or directly in pipeline - use absolute coordinates
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
newX = position.x;
|
|
104
|
+
newY = position.y;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Use Transform API if available, otherwise fall back to direct assignment
|
|
108
|
+
if (go.transform && typeof go.transform.position === "function") {
|
|
109
|
+
go.transform.position(newX, newY);
|
|
110
|
+
} else {
|
|
111
|
+
go.x = newX;
|
|
112
|
+
go.y = newY;
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
// Set text alignment if applicable and enabled
|
package/src/motion/tweenetik.js
CHANGED
|
@@ -136,4 +136,20 @@ export class Tweenetik {
|
|
|
136
136
|
// Remove finished tweens from the list
|
|
137
137
|
Tweenetik._active = Tweenetik._active.filter((t) => !t._finished);
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Kill all active tweens targeting a specific object.
|
|
142
|
+
* Useful when resetting an object's state to avoid lingering tweens.
|
|
143
|
+
* @param {Object} target - The target object whose tweens should be stopped
|
|
144
|
+
*/
|
|
145
|
+
static killTarget(target) {
|
|
146
|
+
Tweenetik._active = Tweenetik._active.filter((t) => t.target !== target);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Kill all active tweens.
|
|
151
|
+
*/
|
|
152
|
+
static killAll() {
|
|
153
|
+
Tweenetik._active = [];
|
|
154
|
+
}
|
|
139
155
|
}
|
package/src/shapes/index.js
CHANGED