@guinetik/gcanvas 1.0.5 → 2.0.0

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 (78) hide show
  1. package/dist/aizawa.html +27 -0
  2. package/dist/clifford.html +25 -0
  3. package/dist/cmb.html +24 -0
  4. package/dist/dadras.html +26 -0
  5. package/dist/dejong.html +25 -0
  6. package/dist/gcanvas.es.js +5130 -372
  7. package/dist/gcanvas.es.min.js +1 -1
  8. package/dist/gcanvas.umd.js +1 -1
  9. package/dist/gcanvas.umd.min.js +1 -1
  10. package/dist/halvorsen.html +27 -0
  11. package/dist/index.html +96 -48
  12. package/dist/js/aizawa.js +425 -0
  13. package/dist/js/bezier.js +5 -5
  14. package/dist/js/clifford.js +236 -0
  15. package/dist/js/cmb.js +594 -0
  16. package/dist/js/dadras.js +405 -0
  17. package/dist/js/dejong.js +257 -0
  18. package/dist/js/halvorsen.js +405 -0
  19. package/dist/js/isometric.js +34 -46
  20. package/dist/js/lorenz.js +425 -0
  21. package/dist/js/painter.js +8 -8
  22. package/dist/js/rossler.js +480 -0
  23. package/dist/js/schrodinger.js +314 -18
  24. package/dist/js/thomas.js +394 -0
  25. package/dist/lorenz.html +27 -0
  26. package/dist/rossler.html +27 -0
  27. package/dist/scene-interactivity-test.html +220 -0
  28. package/dist/thomas.html +27 -0
  29. package/package.json +1 -1
  30. package/readme.md +30 -22
  31. package/src/game/objects/go.js +7 -0
  32. package/src/game/objects/index.js +2 -0
  33. package/src/game/objects/isometric-scene.js +53 -3
  34. package/src/game/objects/layoutscene.js +57 -0
  35. package/src/game/objects/mask.js +241 -0
  36. package/src/game/objects/scene.js +19 -0
  37. package/src/game/objects/wrapper.js +14 -2
  38. package/src/game/pipeline.js +17 -0
  39. package/src/game/ui/button.js +101 -16
  40. package/src/game/ui/theme.js +0 -6
  41. package/src/game/ui/togglebutton.js +25 -14
  42. package/src/game/ui/tooltip.js +12 -4
  43. package/src/index.js +3 -0
  44. package/src/io/gesture.js +409 -0
  45. package/src/io/index.js +4 -1
  46. package/src/io/keys.js +9 -1
  47. package/src/io/screen.js +476 -0
  48. package/src/math/attractors.js +664 -0
  49. package/src/math/heat.js +106 -0
  50. package/src/math/index.js +1 -0
  51. package/src/mixins/draggable.js +15 -19
  52. package/src/painter/painter.shapes.js +11 -5
  53. package/src/particle/particle-system.js +165 -1
  54. package/src/physics/index.js +26 -0
  55. package/src/physics/physics-updaters.js +333 -0
  56. package/src/physics/physics.js +375 -0
  57. package/src/shapes/image.js +5 -5
  58. package/src/shapes/index.js +2 -0
  59. package/src/shapes/parallelogram.js +147 -0
  60. package/src/shapes/righttriangle.js +115 -0
  61. package/src/shapes/svg.js +281 -100
  62. package/src/shapes/text.js +22 -6
  63. package/src/shapes/transformable.js +5 -0
  64. package/src/sound/effects.js +807 -0
  65. package/src/sound/index.js +13 -0
  66. package/src/webgl/index.js +7 -0
  67. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  68. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  69. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  70. package/src/webgl/webgl-clifford-renderer.js +477 -0
  71. package/src/webgl/webgl-dejong-renderer.js +472 -0
  72. package/src/webgl/webgl-line-renderer.js +391 -0
  73. package/src/webgl/webgl-particle-renderer.js +410 -0
  74. package/types/index.d.ts +30 -2
  75. package/types/io.d.ts +217 -0
  76. package/types/physics.d.ts +299 -0
  77. package/types/shapes.d.ts +8 -0
  78. package/types/webgl.d.ts +188 -109
