@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
@@ -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
- this.font = options.font || "12px monospace";
38
- this.textColor = options.textColor || "#fff";
39
- this.bgColor = options.bgColor || "rgba(0,0,0,0.85)";
40
- this.borderColor = options.borderColor || "rgba(255,255,255,0.3)";
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";
@@ -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
- go.x = position.x - relativeObj.x;
93
- go.y = position.y - relativeObj.y;
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
- go.x = position.x - go.parent.x;
97
- go.y = position.y - go.parent.y;
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
- go.x = position.x;
102
- go.y = position.y;
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
@@ -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
  }
@@ -109,3 +109,4 @@ export { Hexagon } from "./hexagon.js";
109
109
  export { Heart } from "./heart.js";
110
110
  export { Cross } from "./cross.js";
111
111
  export * from "./text.js";
112
+ export { ImageShape } from "./image.js";