@guinetik/gcanvas 1.0.4 → 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 (261) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/aizawa.html +27 -0
  3. package/dist/animations.html +31 -0
  4. package/dist/basic.html +38 -0
  5. package/dist/baskara.html +31 -0
  6. package/dist/bezier.html +35 -0
  7. package/dist/beziersignature.html +29 -0
  8. package/dist/blackhole.html +28 -0
  9. package/dist/blob.html +35 -0
  10. package/dist/clifford.html +25 -0
  11. package/dist/cmb.html +24 -0
  12. package/dist/coordinates.html +698 -0
  13. package/dist/cube3d.html +23 -0
  14. package/dist/dadras.html +26 -0
  15. package/dist/dejong.html +25 -0
  16. package/dist/demos.css +303 -0
  17. package/dist/dino.html +42 -0
  18. package/dist/easing.html +28 -0
  19. package/dist/events.html +195 -0
  20. package/dist/fluent.html +647 -0
  21. package/dist/fluid-simple.html +22 -0
  22. package/dist/fluid.html +37 -0
  23. package/dist/fractals.html +36 -0
  24. package/dist/gameobjects.html +626 -0
  25. package/dist/gcanvas.es.js +14368 -9093
  26. package/dist/gcanvas.es.min.js +1 -1
  27. package/dist/gcanvas.umd.js +1 -1
  28. package/dist/gcanvas.umd.min.js +1 -1
  29. package/dist/genart.html +26 -0
  30. package/dist/gendream.html +26 -0
  31. package/dist/group.html +36 -0
  32. package/dist/halvorsen.html +27 -0
  33. package/dist/home.html +587 -0
  34. package/dist/hyperbolic001.html +23 -0
  35. package/dist/hyperbolic002.html +23 -0
  36. package/dist/hyperbolic003.html +23 -0
  37. package/dist/hyperbolic004.html +23 -0
  38. package/dist/hyperbolic005.html +22 -0
  39. package/dist/index.html +446 -0
  40. package/dist/isometric.html +34 -0
  41. package/dist/js/aizawa.js +425 -0
  42. package/dist/js/animations.js +452 -0
  43. package/dist/js/basic.js +204 -0
  44. package/dist/js/baskara.js +751 -0
  45. package/dist/js/bezier.js +692 -0
  46. package/dist/js/beziersignature.js +241 -0
  47. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  48. package/dist/js/blackhole/blackhole.obj.js +318 -0
  49. package/dist/js/blackhole/index.js +409 -0
  50. package/dist/js/blackhole/particle.js +56 -0
  51. package/dist/js/blackhole/starfield.obj.js +218 -0
  52. package/dist/js/blob.js +2276 -0
  53. package/dist/js/clifford.js +236 -0
  54. package/dist/js/cmb.js +594 -0
  55. package/dist/js/coordinates.js +840 -0
  56. package/dist/js/cube3d.js +789 -0
  57. package/dist/js/dadras.js +405 -0
  58. package/dist/js/dejong.js +257 -0
  59. package/dist/js/dino.js +1420 -0
  60. package/dist/js/easing.js +477 -0
  61. package/dist/js/fluent.js +183 -0
  62. package/dist/js/fluid-simple.js +253 -0
  63. package/dist/js/fluid.js +527 -0
  64. package/dist/js/fractals.js +932 -0
  65. package/dist/js/fractalworker.js +93 -0
  66. package/dist/js/gameobjects.js +176 -0
  67. package/dist/js/genart.js +268 -0
  68. package/dist/js/gendream.js +209 -0
  69. package/dist/js/group.js +140 -0
  70. package/dist/js/halvorsen.js +405 -0
  71. package/dist/js/hyperbolic001.js +310 -0
  72. package/dist/js/hyperbolic002.js +388 -0
  73. package/dist/js/hyperbolic003.js +319 -0
  74. package/dist/js/hyperbolic004.js +345 -0
  75. package/dist/js/hyperbolic005.js +340 -0
  76. package/dist/js/info-toggle.js +25 -0
  77. package/dist/js/isometric.js +851 -0
  78. package/dist/js/kerr.js +1547 -0
  79. package/dist/js/lavalamp.js +590 -0
  80. package/dist/js/layout.js +354 -0
  81. package/dist/js/lorenz.js +425 -0
  82. package/dist/js/mondrian.js +285 -0
  83. package/dist/js/opacity.js +275 -0
  84. package/dist/js/painter.js +484 -0
  85. package/dist/js/particles-showcase.js +514 -0
  86. package/dist/js/particles.js +299 -0
  87. package/dist/js/patterns.js +397 -0
  88. package/dist/js/penrose/artifact.js +69 -0
  89. package/dist/js/penrose/blackhole.js +121 -0
  90. package/dist/js/penrose/constants.js +73 -0
  91. package/dist/js/penrose/game.js +943 -0
  92. package/dist/js/penrose/lore.js +278 -0
  93. package/dist/js/penrose/penrosescene.js +892 -0
  94. package/dist/js/penrose/ship.js +216 -0
  95. package/dist/js/penrose/sounds.js +211 -0
  96. package/dist/js/penrose/voidparticle.js +55 -0
  97. package/dist/js/penrose/voidscene.js +258 -0
  98. package/dist/js/penrose/voidship.js +144 -0
  99. package/dist/js/penrose/wormhole.js +46 -0
  100. package/dist/js/pipeline.js +555 -0
  101. package/dist/js/plane3d.js +256 -0
  102. package/dist/js/platformer.js +1579 -0
  103. package/dist/js/rossler.js +480 -0
  104. package/dist/js/scene.js +304 -0
  105. package/dist/js/scenes.js +320 -0
  106. package/dist/js/schrodinger.js +706 -0
  107. package/dist/js/schwarzschild.js +1015 -0
  108. package/dist/js/shapes.js +628 -0
  109. package/dist/js/space/alien.js +171 -0
  110. package/dist/js/space/boom.js +98 -0
  111. package/dist/js/space/boss.js +353 -0
  112. package/dist/js/space/buff.js +73 -0
  113. package/dist/js/space/bullet.js +102 -0
  114. package/dist/js/space/constants.js +85 -0
  115. package/dist/js/space/game.js +1884 -0
  116. package/dist/js/space/hud.js +112 -0
  117. package/dist/js/space/laserbeam.js +179 -0
  118. package/dist/js/space/lightning.js +277 -0
  119. package/dist/js/space/minion.js +192 -0
  120. package/dist/js/space/missile.js +212 -0
  121. package/dist/js/space/player.js +430 -0
  122. package/dist/js/space/powerup.js +90 -0
  123. package/dist/js/space/starfield.js +58 -0
  124. package/dist/js/space/starpower.js +90 -0
  125. package/dist/js/spacetime.js +559 -0
  126. package/dist/js/sphere3d.js +229 -0
  127. package/dist/js/sprite.js +473 -0
  128. package/dist/js/starfaux/config.js +118 -0
  129. package/dist/js/starfaux/enemy.js +353 -0
  130. package/dist/js/starfaux/hud.js +78 -0
  131. package/dist/js/starfaux/index.js +482 -0
  132. package/dist/js/starfaux/laser.js +182 -0
  133. package/dist/js/starfaux/player.js +468 -0
  134. package/dist/js/starfaux/terrain.js +560 -0
  135. package/dist/js/study001.js +275 -0
  136. package/dist/js/study002.js +366 -0
  137. package/dist/js/study003.js +331 -0
  138. package/dist/js/study004.js +389 -0
  139. package/dist/js/study005.js +209 -0
  140. package/dist/js/study006.js +194 -0
  141. package/dist/js/study007.js +192 -0
  142. package/dist/js/study008.js +413 -0
  143. package/dist/js/svgtween.js +204 -0
  144. package/dist/js/tde/accretiondisk.js +471 -0
  145. package/dist/js/tde/blackhole.js +219 -0
  146. package/dist/js/tde/blackholescene.js +209 -0
  147. package/dist/js/tde/config.js +59 -0
  148. package/dist/js/tde/index.js +820 -0
  149. package/dist/js/tde/jets.js +290 -0
  150. package/dist/js/tde/lensedstarfield.js +154 -0
  151. package/dist/js/tde/tdestar.js +297 -0
  152. package/dist/js/tde/tidalstream.js +372 -0
  153. package/dist/js/tde_old/blackhole.obj.js +354 -0
  154. package/dist/js/tde_old/debris.obj.js +791 -0
  155. package/dist/js/tde_old/flare.obj.js +239 -0
  156. package/dist/js/tde_old/index.js +448 -0
  157. package/dist/js/tde_old/star.obj.js +812 -0
  158. package/dist/js/tetris/config.js +157 -0
  159. package/dist/js/tetris/grid.js +286 -0
  160. package/dist/js/tetris/index.js +1195 -0
  161. package/dist/js/tetris/renderer.js +634 -0
  162. package/dist/js/tetris/tetrominos.js +280 -0
  163. package/dist/js/thomas.js +394 -0
  164. package/dist/js/tiles.js +312 -0
  165. package/dist/js/tweendemo.js +79 -0
  166. package/dist/js/visibility.js +102 -0
  167. package/dist/kerr.html +28 -0
  168. package/dist/lavalamp.html +27 -0
  169. package/dist/layouts.html +37 -0
  170. package/dist/logo.svg +4 -0
  171. package/dist/loop.html +84 -0
  172. package/dist/lorenz.html +27 -0
  173. package/dist/mondrian.html +32 -0
  174. package/dist/og_image.png +0 -0
  175. package/dist/opacity.html +36 -0
  176. package/dist/painter.html +39 -0
  177. package/dist/particles-showcase.html +28 -0
  178. package/dist/particles.html +24 -0
  179. package/dist/patterns.html +33 -0
  180. package/dist/penrose-game.html +31 -0
  181. package/dist/pipeline.html +737 -0
  182. package/dist/plane3d.html +24 -0
  183. package/dist/platformer.html +43 -0
  184. package/dist/rossler.html +27 -0
  185. package/dist/scene-interactivity-test.html +220 -0
  186. package/dist/scene.html +33 -0
  187. package/dist/scenes.html +96 -0
  188. package/dist/schrodinger.html +27 -0
  189. package/dist/schwarzschild.html +27 -0
  190. package/dist/shapes.html +16 -0
  191. package/dist/space.html +85 -0
  192. package/dist/spacetime.html +27 -0
  193. package/dist/sphere3d.html +24 -0
  194. package/dist/sprite.html +18 -0
  195. package/dist/starfaux.html +22 -0
  196. package/dist/study001.html +23 -0
  197. package/dist/study002.html +23 -0
  198. package/dist/study003.html +23 -0
  199. package/dist/study004.html +23 -0
  200. package/dist/study005.html +22 -0
  201. package/dist/study006.html +24 -0
  202. package/dist/study007.html +24 -0
  203. package/dist/study008.html +22 -0
  204. package/dist/svgtween.html +29 -0
  205. package/dist/tde.html +28 -0
  206. package/dist/tetris3d.html +25 -0
  207. package/dist/thomas.html +27 -0
  208. package/dist/tiles.html +28 -0
  209. package/dist/transforms.html +400 -0
  210. package/dist/tween.html +45 -0
  211. package/dist/visibility.html +33 -0
  212. package/package.json +1 -1
  213. package/readme.md +30 -22
  214. package/src/game/objects/go.js +7 -0
  215. package/src/game/objects/index.js +2 -0
  216. package/src/game/objects/isometric-scene.js +53 -3
  217. package/src/game/objects/layoutscene.js +57 -0
  218. package/src/game/objects/mask.js +241 -0
  219. package/src/game/objects/scene.js +19 -0
  220. package/src/game/objects/wrapper.js +14 -2
  221. package/src/game/pipeline.js +17 -0
  222. package/src/game/ui/button.js +101 -16
  223. package/src/game/ui/theme.js +0 -6
  224. package/src/game/ui/togglebutton.js +25 -14
  225. package/src/game/ui/tooltip.js +12 -4
  226. package/src/index.js +3 -0
  227. package/src/io/gesture.js +409 -0
  228. package/src/io/index.js +4 -1
  229. package/src/io/keys.js +9 -1
  230. package/src/io/screen.js +476 -0
  231. package/src/math/attractors.js +664 -0
  232. package/src/math/heat.js +106 -0
  233. package/src/math/index.js +1 -0
  234. package/src/mixins/draggable.js +15 -19
  235. package/src/painter/painter.shapes.js +11 -5
  236. package/src/particle/particle-system.js +165 -1
  237. package/src/physics/index.js +26 -0
  238. package/src/physics/physics-updaters.js +333 -0
  239. package/src/physics/physics.js +375 -0
  240. package/src/shapes/image.js +5 -5
  241. package/src/shapes/index.js +2 -0
  242. package/src/shapes/parallelogram.js +147 -0
  243. package/src/shapes/righttriangle.js +115 -0
  244. package/src/shapes/svg.js +281 -100
  245. package/src/shapes/text.js +22 -6
  246. package/src/shapes/transformable.js +5 -0
  247. package/src/sound/effects.js +807 -0
  248. package/src/sound/index.js +13 -0
  249. package/src/webgl/index.js +7 -0
  250. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  251. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  252. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  253. package/src/webgl/webgl-clifford-renderer.js +477 -0
  254. package/src/webgl/webgl-dejong-renderer.js +472 -0
  255. package/src/webgl/webgl-line-renderer.js +391 -0
  256. package/src/webgl/webgl-particle-renderer.js +410 -0
  257. package/types/index.d.ts +30 -2
  258. package/types/io.d.ts +217 -0
  259. package/types/physics.d.ts +299 -0
  260. package/types/shapes.d.ts +8 -0
  261. package/types/webgl.d.ts +188 -109