package/src/math/heat.js CHANGED
@@ -200,3 +200,109 @@ export function heatTransferFalloff(temp1, temp2, distance, maxDistance, rate, f
200
200
 
201
201
  return heatDiff * rate * proximity;
202
202
  }
203
+
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+ // PARTICLE SYSTEM HEAT TRANSFER
206
+ // ─────────────────────────────────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Apply heat transfer between nearby particles in a particle system.
210
+ * Uses Newton's law of cooling with distance falloff.
211
+ * Follows the same pattern as Collision.applyCircleSeparation.
212
+ *
213
+ * @param {Array<Object>} particles - Array of particles with { x, y, custom }
214
+ * @param {Object} [options={}] - Configuration options
215
+ * @param {number} [options.maxDistance=50] - Maximum distance for heat transfer
216
+ * @param {number} [options.rate=0.01] - Heat transfer rate coefficient
217
+ * @param {number} [options.falloff=1] - Distance falloff (1=linear, 2=quadratic)
218
+ * @param {string} [options.temperatureKey='temperature'] - Key in particle.custom
219
+ * @param {Function} [options.filter=null] - Filter function (particle) => boolean
220
+ * @param {boolean} [options.useSizeAsRadius=true] - Use particle.size for distance calc
221
+ *
222
+ * @example
223
+ * // Basic usage - all particles exchange heat
224
+ * applyParticleHeatTransfer(particles, {
225
+ * maxDistance: 40,
226
+ * rate: 0.02,
227
+ * });
228
+ *
229
+ * @example
230
+ * // With filter - exclude sorted particles (Maxwell's Demon)
231
+ * applyParticleHeatTransfer(particles, {
232
+ * maxDistance: 36,
233
+ * rate: 0.015,
234
+ * filter: (p) => !p.custom.sorted,
235
+ * });
236
+ *
237
+ * @example
238
+ * // With quadratic falloff for more localized transfer
239
+ * applyParticleHeatTransfer(particles, {
240
+ * maxDistance: 60,
241
+ * rate: 0.025,
242
+ * falloff: 2,
243
+ * });
244
+ */
245
+ export function applyParticleHeatTransfer(particles, options = {}) {
246
+ const {
247
+ maxDistance = 50,
248
+ rate = 0.01,
249
+ falloff = 1,
250
+ temperatureKey = 'temperature',
251
+ filter = null,
252
+ useSizeAsRadius = true,
253
+ } = options;
254
+
255
+ const n = particles.length;
256
+ const maxDist2 = maxDistance * maxDistance;
257
+
258
+ for (let i = 0; i < n; i++) {
259
+ const pi = particles[i];
260
+
261
+ // Skip filtered particles
262
+ if (filter && !filter(pi)) continue;
263
+
264
+ // Ensure temperature exists
265
+ if (pi.custom[temperatureKey] === undefined) {
266
+ pi.custom[temperatureKey] = 0.5;
267
+ }
268
+
269
+ for (let j = i + 1; j < n; j++) {
270
+ const pj = particles[j];
271
+
272
+ // Skip filtered particles
273
+ if (filter && !filter(pj)) continue;
274
+
275
+ // Ensure temperature exists
276
+ if (pj.custom[temperatureKey] === undefined) {
277
+ pj.custom[temperatureKey] = 0.5;
278
+ }
279
+
280
+ // Distance check (squared for performance)
281
+ const dx = pi.x - pj.x;
282
+ const dy = pi.y - pj.y;
283
+ const dist2 = dx * dx + dy * dy;
284
+
285
+ if (dist2 >= maxDist2 || dist2 < 0.0001) continue;
286
+
287
+ const dist = Math.sqrt(dist2);
288
+
289
+ // Calculate heat transfer with optional distance falloff
290
+ const delta = heatTransferFalloff(
291
+ pi.custom[temperatureKey],
292
+ pj.custom[temperatureKey],
293
+ dist,
294
+ maxDistance,
295
+ rate,
296
+ falloff
297
+ );
298
+
299
+ // Apply symmetric transfer (energy conservation)
300
+ pi.custom[temperatureKey] += delta;
301
+ pj.custom[temperatureKey] -= delta;
302
+
303
+ // Clamp to valid range
304
+ pi.custom[temperatureKey] = Math.max(0, Math.min(1, pi.custom[temperatureKey]));
305
+ pj.custom[temperatureKey] = Math.max(0, Math.min(1, pj.custom[temperatureKey]));
306
+ }
307
+ }
308
+ }
package/src/math/index.js CHANGED
@@ -6,6 +6,7 @@ export {Noise} from "./noise.js";
6
6
  export {Tensor} from "./tensor.js";
