@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
@@ -7,7 +7,7 @@
7
7
  * Ψ(x,t) = A * e^(-(x-vt)²/4a²) * e^(i(kx-ωt))
8
8
  */
9
9
 
10
- import { Game, Painter, Camera3D, Text, applyAnchor, Position, Scene, verticalLayout, applyLayout } from "/gcanvas.es.min.js";
10
+ import { Game, Painter, Camera3D, Text, applyAnchor, Position, Scene, verticalLayout, applyLayout, Gesture, Screen } from "/gcanvas.es.min.js";
11
11
  import { gaussianWavePacket } from "/gcanvas.es.min.js";
12
12
 
13
13
  // Configuration
@@ -16,8 +16,8 @@ const CONFIG = {
16
16
  amplitude: 1.0,
17
17
  sigma: 0.8, // Width of Gaussian envelope
18
18
  k: 8.0, // Wave number (controls oscillation frequency)
19
- omega: 4.0, // Angular frequency
20
- velocity: 0.5, // Group velocity of packet
19
+ omega: 8.0, // Angular frequency (phase velocity = ω/k = 1.0)
20
+ velocity: 0.5, // Group velocity of packet (envelope moves slower than phase)
21
21
 
22
22
  // Visualization
23
23
  numPoints: 300, // Points along the wave
@@ -38,6 +38,17 @@ const CONFIG = {
38
38
  // Animation
39
39
  timeScale: 1.0,
40
40
 
41
+ // Zoom
42
+ minZoom: 0.3,
43
+ maxZoom: 3.0,
44
+ zoomSpeed: 0.5, // Zoom sensitivity (increased)
45
+ zoomEasing: 0.12, // Easing interpolation speed (0-1)
46
+ baseScreenSize: 600, // Reference screen size for zoom calculation
47
+
48
+ // Collapse interaction
49
+ collapseHoldTime: 200, // ms to hold before collapse triggers
50
+ collapseDragThreshold: 8, // px movement to cancel collapse (it's a drag)
51
+
41
52
  // Colors
42
53
  waveColor: [80, 160, 255], // Cyan-blue for the helix
43
54
  waveGlow: [150, 200, 255], // Brighter for the center
@@ -57,23 +68,59 @@ class SchrodingerDemo extends Game {
57
68
  super.init();
58
69
  this.time = 0;
59
70
 
60
- // Create 3D camera with mouse controls
71
+ // Calculate initial zoom based on screen size to fill canvas better
72
+ const initialZoom = Math.min(
73
+ CONFIG.maxZoom,
74
+ Math.max(CONFIG.minZoom, Screen.minDimension() / CONFIG.baseScreenSize)
75
+ );
76
+ this.zoom = initialZoom;
77
+ this.targetZoom = initialZoom;
78
+ this.defaultZoom = initialZoom;
79
+
80
+ // Create 3D camera with mouse controls and inertia
61
81
  this.camera = new Camera3D({
62
82
  rotationX: CONFIG.rotationX,
63
83
  rotationY: CONFIG.rotationY,
64
84
  perspective: CONFIG.perspective,
65
- minRotationX: -1.2,
66
- maxRotationX: 1.2,
85
+ clampX: false, // Allow free rotation without limits
86
+ inertia: true, // Enable momentum after drag release
87
+ friction: 0.94, // Velocity decay (higher = longer drift)
88
+ velocityScale: 1.2, // Multiplier for throw velocity
67
89
  });
68
90
 
69
91
  // Enable mouse/touch rotation
70
92
  this.camera.enableMouseControl(this.canvas);
71
93
 
72
- // Override double-click to also reset time
94
+ // Enable zoom via mouse wheel and pinch gesture
95
+ this.gesture = new Gesture(this.canvas, {
96
+ onZoom: (delta) => {
97
+ // Update target zoom (eases smoothly in update loop)
98
+ this.targetZoom *= 1 + delta * CONFIG.zoomSpeed;
99
+ this.targetZoom = Math.max(CONFIG.minZoom, Math.min(CONFIG.maxZoom, this.targetZoom));
100
+ },
101
+ // Don't handle pan - Camera3D handles rotation via drag
102
+ onPan: null,
103
+ });
104
+
105
+ // Override double-click to also reset time and zoom
73
106
  this.canvas.addEventListener("dblclick", () => {
74
107
  this.time = 0;
108
+ this.targetZoom = this.defaultZoom;
75
109
  });
76
110
 
111
+ // Collapse interaction state
112
+ this.isCollapsed = false;
113
+ this.collapseAmount = 0; // 0 = normal, 1 = fully collapsed (animated)
114
+ this.collapseOffset = 0; // Current offset (tweening)
115
+ this.collapseTargetOffset = 0; // Target offset to tween toward
116
+ this.collapseTimer = null;
117
+ this.collapseSampleTimer = null; // Timer for periodic resampling
118
+ this.pointerStartX = 0;
119
+ this.pointerStartY = 0;
120
+
121
+ // Setup collapse detection (hold without dragging = measure/collapse)
122
+ this.setupCollapseInteraction();
123
+
77
124
  // Create info panel container anchored to top center
78
125
  this.infoPanel = new Scene(this, { x: 0, y: 0 });
79
126
  applyAnchor(this.infoPanel, {
@@ -137,16 +184,157 @@ class SchrodingerDemo extends Game {
137
184
  }
138
185
 
139
186
  /**
140
- * 3D rotation and projection - delegates to Camera3D
187
+ * Setup collapse interaction - hold without moving triggers wave function collapse
188
+ */
189
+ setupCollapseInteraction() {
190
+ const startCollapse = (x, y) => {
191
+ this.pointerStartX = x;
192
+ this.pointerStartY = y;
193
+
194
+ // Start timer - if we hold long enough without moving, collapse
195
+ this.collapseTimer = setTimeout(() => {
196
+ if (!this.isCollapsed) {
197
+ this.collapse();
198
+ }
199
+ }, CONFIG.collapseHoldTime);
200
+ };
201
+
202
+ const checkDrag = (x, y) => {
203
+ // If moved beyond threshold, cancel collapse timer (it's a drag)
204
+ const dx = Math.abs(x - this.pointerStartX);
205
+ const dy = Math.abs(y - this.pointerStartY);
206
+ if (dx > CONFIG.collapseDragThreshold || dy > CONFIG.collapseDragThreshold) {
207
+ this.cancelCollapseTimer();
208
+ }
209
+ };
210
+
211
+ const endCollapse = () => {
212
+ this.cancelCollapseTimer();
213
+ this.stopCollapseSampling();
214
+ this.isCollapsed = false;
215
+ };
216
+
217
+ // Mouse events
218
+ this.canvas.addEventListener("mousedown", (e) => {
219
+ startCollapse(e.clientX, e.clientY);
220
+ });
221
+ this.canvas.addEventListener("mousemove", (e) => {
222
+ checkDrag(e.clientX, e.clientY);
223
+ });
224
+ this.canvas.addEventListener("mouseup", endCollapse);
225
+ this.canvas.addEventListener("mouseleave", endCollapse);
226
+
227
+ // Touch events
228
+ this.canvas.addEventListener("touchstart", (e) => {
229
+ if (e.touches.length === 1) {
230
+ startCollapse(e.touches[0].clientX, e.touches[0].clientY);
231
+ }
232
+ });
233
+ this.canvas.addEventListener("touchmove", (e) => {
234
+ if (e.touches.length === 1) {
235
+ checkDrag(e.touches[0].clientX, e.touches[0].clientY);
236
+ } else {
237
+ // Multi-touch (pinch) cancels collapse
238
+ this.cancelCollapseTimer();
239
+ }
240
+ });
241
+ this.canvas.addEventListener("touchend", endCollapse);
242
+ this.canvas.addEventListener("touchcancel", endCollapse);
243
+ }
244
+
245
+ /**
246
+ * Cancel any pending collapse timer
247
+ */
248
+ cancelCollapseTimer() {
249
+ if (this.collapseTimer) {
250
+ clearTimeout(this.collapseTimer);
251
+ this.collapseTimer = null;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Sample a new random offset from Gaussian distribution
257
+ */
258
+ sampleCollapseOffset() {
259
+ const u1 = Math.random();
260
+ const u2 = Math.random();
261
+ const gaussian = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
262
+ return gaussian * CONFIG.sigma;
263
+ }
264
+
265
+ /**
266
+ * Collapse the wave function - start sampling positions periodically
267
+ */
268
+ collapse() {
269
+ this.isCollapsed = true;
270
+
271
+ // Initial sample
272
+ this.collapseOffset = this.sampleCollapseOffset();
273
+ this.collapseTargetOffset = this.collapseOffset;
274
+
275
+ // Start periodic resampling while collapsed
276
+ this.startCollapseSampling();
277
+ }
278
+
279
+ /**
280
+ * Start periodic position sampling while collapsed
281
+ */
282
+ startCollapseSampling() {
283
+ this.stopCollapseSampling();
284
+
285
+ const resample = () => {
286
+ if (this.isCollapsed) {
287
+ // Set new target position
288
+ this.collapseTargetOffset = this.sampleCollapseOffset();
289
+ // Schedule next sample (300-600ms random interval)
290
+ this.collapseSampleTimer = setTimeout(resample, 300 + Math.random() * 300);
291
+ }
292
+ };
293
+
294
+ // First resample after initial delay
295
+ this.collapseSampleTimer = setTimeout(resample, 400);
296
+ }
297
+
298
+ /**
299
+ * Stop periodic sampling
300
+ */
301
+ stopCollapseSampling() {
302
+ if (this.collapseSampleTimer) {
303
+ clearTimeout(this.collapseSampleTimer);
304
+ this.collapseSampleTimer = null;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * 3D rotation and projection - delegates to Camera3D with zoom applied
141
310
  */
142
311
  project3D(x, y, z) {
143
- return this.camera.project(x, y, z);
312
+ const proj = this.camera.project(x, y, z);
313
+ return {
314
+ x: proj.x * this.zoom,
315
+ y: proj.y * this.zoom,
316
+ z: proj.z,
317
+ scale: proj.scale * this.zoom,
318
+ };
144
319
  }
145
320
 
146
321
  update(dt) {
147
322
  super.update(dt);
148
323
  this.time += dt * CONFIG.timeScale;
149
324
 
325
+ // Update camera for inertia physics
326
+ this.camera.update(dt);
327
+
328
+ // Ease zoom towards target
329
+ this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoomEasing;
330
+
331
+ // Animate collapse amount
332
+ const targetCollapse = this.isCollapsed ? 1 : 0;
333
+ this.collapseAmount += (targetCollapse - this.collapseAmount) * 0.15;
334
+
335
+ // Tween collapse offset toward target (smooth bezier-like easing)
336
+ this.collapseOffset += (this.collapseTargetOffset - this.collapseOffset) * 0.08;
337
+
150
338
  // Loop when wave packet exits the visible range
151
339
  const packetCenter = CONFIG.velocity * this.time;
152
340
  if (packetCenter > CONFIG.xRange * 0.6) {
@@ -161,6 +349,14 @@ class SchrodingerDemo extends Game {
161
349
  }
162
350
  }
163
351
 
352
+ onResize() {
353
+ // Recalculate default zoom for new screen size
354
+ this.defaultZoom = Math.min(
355
+ CONFIG.maxZoom,
356
+ Math.max(CONFIG.minZoom, Screen.minDimension() / CONFIG.baseScreenSize)
357
+ );
358
+ }
359
+
164
360
  render() {
165
361
  const w = this.width;
166
362
  const h = this.height;
@@ -173,6 +369,12 @@ class SchrodingerDemo extends Game {
173
369
  const points = [];
174
370
  const { numPoints, xRange, helixRadius, zScale } = CONFIG;
175
371
 
372
+ // Collapsed target position = sampled position (center + offset)
373
+ const packetCenter = CONFIG.velocity * this.time;
374
+ const collapsedX = packetCenter + this.collapseOffset;
375
+ const collapsedZ = collapsedX * zScale;
376
+ const collapse = this.collapseAmount;
377
+
176
378
  for (let i = 0; i < numPoints; i++) {
177
379
  const t_param = i / (numPoints - 1);
178
380
  const x = (t_param - 0.5) * xRange;
@@ -180,9 +382,16 @@ class SchrodingerDemo extends Game {
180
382
  const { psi, envelope } = this.computeWavePacket(x, this.time);
181
383
 
182
384
  // 3D coordinates: helix in Re/Im plane, extending along Z
183
- const px = psi.real * helixRadius; // Re(Ψ) -> X
184
- const py = psi.imag * helixRadius; // Im(Ψ) -> Y
185
- const pz = x * zScale; // position -> Z
385
+ let px = psi.real * helixRadius; // Re(Ψ) -> X
386
+ let py = psi.imag * helixRadius; // Im(Ψ) -> Y
387
+ let pz = x * zScale; // position -> Z
388
+
389
+ // Collapse animation: lerp all points toward the collapsed position
390
+ if (collapse > 0.01) {
391
+ px = px * (1 - collapse); // X collapses to 0
392
+ py = py * (1 - collapse); // Y collapses to 0
393
+ pz = pz + (collapsedZ - pz) * collapse; // Z collapses to measured position
394
+ }
186
395
 
187
396
  const projected = this.project3D(px, py, pz);
188
397
 
@@ -218,10 +427,93 @@ class SchrodingerDemo extends Game {
218
427
  // Draw projection on grid (2D wave)
219
428
  this.drawProjection(cx, cy, points);
220
429
 
430
+ // Draw collapse indicator if collapsed
431
+ if (this.isCollapsed) {
432
+ this.drawCollapseIndicator(cx, cy);
433
+ }
434
+
221
435
  // Info text
222
436
  this.drawInfo(w, h);
223
437
  }
224
438
 
439
+ /**
440
+ * Draw squiggly line from collapsed position to red envelope curve
441
+ */
442
+ drawCollapseIndicator(cx, cy) {
443
+ const { zScale, gridY, helixRadius } = CONFIG;
444
+ const collapse = this.collapseAmount;
445
+
446
+ // Sampled collapse position (center + random offset, travels with envelope)
447
+ const packetCenter = CONFIG.velocity * this.time;
448
+ const collapsedX = packetCenter + this.collapseOffset;
449
+
450
+ // Get the envelope height at the sampled position
451
+ const { envelope } = this.computeWavePacket(collapsedX, this.time);
452
+ const envHeight = envelope * helixRadius * 0.8;
453
+
454
+ // Project points: collapsed position on axis and on envelope
455
+ const axisProj = this.project3D(0, 0, collapsedX * zScale);
456
+ const envelopeProj = this.project3D(0, gridY - envHeight, collapsedX * zScale);
457
+
458
+ // Draw squiggly line from axis to envelope (quantum fluctuations)
459
+ Painter.useCtx((ctx) => {
460
+ ctx.strokeStyle = `rgba(255, 255, 100, ${0.8 * collapse})`;
461
+ ctx.lineWidth = 2;
462
+ ctx.lineCap = "round";
463
+ ctx.lineJoin = "round";
464
+
465
+ const startX = cx + axisProj.x;
466
+ const startY = cy + axisProj.y;
467
+ const endX = cx + envelopeProj.x;
468
+ const endY = cy + envelopeProj.y;
469
+
470
+ // Number of segments and wave properties
471
+ const segments = 20;
472
+ const waveAmplitude = 8 * collapse;
473
+ const waveFrequency = 3;
474
+ const timeOffset = this.time * 10; // Animate the squiggle
475
+
476
+ ctx.moveTo(startX, startY);
477
+
478
+ for (let i = 1; i <= segments; i++) {
479
+ const t = i / segments;
480
+ // Linear interpolation along the line
481
+ const baseX = startX + (endX - startX) * t;
482
+ const baseY = startY + (endY - startY) * t;
483
+
484
+ // Perpendicular offset for squiggle (sine wave)
485
+ const angle = Math.atan2(endY - startY, endX - startX) + Math.PI / 2;
486
+ const wave = Math.sin(t * Math.PI * waveFrequency * 2 + timeOffset) * waveAmplitude * (1 - Math.abs(t - 0.5) * 2);
487
+
488
+ const squiggleX = baseX + Math.cos(angle) * wave;
489
+ const squiggleY = baseY + Math.sin(angle) * wave;
490
+
491
+ ctx.lineTo(squiggleX, squiggleY);
492
+ }
493
+ ctx.stroke();
494
+ });
495
+
496
+ // Draw particle dot on axis (where it collapsed to)
497
+ Painter.useCtx((ctx) => {
498
+ ctx.fillStyle = `rgba(255, 255, 100, ${collapse})`;
499
+ ctx.shadowColor = "#ffff66";
500
+ ctx.shadowBlur = 12 * collapse;
501
+ ctx.arc(cx + axisProj.x, cy + axisProj.y, 5 * axisProj.scale, 0, Math.PI * 2);
502
+ ctx.fill();
503
+ ctx.shadowBlur = 0;
504
+ });
505
+
506
+ // Draw dot on the envelope curve
507
+ Painter.useCtx((ctx) => {
508
+ ctx.fillStyle = `rgba(255, 255, 100, ${collapse})`;
509
+ ctx.shadowColor = "#ffff66";
510
+ ctx.shadowBlur = 10 * collapse;
511
+ ctx.arc(cx + envelopeProj.x, cy + envelopeProj.y, 5 * envelopeProj.scale, 0, Math.PI * 2);
512
+ ctx.fill();
513
+ ctx.shadowBlur = 0;
514
+ });
515
+ }
516
+
225
517
  drawGrid(cx, cy) {
226
518
  const { gridSize, gridSpacing, gridY } = CONFIG;
227
519
  const halfGrid = (gridSize * gridSpacing) / 2;
@@ -260,28 +552,32 @@ class SchrodingerDemo extends Game {
260
552
 
261
553
  drawAxis(cx, cy) {
262
554
  const { zScale, xRange } = CONFIG;
555
+
556
+ // Fade out when collapsed (losing momentum information)
557
+ const fade = 1 - this.collapseAmount;
558
+ if (fade < 0.01) return; // Fully collapsed, don't draw
263
559
 
264
560
  // Main position axis (Z direction in 3D space)
265
561
  const p1 = this.project3D(0, 0, -xRange / 2 * zScale * 1.2);
266
562
  const p2 = this.project3D(0, 0, xRange / 2 * zScale * 1.2);
267
563
 
268
- // Glowing axis line
564
+ // Glowing axis line (fades with collapse)
269
565
  Painter.useCtx((ctx) => {
270
- ctx.strokeStyle = CONFIG.axisColor;
566
+ ctx.strokeStyle = `rgba(100, 150, 200, ${0.6 * fade})`;
271
567
  ctx.lineWidth = 2;
272
568
  ctx.moveTo(cx + p1.x, cy + p1.y);
273
569
  ctx.lineTo(cx + p2.x, cy + p2.y);
274
570
  ctx.stroke();
275
571
  });
276
572
 
277
- // Bright center dot where wave packet is
573
+ // Bright center dot where wave packet is (fades with collapse)
278
574
  const packetCenter = CONFIG.velocity * this.time;
279
575
  const centerProj = this.project3D(0, 0, packetCenter * zScale);
280
576
 
281
577
  Painter.useCtx((ctx) => {
282
- ctx.fillStyle = "#fff";
578
+ ctx.fillStyle = `rgba(255, 255, 255, ${fade})`;
283
579
  ctx.shadowColor = "#88ccff";
284
- ctx.shadowBlur = 20;
580
+ ctx.shadowBlur = 20 * fade;
285
581
  ctx.arc(cx + centerProj.x, cy + centerProj.y, 4, 0, Math.PI * 2);
286
582
  ctx.fill();
287
583
  ctx.shadowBlur = 0;
@@ -394,7 +690,7 @@ class SchrodingerDemo extends Game {
394
690
  ctx.fillStyle = "#445";
395
691
  ctx.font = "10px monospace";
396
692
  ctx.textAlign = "right";
397
- ctx.fillText("drag to rotate | double-click to reset", w - 15, h - 30);
693
+ ctx.fillText("drag to rotate | scroll to zoom | hold to collapse | double-click to reset", w - 15, h - 30);
398
694
 
399
695
  // Legend
400
696
  ctx.fillText("Helix = \u03A8 | Blue = Re(\u03A8) | Red = |\u03A8|\u00B2", w - 15, h - 15);