@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
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Lorenz Attractor 3D Visualization
3
+ *
4
+ * The classic "butterfly effect" attractor discovered by Edward Lorenz (1963)
5
+ * while studying atmospheric convection. Particles follow the chaotic
6
+ * trajectories colored by velocity (blue=slow, red=fast).
7
+ *
8
+ * Uses the Attractors module for pure math functions and WebGL for
9
+ * high-performance line rendering.
10
+ */
11
+
12
+ import { Game, Gesture, Screen, Attractors } from "/gcanvas.es.min.js";
13
+ import { Camera3D } from "/gcanvas.es.min.js";
14
+ import { WebGLLineRenderer } from "/gcanvas.es.min.js";
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // CONFIGURATION
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ const CONFIG = {
21
+ // Attractor settings (uses Attractors.lorenz for equations)
22
+ attractor: {
23
+ dt: 0.005, // Integration time step
24
+ scale: 12, // Scale factor for display (Lorenz is larger)
25
+ },
26
+
27
+ // Particle settings
28
+ particles: {
29
+ count: 400,
30
+ trailLength: 250,
31
+ spawnRange: 2, // Initial position range around origin
32
+ },
33
+
34
+ // Center offset - Lorenz attractor orbits around z≈27 (ρ-1)
35
+ center: {
36
+ x: 5,
37
+ y: 0,
38
+ z: 27,
39
+ },
40
+
41
+ // Camera settings - angled to show butterfly shape
42
+ camera: {
43
+ perspective: 800,
44
+ rotationX: -2, // Tilt to see butterfly spread
45
+ rotationY: -3, // Rotated to face the wings
46
+ inertia: true,
47
+ friction: 0.95,
48
+ clampX: false,
49
+ },
50
+
51
+ // Visual settings
52
+ visual: {
53
+ minHue: 30, // Orange-red (fast)
54
+ maxHue: 200, // Cyan-blue (slow)
55
+ maxSpeed: 50, // Speed normalization threshold
56
+ saturation: 85,
57
+ lightness: 55,
58
+ maxAlpha: 0.85,
59
+ hueShiftSpeed: 15, // Degrees per second (0 to disable)
60
+ },
61
+
62
+ // Glitch/blink effect
63
+ blink: {
64
+ chance: 0.015,
65
+ minDuration: 0.05,
66
+ maxDuration: 0.25,
67
+ intensityBoost: 1.4,
68
+ saturationBoost: 1.15,
69
+ alphaBoost: 1.25,
70
+ },
71
+
72
+ // Zoom settings
73
+ zoom: {
74
+ min: 0.2,
75
+ max: 2.5,
76
+ speed: 0.5,
77
+ easing: 0.12,
78
+ baseScreenSize: 600,
79
+ },
80
+ };
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // HELPER FUNCTIONS
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Convert HSL to RGB
88
+ */
89
+ function hslToRgb(h, s, l) {
90
+ s /= 100;
91
+ l /= 100;
92
+ const k = (n) => (n + h / 30) % 12;
93
+ const a = s * Math.min(l, 1 - l);
94
+ const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
95
+ return {
96
+ r: Math.round(255 * f(0)),
97
+ g: Math.round(255 * f(8)),
98
+ b: Math.round(255 * f(4)),
99
+ };
100
+ }
101
+
102
+ // ─────────────────────────────────────────────────────────────────────────────
103
+ // ATTRACTOR PARTICLE
104
+ // ─────────────────────────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * A particle following attractor dynamics
108
+ */
109
+ class AttractorParticle {
110
+ /**
111
+ * @param {Function} stepFn - Attractor step function
112
+ * @param {number} spawnRange - Initial position range
113
+ */
114
+ constructor(stepFn, spawnRange) {
115
+ this.stepFn = stepFn;
116
+ this.position = {
117
+ x: (Math.random() - 0.5) * spawnRange,
118
+ y: (Math.random() - 0.5) * spawnRange,
119
+ z: (Math.random() - 0.5) * spawnRange + CONFIG.center.z, // Start near attractor
120
+ };
121
+ this.trail = [];
122
+ this.speed = 0;
123
+
124
+ // Blink/glitch state
125
+ this.blinkTime = 0;
126
+ this.blinkIntensity = 0;
127
+ }
128
+
129
+ /**
130
+ * Update blink state
131
+ */
132
+ updateBlink(dt) {
133
+ const { chance, minDuration, maxDuration } = CONFIG.blink;
134
+
135
+ if (this.blinkTime > 0) {
136
+ this.blinkTime -= dt;
137
+ this.blinkIntensity = Math.max(
138
+ 0,
139
+ this.blinkTime > 0
140
+ ? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
141
+ : 0
142
+ );
143
+ } else {
144
+ if (Math.random() < chance) {
145
+ this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
146
+ this.blinkIntensity = 1;
147
+ } else {
148
+ this.blinkIntensity = 0;
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Update particle position using attractor
155
+ */
156
+ update(dt, scale) {
157
+ // Use the attractor step function
158
+ const result = this.stepFn(this.position, dt);
159
+
160
+ // Update position
161
+ this.position = result.position;
162
+ this.speed = result.speed;
163
+
164
+ // Add to trail (scaled and centered for display)
165
+ // Subtract center offset so attractor rotates around its center
166
+ this.trail.unshift({
167
+ x: (this.position.x - CONFIG.center.x) * scale,
168
+ y: (this.position.y - CONFIG.center.y) * scale,
169
+ z: (this.position.z - CONFIG.center.z) * scale,
170
+ speed: this.speed,
171
+ });
172
+
173
+ // Trim trail
174
+ if (this.trail.length > CONFIG.particles.trailLength) {
175
+ this.trail.pop();
176
+ }
177
+ }
178
+ }
179
+
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+ // DEMO CLASS
182
+ // ─────────────────────────────────────────────────────────────────────────────
183
+
184
+ /**
185
+ * Lorenz Attractor Demo
186
+ */
187
+ class LorenzDemo extends Game {
188
+ constructor(canvas) {
189
+ super(canvas);
190
+ this.backgroundColor = "#000";
191
+ this.enableFluidSize();
192
+ }
193
+
194
+ init() {
195
+ super.init();
196
+
197
+ // Get attractor info for display
198
+ this.attractor = Attractors.lorenz;
199
+ console.log(`Attractor: ${this.attractor.name}`);
200
+ console.log(`Equations:`, this.attractor.equations);
201
+
202
+ // Create stepper function with default params
203
+ this.stepFn = this.attractor.createStepper();
204
+
205
+ // Calculate initial zoom
206
+ const { min, max, baseScreenSize } = CONFIG.zoom;
207
+ const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
208
+ this.zoom = initialZoom;
209
+ this.targetZoom = initialZoom;
210
+ this.defaultZoom = initialZoom;
211
+
212
+ // Camera with mouse control
213
+ this.camera = new Camera3D({
214
+ perspective: CONFIG.camera.perspective,
215
+ rotationX: CONFIG.camera.rotationX,
216
+ rotationY: CONFIG.camera.rotationY,
217
+ inertia: CONFIG.camera.inertia,
218
+ friction: CONFIG.camera.friction,
219
+ clampX: CONFIG.camera.clampX,
220
+ });
221
+ this.camera.enableMouseControl(this.canvas);
222
+
223
+ // Gesture handler for zoom
224
+ this.gesture = new Gesture(this.canvas, {
225
+ onZoom: (delta) => {
226
+ this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
227
+ },
228
+ onPan: null,
229
+ });
230
+
231
+ // Double-click to reset
232
+ this.canvas.addEventListener("dblclick", () => {
233
+ this.targetZoom = this.defaultZoom;
234
+ });
235
+
236
+ // Log camera params on mouse release (for finding good starting angle)
237
+ this.canvas.addEventListener("mouseup", () => {
238
+ console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
239
+ });
240
+
241
+ // Initialize particles using the attractor step function
242
+ this.particles = [];
243
+ for (let i = 0; i < CONFIG.particles.count; i++) {
244
+ this.particles.push(
245
+ new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange)
246
+ );
247
+ }
248
+
249
+ // WebGL line renderer
250
+ const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
251
+ this.lineRenderer = new WebGLLineRenderer(maxSegments, {
252
+ width: this.width,
253
+ height: this.height,
254
+ blendMode: "additive",
255
+ });
256
+
257
+ this.segments = [];
258
+
259
+ if (!this.lineRenderer.isAvailable()) {
260
+ console.warn("WebGL not available, falling back to Canvas 2D");
261
+ this.useWebGL = false;
262
+ } else {
263
+ this.useWebGL = true;
264
+ console.log(`WebGL enabled, ${maxSegments} max segments`);
265
+ }
266
+
267
+ this.time = 0;
268
+ }
269
+
270
+ onResize() {
271
+ if (this.lineRenderer?.isAvailable()) {
272
+ this.lineRenderer.resize(this.width, this.height);
273
+ }
274
+ const { min, max, baseScreenSize } = CONFIG.zoom;
275
+ this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
276
+ }
277
+
278
+ update(dt) {
279
+ super.update(dt);
280
+ this.camera.update(dt);
281
+
282
+ // Normalize rotation to prevent unbounded values
283
+ const TAU = Math.PI * 2;
284
+ this.camera.rotationY = ((this.camera.rotationY % TAU) + TAU) % TAU;
285
+
286
+ this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
287
+ this.time += dt;
288
+
289
+ for (const particle of this.particles) {
290
+ particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale);
291
+ particle.updateBlink(dt);
292
+ }
293
+ }
294
+
295
+ collectSegments(cx, cy) {
296
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
297
+ CONFIG.visual;
298
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
299
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
300
+
301
+ this.segments.length = 0;
302
+
303
+ for (const particle of this.particles) {
304
+ if (particle.trail.length < 2) continue;
305
+
306
+ const blink = particle.blinkIntensity;
307
+
308
+ for (let i = 1; i < particle.trail.length; i++) {
309
+ const curr = particle.trail[i];
310
+ const prev = particle.trail[i - 1];
311
+
312
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
313
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
314
+
315
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
316
+
317
+ const age = i / particle.trail.length;
318
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
319
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
320
+ const hue = (baseHue + hueOffset) % 360;
321
+
322
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
323
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
324
+ const rgb = hslToRgb(hue, sat, lit);
325
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
326
+
327
+ this.segments.push({
328
+ x1: cx + p1.x * this.zoom,
329
+ y1: cy + p1.y * this.zoom,
330
+ x2: cx + p2.x * this.zoom,
331
+ y2: cy + p2.y * this.zoom,
332
+ r: rgb.r,
333
+ g: rgb.g,
334
+ b: rgb.b,
335
+ a: alpha,
336
+ });
337
+ }
338
+ }
339
+
340
+ return this.segments.length;
341
+ }
342
+
343
+ renderCanvas2D(cx, cy) {
344
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
345
+ CONFIG.visual;
346
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
347
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
348
+
349
+ const ctx = this.ctx;
350
+ ctx.save();
351
+ ctx.globalCompositeOperation = "lighter";
352
+ ctx.lineCap = "round";
353
+
354
+ for (const particle of this.particles) {
355
+ if (particle.trail.length < 2) continue;
356
+
357
+ const blink = particle.blinkIntensity;
358
+
359
+ for (let i = 1; i < particle.trail.length; i++) {
360
+ const curr = particle.trail[i];
361
+ const prev = particle.trail[i - 1];
362
+
363
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
364
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
365
+
366
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
367
+
368
+ const age = i / particle.trail.length;
369
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
370
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
371
+ const hue = (baseHue + hueOffset) % 360;
372
+
373
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
374
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
375
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
376
+
377
+ ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
378
+ ctx.lineWidth = 1;
379
+
380
+ ctx.beginPath();
381
+ ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
382
+ ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
383
+ ctx.stroke();
384
+ }
385
+ }
386
+
387
+ ctx.restore();
388
+ }
389
+
390
+ render() {
391
+ super.render();
392
+ if (!this.particles) return;
393
+
394
+ const cx = this.width / 2;
395
+ const cy = this.height / 2;
396
+
397
+ if (this.useWebGL && this.lineRenderer.isAvailable()) {
398
+ const segmentCount = this.collectSegments(cx, cy);
399
+ if (segmentCount > 0) {
400
+ this.lineRenderer.clear();
401
+ this.lineRenderer.updateLines(this.segments);
402
+ this.lineRenderer.render(segmentCount);
403
+ this.lineRenderer.compositeOnto(this.ctx, 0, 0);
404
+ }
405
+ } else {
406
+ this.renderCanvas2D(cx, cy);
407
+ }
408
+ }
409
+
410
+ destroy() {
411
+ this.gesture?.destroy();
412
+ this.lineRenderer?.destroy();
413
+ super.destroy?.();
414
+ }
415
+ }
416
+
417
+ // ─────────────────────────────────────────────────────────────────────────────
418
+ // INITIALIZATION
419
+ // ─────────────────────────────────────────────────────────────────────────────
420
+
421
+ window.addEventListener("load", () => {
422
+ const canvas = document.getElementById("game");
423
+ const demo = new LorenzDemo(canvas);
424
+ demo.start();
425
+ });
@@ -289,7 +289,7 @@ class PaintScene extends GameObject {
289
289
  // First check total point count
290
290
  this.totalPoints = this.strokes.reduce(
291
291
  (sum, stroke) => sum + stroke.points.length,
292
- 0
292
+ 0,
293
293
  );
294
294
 
295
295
  // If too many points, start removing oldest strokes
@@ -310,7 +310,7 @@ class PaintScene extends GameObject {
310
310
  this.totalPoints -= pointsToRemove;
311
311
 
312
312
  console.log(
313
- `Removed ${this.REMOVE_BATCH} oldest strokes. ${this.strokes.length} strokes remaining.`
313
+ `Removed ${this.REMOVE_BATCH} oldest strokes. ${this.strokes.length} strokes remaining.`,
314
314
  );
315
315
  }
316
316
  }
@@ -324,7 +324,7 @@ class PaintScene extends GameObject {
324
324
  class UIScene extends Scene {
325
325
  constructor(game, paintScene, options = {}) {
326
326
  super(game, options);
327
- this.debug = true;
327
+ this.debug = false;
328
328
  this.debugColor = "yellow";
329
329
  this.paintScene = paintScene;
330
330
  this.layout = new HorizontalLayout(game, {
@@ -352,7 +352,7 @@ class UIScene extends Scene {
352
352
  currentTool = this.toolPencil;
353
353
  }
354
354
  },
355
- })
355
+ }),
356
356
  );
357
357
  this.toolEraser = this.layout.add(
358
358
  new ToggleButton(game, {
@@ -372,7 +372,7 @@ class UIScene extends Scene {
372
372
  currentTool = this.toolEraser;
373
373
  }
374
374
  },
375
- })
375
+ }),
376
376
  );
377
377
  this.toolLine = this.layout.add(
378
378
  new ToggleButton(game, {
@@ -392,7 +392,7 @@ class UIScene extends Scene {
392
392
  paintScene.setTool("line");
393
393
  }
394
394
  },
395
- })
395
+ }),
396
396
  );
397
397
  this.layout.add(
398
398
  new Button(game, {
@@ -408,7 +408,7 @@ class UIScene extends Scene {
408
408
  this.paintScene.activeStroke = null;
409
409
  this.paintScene.lineStart = null;
410
410
  },
411
- })
411
+ }),
412
412
  );
413
413
  let currentTool = this.toolPencil;
414
414
  this.add(this.layout);
@@ -433,7 +433,7 @@ class DemoGame extends Game {
433
433
  anchor: "bottom-right",
434
434
  width: 20,
435
435
  height: 20,
436
- })
436
+ }),
437
437
  );
438
438
  }
439
439