@@ -0,0 +1,706 @@
1
+ /**
2
+ * Schrodinger Wave Packet - Math & Physics Demo
3
+ *
4
+ * 3D visualization of a Gaussian wave packet showing the complex
5
+ * wave function as a helix/spiral traveling through space.
6
+ *
7
+ * Ψ(x,t) = A * e^(-(x-vt)²/4a²) * e^(i(kx-ωt))
8
+ */
9
+
10
+ import { Game, Painter, Camera3D, Text, applyAnchor, Position, Scene, verticalLayout, applyLayout, Gesture, Screen } from "/gcanvas.es.min.js";
11
+ import { gaussianWavePacket } from "/gcanvas.es.min.js";
12
+
13
+ // Configuration
14
+ const CONFIG = {
15
+ // Wave packet parameters
16
+ amplitude: 1.0,
17
+ sigma: 0.8, // Width of Gaussian envelope
18
+ k: 8.0, // Wave number (controls oscillation frequency)
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
+
22
+ // Visualization
23
+ numPoints: 300, // Points along the wave
24
+ xRange: 12, // Total x range (-xRange/2 to +xRange/2)
25
+ helixRadius: 80, // Radius of the helix (Re/Im amplitude)
26
+ zScale: 40, // Scale for z-axis (position)
27
+
28
+ // 3D view
29
+ rotationX: 0.3, // Tilt angle
30
+ rotationY: -0.4, // Side rotation
31
+ perspective: 800, // Perspective depth
32
+
33
+ // Grid
34
+ gridSize: 16, // Grid lines count
35
+ gridSpacing: 30, // Spacing between grid lines
36
+ gridY: 120, // Y offset for grid plane
37
+
38
+ // Animation
39
+ timeScale: 1.0,
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
+
52
+ // Colors
53
+ waveColor: [80, 160, 255], // Cyan-blue for the helix
54
+ waveGlow: [150, 200, 255], // Brighter for the center
55
+ gridColor: "rgba(60, 80, 120, 0.4)",
56
+ axisColor: "rgba(100, 150, 200, 0.6)",
57
+ envelopeColor: "rgba(255, 100, 100, 0.5)", // Red envelope line
58
+ };
59
+
60
+ class SchrodingerDemo extends Game {
61
+ constructor(canvas) {
62
+ super(canvas);
63
+ this.backgroundColor = "#000000";
64
+ this.enableFluidSize();
65
+ }
66
+
67
+ init() {
68
+ super.init();
69
+ this.time = 0;
70
+
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
81
+ this.camera = new Camera3D({
82
+ rotationX: CONFIG.rotationX,
83
+ rotationY: CONFIG.rotationY,
84
+ perspective: CONFIG.perspective,
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
89
+ });
90
+
91
+ // Enable mouse/touch rotation
92
+ this.camera.enableMouseControl(this.canvas);
93
+
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
106
+ this.canvas.addEventListener("dblclick", () => {
107
+ this.time = 0;
108
+ this.targetZoom = this.defaultZoom;
109
+ });
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
+
124
+ // Create info panel container anchored to top center
125
+ this.infoPanel = new Scene(this, { x: 0, y: 0 });
126
+ applyAnchor(this.infoPanel, {
127
+ anchor: Position.TOP_CENTER,
128
+ anchorOffsetY: 150,
129
+ });
130
+ this.pipeline.add(this.infoPanel);
131
+
132
+ // Create all text items
133
+ const { amplitude, sigma, k, omega, velocity } = CONFIG;
134
+
135
+ this.titleText = new Text(this, "Gaussian Wave Packet", {
136
+ font: "bold 16px monospace",
137
+ color: "#7af",
138
+ align: "center",
139
+ baseline: "middle",
140
+ });
141
+
142
+ this.equationText = new Text(this, "\u03A8(x,t) = A\u00B7e^(-(x-vt)\u00B2/4\u03C3\u00B2) \u00B7 e^(i(kx-\u03C9t))", {
143
+ font: "14px monospace",
144
+ color: "#fff",
145
+ align: "center",
146
+ baseline: "middle",
147
+ });
148
+
149
+ this.paramsText = new Text(this, `A=${amplitude} \u03C3=${sigma} k=${k} \u03C9=${omega} v=${velocity}`, {
150
+ font: "12px monospace",
151
+ color: "#6d8",
152
+ align: "center",
153
+ baseline: "middle",
154
+ });
155
+
156
+ this.liveText = new Text(this, "t=0.00s x\u2080=0.00", {
157
+ font: "12px monospace",
158
+ color: "#fa6",
159
+ align: "center",
160
+ baseline: "middle",
161
+ });
162
+
163
+ // Use vertical layout to position items
164
+ const textItems = [this.titleText, this.equationText, this.paramsText, this.liveText];
165
+ const layout = verticalLayout(textItems, { spacing: 20, align: "center" });
166
+ applyLayout(textItems, layout.positions);
167
+
168
+ // Add all to panel
169
+ textItems.forEach(item => this.infoPanel.add(item));
170
+ }
171
+
172
+ /**
173
+ * Compute Gaussian wave packet using quantum.js module.
174
+ * Ψ(x,t) = A * e^(-(x-vt)²/4σ²) * e^(i(kx-ωt))
175
+ */
176
+ computeWavePacket(x, t) {
177
+ return gaussianWavePacket(x, t, {
178
+ amplitude: CONFIG.amplitude,
179
+ sigma: CONFIG.sigma,
180
+ k: CONFIG.k,
181
+ omega: CONFIG.omega,
182
+ velocity: CONFIG.velocity,
183
+ });
184
+ }
185
+
186
+ /**
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
310
+ */
311
+ project3D(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
+ };
319
+ }
320
+
321
+ update(dt) {
322
+ super.update(dt);
323
+ this.time += dt * CONFIG.timeScale;
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
+
338
+ // Loop when wave packet exits the visible range
339
+ const packetCenter = CONFIG.velocity * this.time;
340
+ if (packetCenter > CONFIG.xRange * 0.6) {
341
+ this.time = -CONFIG.xRange * 0.6 / CONFIG.velocity;
342
+ }
343
+
344
+ // Update live text values
345
+ if (this.liveText) {
346
+ const t = this.time.toFixed(2);
347
+ const x0 = (CONFIG.velocity * this.time).toFixed(2);
348
+ this.liveText.text = `t=${t}s x\u2080=${x0}`;
349
+ }
350
+ }
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
+
360
+ render() {
361
+ const w = this.width;
362
+ const h = this.height;
363
+ const cx = w / 2;
364
+ const cy = h / 2 - 30;
365
+
366
+ super.render();
367
+
368
+ // Compute wave packet points
369
+ const points = [];
370
+ const { numPoints, xRange, helixRadius, zScale } = CONFIG;
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
+
378
+ for (let i = 0; i < numPoints; i++) {
379
+ const t_param = i / (numPoints - 1);
380
+ const x = (t_param - 0.5) * xRange;
381
+
382
+ const { psi, envelope } = this.computeWavePacket(x, this.time);
383
+
384
+ // 3D coordinates: helix in Re/Im plane, extending along 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
+ }
395
+
396
+ const projected = this.project3D(px, py, pz);
397
+
398
+ points.push({
399
+ x: cx + projected.x,
400
+ y: cy + projected.y,
401
+ z: projected.z,
402
+ scale: projected.scale,
403
+ envelope,
404
+ psi,
405
+ worldX: x,
406
+ });
407
+ }
408
+
409
+ // Draw grid plane
410
+ this.drawGrid(cx, cy);
411
+
412
+ // Draw axis line (position axis)
413
+ this.drawAxis(cx, cy);
414
+
415
+ // Draw envelope curves (Gaussian bell on grid)
416
+ this.drawEnvelope(cx, cy, points);
417
+
418
+ // Sort points by depth for proper rendering
419
+ const sortedIndices = points
420
+ .map((p, i) => ({ z: p.z, i }))
421
+ .sort((a, b) => a.z - b.z)
422
+ .map(item => item.i);
423
+
424
+ // Draw the helix wave
425
+ this.drawHelix(points, sortedIndices);
426
+
427
+ // Draw projection on grid (2D wave)
428
+ this.drawProjection(cx, cy, points);
429
+
430
+ // Draw collapse indicator if collapsed
431
+ if (this.isCollapsed) {
432
+ this.drawCollapseIndicator(cx, cy);
433
+ }
434
+
435
+ // Info text
436
+ this.drawInfo(w, h);
437
+ }
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
+
517
+ drawGrid(cx, cy) {
518
+ const { gridSize, gridSpacing, gridY } = CONFIG;
519
+ const halfGrid = (gridSize * gridSpacing) / 2;
520
+
521
+ // Draw grid lines
522
+ for (let i = -gridSize / 2; i <= gridSize / 2; i++) {
523
+ // Lines along Z
524
+ const x1 = i * gridSpacing;
525
+ const z1 = -halfGrid;
526
+ const z2 = halfGrid;
527
+
528
+ const p1 = this.project3D(x1, gridY, z1);
529
+ const p2 = this.project3D(x1, gridY, z2);
530
+
531
+ Painter.useCtx((ctx) => {
532
+ ctx.strokeStyle = CONFIG.gridColor;
533
+ ctx.lineWidth = 1;
534
+ ctx.moveTo(cx + p1.x, cy + p1.y);
535
+ ctx.lineTo(cx + p2.x, cy + p2.y);
536
+ ctx.stroke();
537
+ });
538
+
539
+ // Lines along X
540
+ const p3 = this.project3D(-halfGrid, gridY, i * gridSpacing);
541
+ const p4 = this.project3D(halfGrid, gridY, i * gridSpacing);
542
+
543
+ Painter.useCtx((ctx) => {
544
+ ctx.strokeStyle = CONFIG.gridColor;
545
+ ctx.lineWidth = 1;
546
+ ctx.moveTo(cx + p3.x, cy + p3.y);
547
+ ctx.lineTo(cx + p4.x, cy + p4.y);
548
+ ctx.stroke();
549
+ });
550
+ }
551
+ }
552
+
553
+ drawAxis(cx, cy) {
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
559
+
560
+ // Main position axis (Z direction in 3D space)
561
+ const p1 = this.project3D(0, 0, -xRange / 2 * zScale * 1.2);
562
+ const p2 = this.project3D(0, 0, xRange / 2 * zScale * 1.2);
563
+
564
+ // Glowing axis line (fades with collapse)
565
+ Painter.useCtx((ctx) => {
566
+ ctx.strokeStyle = `rgba(100, 150, 200, ${0.6 * fade})`;
567
+ ctx.lineWidth = 2;
568
+ ctx.moveTo(cx + p1.x, cy + p1.y);
569
+ ctx.lineTo(cx + p2.x, cy + p2.y);
570
+ ctx.stroke();
571
+ });
572
+
573
+ // Bright center dot where wave packet is (fades with collapse)
574
+ const packetCenter = CONFIG.velocity * this.time;
575
+ const centerProj = this.project3D(0, 0, packetCenter * zScale);
576
+
577
+ Painter.useCtx((ctx) => {
578
+ ctx.fillStyle = `rgba(255, 255, 255, ${fade})`;
579
+ ctx.shadowColor = "#88ccff";
580
+ ctx.shadowBlur = 20 * fade;
581
+ ctx.arc(cx + centerProj.x, cy + centerProj.y, 4, 0, Math.PI * 2);
582
+ ctx.fill();
583
+ ctx.shadowBlur = 0;
584
+ });
585
+ }
586
+
587
+ drawEnvelope(cx, cy, points) {
588
+ const { gridY } = CONFIG;
589
+
590
+ // Precompute envelope path points
591
+ const pathPoints = points.map(p => {
592
+ const envHeight = p.envelope * CONFIG.helixRadius * 0.8;
593
+ return this.project3D(0, gridY - envHeight, p.worldX * CONFIG.zScale);
594
+ });
595
+
596
+ // Draw Gaussian envelope on the grid plane
597
+ Painter.useCtx((ctx) => {
598
+ ctx.strokeStyle = CONFIG.envelopeColor;
599
+ ctx.lineWidth = 2;
600
+
601
+ for (let i = 0; i < pathPoints.length; i++) {
602
+ const proj = pathPoints[i];
603
+ if (i === 0) {
604
+ ctx.moveTo(cx + proj.x, cy + proj.y);
605
+ } else {
606
+ ctx.lineTo(cx + proj.x, cy + proj.y);
607
+ }
608
+ }
609
+ ctx.stroke();
610
+ });
611
+ }
612
+
613
+ drawHelix(points, sortedIndices) {
614
+ const { waveColor, waveGlow } = CONFIG;
615
+
616
+ // Draw helix as connected line segments with varying thickness
617
+ for (let j = 0; j < sortedIndices.length - 1; j++) {
618
+ const i = sortedIndices[j];
619
+ const p1 = points[i];
620
+ const p2 = points[i + 1] || points[i];
621
+
622
+ if (Math.abs(i - (sortedIndices[j + 1] || i)) > 2) continue;
623
+
624
+ // Thickness based on envelope (thicker where amplitude is higher)
625
+ const thickness = 1 + p1.envelope * 6;
626
+
627
+ // Color intensity based on envelope
628
+ const intensity = 0.3 + p1.envelope * 0.7;
629
+
630
+ const r = Math.floor(waveColor[0] + (waveGlow[0] - waveColor[0]) * p1.envelope);
631
+ const g = Math.floor(waveColor[1] + (waveGlow[1] - waveColor[1]) * p1.envelope);
632
+ const b = Math.floor(waveColor[2] + (waveGlow[2] - waveColor[2]) * p1.envelope);
633
+
634
+ Painter.useCtx((ctx) => {
635
+ ctx.lineCap = "round";
636
+ ctx.lineJoin = "round";
637
+ ctx.strokeStyle = `rgba(${r},${g},${b},${intensity})`;
638
+ ctx.lineWidth = thickness * p1.scale;
639
+ ctx.moveTo(p1.x, p1.y);
640
+ ctx.lineTo(p2.x, p2.y);
641
+ ctx.stroke();
642
+ });
643
+ }
644
+
645
+ // Add glow effect at high-amplitude regions
646
+ for (const p of points) {
647
+ if (p.envelope > 0.5) {
648
+ const size = p.envelope * 4 * p.scale;
649
+ Painter.useCtx((ctx) => {
650
+ ctx.shadowColor = "rgba(100, 180, 255, 0.8)";
651
+ ctx.shadowBlur = 15;
652
+ ctx.fillStyle = `rgba(200, 230, 255, ${p.envelope * 0.5})`;
653
+ ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
654
+ ctx.fill();
655
+ ctx.shadowBlur = 0;
656
+ });
657
+ }
658
+ }
659
+ }
660
+
661
+ drawProjection(cx, cy, points) {
662
+ const { gridY, zScale } = CONFIG;
663
+
664
+ // Precompute projection path points
665
+ const pathPoints = points.map(p => {
666
+ const waveHeight = p.psi.real * CONFIG.helixRadius * 0.5;
667
+ return this.project3D(waveHeight, gridY, p.worldX * zScale);
668
+ });
669
+
670
+ // Draw 2D wave projection on grid (Re(Ψ) only)
671
+ Painter.useCtx((ctx) => {
672
+ ctx.strokeStyle = "rgba(80, 160, 255, 0.6)";
673
+ ctx.lineWidth = 1.5;
674
+
675
+ for (let i = 0; i < pathPoints.length; i++) {
676
+ const proj = pathPoints[i];
677
+ if (i === 0) {
678
+ ctx.moveTo(cx + proj.x, cy + proj.y);
679
+ } else {
680
+ ctx.lineTo(cx + proj.x, cy + proj.y);
681
+ }
682
+ }
683
+ ctx.stroke();
684
+ });
685
+ }
686
+
687
+ drawInfo(w, h) {
688
+ Painter.useCtx((ctx) => {
689
+ // Controls hint (bottom right)
690
+ ctx.fillStyle = "#445";
691
+ ctx.font = "10px monospace";
692
+ ctx.textAlign = "right";
693
+ ctx.fillText("drag to rotate | scroll to zoom | hold to collapse | double-click to reset", w - 15, h - 30);
694
+
695
+ // Legend
696
+ ctx.fillText("Helix = \u03A8 | Blue = Re(\u03A8) | Red = |\u03A8|\u00B2", w - 15, h - 15);
697
+ ctx.textAlign = "left";
698
+ });
699
+ }
700
+ }
701
+
702
+ window.addEventListener("load", () => {
703
+ const canvas = document.getElementById("game");
704
+ const demo = new SchrodingerDemo(canvas);
705
+ demo.start();
706
+ });