7
7
  export {generatePenroseTilingPixels} from "./penrose.js";
8
8
  export { BooleanAlgebra } from "./boolean.js";
9
+ export { Attractors, AttractorType, AttractorDimension, getAttractorNames, getAttractor, get3DAttractors, get2DAttractors } from "./attractors.js";
9
10
 
10
11
  // Physics modules
11
12
  export * from "./gr.js";
@@ -1,10 +1,10 @@
1
1
  export function applyDraggable(go, options = {}) {
2
2
  const game = go.game;
3
-
3
+
4
4
  // Clear any existing state to avoid duplicates
5
5
  go.dragging = false;
6
6
  go.dragOffset = { x: 0, y: 0 };
7
-
7
+
8
8
  // Clean up any existing event handlers to prevent duplicates
9
9
  if (go._dragInputMoveHandler) {
10
10
  game.events.off("inputmove", go._dragInputMoveHandler);
@@ -12,26 +12,22 @@ export function applyDraggable(go, options = {}) {
12
12
  if (go._dragInputUpHandler) {
13
13
  game.events.off("inputup", go._dragInputUpHandler);
14
14
  }
15
-
15
+
16
16
  // Make sure the object is interactive
17
- if (typeof go.enableInteractivity === 'function') {
18
- go.enableInteractivity(go);
19
- } else {
20
- go.interactive = true;
21
- }
22
-
17
+ go.interactive = true;
18
+
23
19
  // Define the input handlers and store them on the object to allow cleanup
24
20
  go._dragInputDownHandler = (e) => {
25
- // console.log("Drag input down", go.constructor.name);
21
+ // console.log("Drag input down", go.constructor.name);
26
22
  go.dragging = true;
27
-
23
+
28
24
  // Calculate offset from mouse position to object center
29
25
  go.dragOffset.x = go.x - e.x;
30
26
  go.dragOffset.y = go.y - e.y;
31
-
27
+
32
28
  if (options.onDragStart) options.onDragStart();
33
29
  };
34
-
30
+
35
31
  go._dragInputMoveHandler = (e) => {
36
32
  //console.log("Drag input move", go.constructor.name, "dragging:", go.dragging);
37
33
  if (go.dragging) {
@@ -41,27 +37,27 @@ export function applyDraggable(go, options = {}) {
41
37
  go.y = e.y + go.dragOffset.y;
42
38
  }
43
39
  };
44
-
40
+
45
41
  go._dragInputUpHandler = (e) => {
46
42
  //console.log("Drag input up", go.constructor.name, "dragging:", go.dragging);
47
43
  if (!go.dragging) return;
48
-
44
+
49
45
  go.dragging = false;
50
46
  if (options.onDragEnd) options.onDragEnd();
51
47
  };
52
-
48
+
53
49
  // Bind the event handlers
54
50
  go.on("inputdown", go._dragInputDownHandler);
55
51
  game.events.on("inputmove", go._dragInputMoveHandler);
56
52
  game.events.on("inputup", go._dragInputUpHandler);
57
-
53
+
58
54
  // Return a cleanup function
59
55
  return () => {
60
56
  // Remove event listeners
61
57
  go.off("inputdown", go._dragInputDownHandler);
62
58
  game.events.off("inputmove", go._dragInputMoveHandler);
63
59
  game.events.off("inputup", go._dragInputUpHandler);
64
-
60
+
65
61
  // Clean up properties
66
62
  delete go._dragInputDownHandler;
67
63
  delete go._dragInputMoveHandler;
@@ -69,4 +65,4 @@ export function applyDraggable(go, options = {}) {
69
65
  delete go.dragging;
70
66
  delete go.dragOffset;
71
67
  };
72
- }
68
+ }
@@ -254,23 +254,29 @@ export class PainterShapes {
254
254
  * @param {number} [lineWidth] - Line width
255
255
  * @returns {void}
256
256
  */
257
- static polygon(points, fillColor, strokeColor, lineWidth) {
257
+ static polygon(points, fillColor, strokeColor, lineWidth, lineJoin) {
258
258
  if (points.length < 2) return;
259
259
 
260
- Painter.lines.beginPath();
261
- Painter.lines.moveTo(points[0].x, points[0].y);
260
+ const ctx = Painter.ctx;
261
+
262
+ // Build the path
263
+ ctx.beginPath();
264
+ ctx.moveTo(points[0].x, points[0].y);
262
265
 
263
266
  for (let i = 1; i < points.length; i++) {
264
- Painter.lines.lineTo(points[i].x, points[i].y);
267
+ ctx.lineTo(points[i].x, points[i].y);
265
268
  }
266
269
 
267
- Painter.lines.closePath();
270
+ ctx.closePath();
268
271
 
272
+ // Fill using Painter.colors (sets fillStyle and calls fill)
269
273
  if (fillColor) {
270
274
  Painter.colors.fill(fillColor);
271
275
  }
272
276
 
277
+ // Stroke using Painter.colors
273
278
  if (strokeColor) {
279
+ if (lineJoin) ctx.lineJoin = lineJoin;
274
280
  Painter.colors.stroke(strokeColor, lineWidth);
275
281
  }
276
282
  }
@@ -7,6 +7,7 @@
7
7
  * - Optional Camera3D integration with depth sorting
8
8
  * - Multiple emitter support
9
9
  * - Blend mode control
10
+ * - Optional WebGL GPU-accelerated rendering
10
11
  *
11
12
  * @example
12
13
  * const particles = new ParticleSystem(this, {
@@ -14,6 +15,7 @@
14
15
  * depthSort: true,
15
16
  * maxParticles: 3000,
16
17
  * blendMode: "screen",
18
+ * useWebGL: true, // Enable GPU rendering
17
19
  * updaters: [Updaters.velocity, Updaters.lifetime, Updaters.gravity(150)],
18
20
  * });
19
21
  * particles.addEmitter("fountain", new ParticleEmitter({ rate: 50 }));
@@ -23,6 +25,7 @@ import { GameObject } from "../game/objects/go.js";
23
25
  import { Painter } from "../painter/painter.js";
24
26
  import { Particle } from "./particle.js";
25
27
  import { Updaters } from "./updaters.js";
28
+ import { WebGLParticleRenderer } from "../webgl/webgl-particle-renderer.js";
26
29
 
27
30
  export class ParticleSystem extends GameObject {
28
31
  /**
@@ -34,6 +37,13 @@ export class ParticleSystem extends GameObject {
34
37
  * @param {string} [options.blendMode="source-over"] - Canvas blend mode
35
38
  * @param {Function[]} [options.updaters] - Array of updater functions
36
39
  * @param {boolean} [options.worldSpace=false] - Position particles in world space
40
+ * @param {boolean} [options.useWebGL=false] - Use GPU-accelerated rendering
41
+ * @param {WebGLParticleRenderer} [options.webglRenderer] - External WebGL renderer
42
+ * @param {string} [options.webglShape='circle'] - WebGL particle shape
43
+ * @param {string} [options.webglBlendMode='alpha'] - WebGL blend mode ('alpha' or 'additive')
44
+ * @param {boolean} [options.depthShading=false] - Shade particles by depth (closer=brighter)
45
+ * @param {number} [options.depthShadingMin=0.3] - Minimum brightness for far particles
46
+ * @param {number} [options.depthShadingMax=1.0] - Maximum brightness for near particles
37
47
  */
38
48
  constructor(game, options = {}) {
39
49
  super(game, options);
@@ -60,6 +70,37 @@ export class ParticleSystem extends GameObject {
60
70
  this.blendMode = options.blendMode ?? "source-over";
61
71
  this.worldSpace = options.worldSpace ?? false;
62
72
 
73
+ // WebGL rendering (optional GPU acceleration)
74
+ this.webglRenderer = null;
75
+ if (options.webglRenderer) {
76
+ // Use provided renderer
77
+ this.webglRenderer = options.webglRenderer;
78
+ } else if (options.useWebGL) {
79
+ // Auto-create WebGL renderer
80
+ this.webglRenderer = new WebGLParticleRenderer(this.maxParticles, {
81
+ width: game.width,
82
+ height: game.height,
83
+ shape: options.webglShape ?? 'circle',
84
+ blendMode: options.webglBlendMode ?? 'alpha',
85
+ });
86
+ if (!this.webglRenderer.isAvailable()) {
87
+ console.warn('WebGL not available, falling back to Canvas 2D');
88
+ this.webglRenderer = null;
89
+ }
90
+ }
91
+
92
+ // Depth shading (closer = brighter, further = darker)
93
+ this.depthShading = options.depthShading ?? false;
94
+ this.depthShadingMin = options.depthShadingMin ?? 0.3;
95
+ this.depthShadingMax = options.depthShadingMax ?? 1.0;
96
+
97
+ // WebGL offset for particles in local/centered coordinate systems (e.g. inside Scene3D)
98
+ // When particles use centered coords (-w/2 to w/2), set this to { x: w/2, y: h/2 }
99
+ this.webglOffset = options.webglOffset ?? null;
100
+
101
+ // Reusable array for WebGL render list (avoid allocation per frame)
102
+ this._webglRenderList = [];
103
+
63
104
  // Stats
64
105
  this._particleCount = 0;
65
106
  }
@@ -184,13 +225,115 @@ export class ParticleSystem extends GameObject {
184
225
 
185
226
  if (this.particles.length === 0) return;
186
227
 
187
- if (this.camera && this.depthSort) {
228
+ // Use WebGL if available
229
+ if (this.webglRenderer) {
230
+ this.renderWebGL();
231
+ } else if (this.camera && this.depthSort) {
188
232
  this.renderWithDepthSort();
189
233
  } else {
190
234
  this.renderSimple();
191
235
  }
192
236
  }
193
237
 
238
+ /**
239
+ * GPU-accelerated rendering using WebGL point sprites.
240
+ * Projects particles through camera (if available), depth sorts,
241
+ * and renders via WebGLParticleRenderer.
242
+ */
243
+ renderWebGL() {
244
+ const renderer = this.webglRenderer;
245
+ const renderList = this._webglRenderList;
246
+
247
+ // Resize WebGL canvas if needed
248
+ if (renderer.width !== this.game.width || renderer.height !== this.game.height) {
249
+ renderer.resize(this.game.width, this.game.height);
250
+ }
251
+
252
+ // Clear render list
253
+ renderList.length = 0;
254
+
255
+ // Build render list with projections
256
+ const centerX = this.game.width / 2;
257
+ const centerY = this.game.height / 2;
258
+
259
+ if (this.camera && this.depthSort) {
260
+ // 3D mode: project through camera
261
+ for (const p of this.particles) {
262
+ const proj = this.camera.project(p.x, p.y, p.z);
263
+
264
+ // Cull particles behind camera
265
+ if (proj.z < -this.camera.perspective + 10) continue;
266
+
267
+ renderList.push({
268
+ x: centerX + proj.x,
269
+ y: centerY + proj.y,
270
+ z: proj.z,
271
+ size: p.size * proj.scale,
272
+ color: p.color,
273
+ });
274
+ }
275
+
276
+ // Sort back to front (painter's algorithm)
277
+ renderList.sort((a, b) => b.z - a.z);
278
+
279
+ // Apply depth shading if enabled
280
+ if (this.depthShading && renderList.length > 1) {
281
+ // Find z range
282
+ let minZ = Infinity, maxZ = -Infinity;
283
+ for (const item of renderList) {
284
+ if (item.z < minZ) minZ = item.z;
285
+ if (item.z > maxZ) maxZ = item.z;
286
+ }
287
+ const zRange = maxZ - minZ;
288
+
289
+ if (zRange > 0) {
290
+ const brightnessRange = this.depthShadingMax - this.depthShadingMin;
291
+
292
+ for (const item of renderList) {
293
+ // Normalize z: 0 = far (maxZ), 1 = near (minZ)
294
+ const t = (maxZ - item.z) / zRange;
295
+ const brightness = this.depthShadingMin + t * brightnessRange;
296
+
297
+ // Apply brightness to color (create new color object to avoid mutating particle)
298
+ item.color = {
299
+ r: item.color.r * brightness,
300
+ g: item.color.g * brightness,
301
+ b: item.color.b * brightness,
302
+ a: item.color.a,
303
+ };
304
+ }
305
+ }
306
+ }
307
+ } else {
308
+ // 2D mode: direct screen coords (with optional offset for centered coordinate systems)
309
+ const offsetX = this.webglOffset?.x ?? 0;
310
+ const offsetY = this.webglOffset?.y ?? 0;
311
+ for (const p of this.particles) {
312
+ renderList.push({
313
+ x: p.x + offsetX,
314
+ y: p.y + offsetY,
315
+ size: p.size,
316
+ color: p.color,
317
+ });
318
+ }
319
+ }
320
+
321
+ // Upload to GPU and render
322
+ renderer.clear();
323
+ const count = renderer.updateParticles(renderList);
324
+ renderer.render(count);
325
+
326
+ // Composite onto main canvas
327
+ // If using webglOffset, composite at negative offset to counteract Scene3D translation
328
+ const compositeX = this.webglOffset ? -this.webglOffset.x : 0;
329
+ const compositeY = this.webglOffset ? -this.webglOffset.y : 0;
330
+ Painter.useCtx((ctx) => {
331
+ ctx.globalCompositeOperation = this.blendMode;
332
+ renderer.compositeOnto(ctx, compositeX, compositeY);
333
+ ctx.globalCompositeOperation = "source-over";
334
+ });
335
+ }
336
+
194
337
  /**
195
338
  * Simple 2D rendering (no depth sorting).
196
339
  */
@@ -304,6 +447,27 @@ export class ParticleSystem extends GameObject {
304
447
  this._particleCount = 0;
305
448
  }
306
449
 
450
+ /**
451
+ * Destroy the particle system and free resources.
452
+ */
453
+ destroy() {
454
+ this.clear();
455
+ if (this.webglRenderer) {
456
+ this.webglRenderer.destroy();
457
+ this.webglRenderer = null;
458
+ }
459
+ this.pool = [];
460
+ this.emitters.clear();
461
+ }
462
+
463
+ /**
464
+ * Check if WebGL rendering is active.
465
+ * @type {boolean}
466
+ */
467
+ get isWebGL() {
468
+ return this.webglRenderer !== null && this.webglRenderer.isAvailable();
469
+ }
470
+
307
471
  /**
308
472
  * Get current particle count.
309
473
  * @type {number}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @module physics
3
+ * @description Physics module for particle simulations.
4
+ *
5
+ * Provides:
6
+ * - Physics: Stateless physics calculations (collision, forces, kinematics)
7
+ * - PhysicsUpdaters: Composable updaters for ParticleSystem
8
+ *
9
+ * @example
10
+ * import { Physics, PhysicsUpdaters } from '@guinetik/gcanvas';
11
+ *
12
+ * // Use Physics directly for custom calculations
13
+ * const collision = Physics.checkCollision(p1, p2);
14
+ *
15
+ * // Use PhysicsUpdaters with ParticleSystem
16
+ * const system = new ParticleSystem(game, {
17
+ * updaters: [
18
+ * Updaters.velocity,
19
+ * PhysicsUpdaters.particleCollisions(0.9),
20
+ * PhysicsUpdaters.bounds3D(bounds),
21
+ * ]
22
+ * });
23
+ */
24
+
25
+ export { Physics } from './physics.js';
26
+ export { PhysicsUpdaters } from './physics-updaters.js';