@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,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Halvorsen Attractor 3D</title>
7
+ <link rel="stylesheet" href="demos.css" />
8
+ <script src="./js/info-toggle.js"></script>
9
+ </head>
10
+ <body>
11
+ <div id="info">
12
+ <strong>Halvorsen Attractor</strong><br/>
13
+ <span style="color:#CCC">
14
+ <li>dx/dt = -ax - 4y - 4z - y²</li>
15
+ <li>dy/dt = -ay - 4z - 4x - z²</li>
16
+ <li>dz/dt = -az - 4x - 4y - x²</li>
17
+ <li>a = 1.89</li>
18
+ <li>Three-fold rotational symmetry</li>
19
+ <li>Blue = slow, Pink = fast</li>
20
+ <li>Drag to rotate</li>
21
+ <li>Scroll/pinch to zoom</li>
22
+ </span>
23
+ </div>
24
+ <canvas id="game"></canvas>
25
+ <script type="module" src="./js/halvorsen.js"></script>
26
+ </body>
27
+ </html>
package/dist/index.html CHANGED
@@ -6,58 +6,91 @@
6
6
 
7
7
  <!-- Primary Meta Tags -->
8
8
  <title>GCanvas Demos - Interactive Examples & Documentation</title>
9
- <meta name="title" content="GCanvas Demos - Interactive Examples & Documentation">
10
- <meta name="description" content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action.">
11
- <meta name="keywords" content="gcanvas, canvas demos, 2d canvas examples, game framework demos, javascript canvas, html5 canvas examples, interactive demos, creative coding examples">
12
- <meta name="author" content="GCanvas">
13
- <meta name="robots" content="index, follow">
14
- <link rel="canonical" href="https://gcanvas.guinetik.com/">
9
+ <meta
10
+ name="title"
11
+ content="GCanvas Demos - Interactive Examples & Documentation"
12
+ />
13
+ <meta
14
+ name="description"
15
+ content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action."
16
+ />
17
+ <meta
18
+ name="keywords"
19
+ content="gcanvas, canvas demos, 2d canvas examples, game framework demos, javascript canvas, html5 canvas examples, interactive demos, creative coding examples"
20
+ />
21
+ <meta name="author" content="GCanvas" />
22
+ <meta name="robots" content="index, follow" />
23
+ <link rel="canonical" href="https://gcanvas.guinetik.com/" />
15
24
 
16
25
  <!-- Open Graph / Facebook -->
17
- <meta property="og:type" content="website">
18
- <meta property="og:url" content="https://gcanvas.guinetik.com/">
19
- <meta property="og:title" content="GCanvas Demos - Interactive Examples & Documentation">
20
- <meta property="og:description" content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action.">
21
- <meta property="og:image" content="https://gcanvas.guinetik.com/og_image.png">
22
- <meta property="og:image:width" content="1200">
23
- <meta property="og:image:height" content="630">
24
- <meta property="og:image:alt" content="GCanvas interactive demos and examples">
25
- <meta property="og:site_name" content="GCanvas">
26
- <meta property="og:locale" content="en_US">
26
+ <meta property="og:type" content="website" />
27
+ <meta property="og:url" content="https://gcanvas.guinetik.com/" />
28
+ <meta
29
+ property="og:title"
30
+ content="GCanvas Demos - Interactive Examples & Documentation"
31
+ />
32
+ <meta
33
+ property="og:description"
34
+ content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action."
35
+ />
36
+ <meta
37
+ property="og:image"
38
+ content="https://gcanvas.guinetik.com/og_image.png"
39
+ />
40
+ <meta property="og:image:width" content="1200" />
41
+ <meta property="og:image:height" content="630" />
42
+ <meta
43
+ property="og:image:alt"
44
+ content="GCanvas interactive demos and examples"
45
+ />
46
+ <meta property="og:site_name" content="GCanvas" />
47
+ <meta property="og:locale" content="en_US" />
27
48
 
28
49
  <!-- Twitter -->
29
- <meta name="twitter:card" content="summary_large_image">
30
- <meta name="twitter:url" content="https://gcanvas.guinetik.com/">
31
- <meta name="twitter:title" content="GCanvas Demos - Interactive Examples & Documentation">
32
- <meta name="twitter:description" content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action.">
33
- <meta name="twitter:image" content="https://gcanvas.guinetik.com/og_image.png">
34
- <meta name="twitter:image:alt" content="GCanvas interactive demos and examples">
35
- <meta name="twitter:creator" content="@guinetik">
50
+ <meta name="twitter:card" content="summary_large_image" />
51
+ <meta name="twitter:url" content="https://gcanvas.guinetik.com/" />
52
+ <meta
53
+ name="twitter:title"
54
+ content="GCanvas Demos - Interactive Examples & Documentation"
55
+ />
56
+ <meta
57
+ name="twitter:description"
58
+ content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action."
59
+ />
60
+ <meta
61
+ name="twitter:image"
62
+ content="https://gcanvas.guinetik.com/og_image.png"
63
+ />
64
+ <meta
65
+ name="twitter:image:alt"
66
+ content="GCanvas interactive demos and examples"
67
+ />
68
+ <meta name="twitter:creator" content="@guinetik" />
36
69
 
37
70
  <!-- Favicon -->
38
- <link rel="icon" type="image/svg+xml" href="./logo.svg">
39
- <link rel="apple-touch-icon" href="./logo.svg">
40
- <meta name="theme-color" content="#000000">
71
+ <link rel="icon" type="image/svg+xml" href="./logo.svg" />
72
+ <link rel="apple-touch-icon" href="./logo.svg" />
73
+ <meta name="theme-color" content="#000000" />
41
74
 
42
75
  <!-- Structured Data (JSON-LD) -->
43
76
  <script type="application/ld+json">
44
- {
45
- "@context": "https://schema.org",
46
- "@type": "WebSite",
47
- "name": "GCanvas Demos",
48
- "url": "https://gcanvas.guinetik.com/",
49
- "description": "Interactive demos and examples for GCanvas, a modular 2D canvas rendering and game framework",
50
- "publisher": {
51
- "@type": "Organization",
52
- "name": "GCanvas",
53
- "url": "https://github.com/guinetik/gcanvas"
54
- },
55
- "potentialAction": {
56
- "@type": "SearchAction",
57
- "target": "https://gcanvas.guinetik.com/?q={search_term_string}",
58
- "query-input": "required name=search_term_string"
59
- }
60
- }
77
+ {
78
+ "@context": "https://schema.org",
79
+ "@type": "WebSite",
80
+ "name": "GCanvas Demos",
81
+ "url": "https://gcanvas.guinetik.com/",
82
+ "description": "Interactive demos and examples for GCanvas, a modular 2D canvas rendering and game framework",
83
+ "publisher": {
84
+ "@type": "Organization",
85
+ "name": "GCanvas",
86
+ "url": "https://github.com/guinetik/gcanvas"
87
+ },
88
+ "potentialAction": {
89
+ "@type": "SearchAction",
90
+ "target": "https://gcanvas.guinetik.com/?q={search_term_string}",
91
+ "query-input": "required name=search_term_string"
92
+ }
93
+ }
61
94
  </script>
62
95
 
63
96
  <link rel="stylesheet" href="demos.css" />
@@ -135,7 +168,7 @@
135
168
  title="View on GitHub"
136
169
  style="
137
170
  display: inline-block;
138
- background: #1F1F1F;
171
+ background: #1f1f1f;
139
172
  border-radius: 50%;
140
173
  width: 36px;
141
174
  height: 36px;
@@ -213,16 +246,22 @@
213
246
  <a href="tiles.html" target="demo-frame">Tile Layout</a>
214
247
  <a href="isometric.html" target="demo-frame">Isometric</a>
215
248
  <a href="scenes.html" target="demo-frame">Scene Transforms</a>
249
+ <a href="scene-interactivity-test.html" target="demo-frame"
250
+ >Scene Interactivity</a
251
+ >
216
252
  <hr />
217
253
  <h2 style="margin-bottom: 0.3em">3D</h2>
218
254
  <a href="sphere3d.html" target="demo-frame">Sphere3D Showcase</a>
219
255
  <a href="plane3d.html" target="demo-frame">Plane3D Showcase</a>
220
256
  <a href="cube3d.html" target="demo-frame">Rubik's Cube</a>
221
257
  <hr />
222
- <h2 style="margin-bottom: 0.3em; color: #0f0;">
223
- <a href="../genuary26/" style="color: #0f0; text-decoration: none;">
224
- Genuary 2026 &rarr;
225
- </a>
258
+ <h2 style="margin-bottom: 0.3em; color: #0f0">
259
+ <a
260
+ href="../genuary26/"
261
+ style="color: #0f0; text-decoration: none"
262
+ >
263
+ Genuary 2026 &rarr;
264
+ </a>
226
265
  </h2>
227
266
  <hr />
228
267
  <h2 style="margin-bottom: 0.3em">Generative Art</h2>
@@ -247,6 +286,7 @@
247
286
  <a href="baskara.html" target="demo-frame">Root Dance</a>
248
287
  <hr />
249
288
  <h2 style="margin-bottom: 0.3em">Math & Physics</h2>
289
+ <a href="cmb.html" target="demo-frame">Cosmic Microwave Background</a>
250
290
  <a href="fluid.html" target="demo-frame">Fluid Playground</a>
251
291
  <a href="fluid-simple.html" target="demo-frame">Fluid System</a>
252
292
  <a href="schrodinger.html" target="demo-frame">Schrodinger Wave</a>
@@ -257,6 +297,14 @@
257
297
  <a href="kerr.html" target="demo-frame">Kerr Metric</a>
258
298
  <a href="blackhole.html" target="demo-frame">Black Hole</a>
259
299
  <a href="tde.html" target="demo-frame">Tidal Disruption</a>
300
+ <a href="dadras.html" target="demo-frame">Dadras Attractor</a>
301
+ <a href="lorenz.html" target="demo-frame">Lorenz Attractor</a>
302
+ <a href="aizawa.html" target="demo-frame">Aizawa Attractor</a>
303
+ <a href="thomas.html" target="demo-frame">Thomas Attractor</a>
304
+ <a href="rossler.html" target="demo-frame">Rössler Attractor</a>
305
+ <a href="halvorsen.html" target="demo-frame">Halvorsen Attractor</a>
306
+ <a href="clifford.html" target="demo-frame">Clifford Attractor</a>
307
+ <a href="dejong.html" target="demo-frame">De Jong Attractor</a>
260
308
  <hr />
261
309
  <h2 style="margin-bottom: 0.3em">Games</h2>
262
310
  <a href="blob.html" target="demo-frame">Bezier Blob</a>
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Aizawa Attractor 3D Visualization
3
+ *
4
+ * A 3D chaotic system with intricate folding structure, named after
5
+ * Japanese mathematician Tomohiko Aizawa. Features complex orbits
6
+ * with a distinctive torus-like shape.
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.aizawa for equations)
22
+ attractor: {
23
+ dt: 0.008, // Integration time step
24
+ scale: 120, // Scale factor for display (Aizawa is small)
25
+ },
26
+
27
+ // Particle settings
28
+ particles: {
29
+ count: 350,
30
+ trailLength: 220,
31
+ spawnRange: 0.5, // Initial position range around origin
32
+ },
33
+
34
+ // Center offset - adjust to match attractor's visual barycenter
35
+ center: {
36
+ x: 0,
37
+ y: 0,
38
+ z: 0.5, // Aizawa orbits around z ≈ 0.5
39
+ },
40
+
41
+ // Camera settings
42
+ camera: {
43
+ perspective: 800,
44
+ rotationX: 0.4,
45
+ rotationY: 0,
46
+ inertia: true,
47
+ friction: 0.95,
48
+ clampX: false,
49
+ },
50
+
51
+ // Visual settings
52
+ visual: {
53
+ minHue: 280, // Magenta (fast)
54
+ maxHue: 180, // Cyan (slow)
55
+ maxSpeed: 8, // Speed normalization threshold
56
+ saturation: 90,
57
+ lightness: 55,
58
+ maxAlpha: 0.85,
59
+ hueShiftSpeed: 12, // Degrees per second
60
+ },
61
+
62
+ // Glitch/blink effect
63
+ blink: {
64
+ chance: 0.018,
65
+ minDuration: 0.04,
66
+ maxDuration: 0.2,
67
+ intensityBoost: 1.5,
68
+ saturationBoost: 1.2,
69
+ alphaBoost: 1.3,
70
+ },
71
+
72
+ // Zoom settings
73
+ zoom: {
74
+ min: 0.3,
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
+ constructor(stepFn, spawnRange) {
111
+ this.stepFn = stepFn;
112
+ this.position = {
113
+ x: (Math.random() - 0.5) * spawnRange,
114
+ y: (Math.random() - 0.5) * spawnRange,
115
+ z: (Math.random() - 0.5) * spawnRange,
116
+ };
117
+ this.trail = [];
118
+ this.speed = 0;
119
+
120
+ // Blink/glitch state
121
+ this.blinkTime = 0;
122
+ this.blinkIntensity = 0;
123
+ }
124
+
125
+ updateBlink(dt) {
126
+ const { chance, minDuration, maxDuration } = CONFIG.blink;
127
+
128
+ if (this.blinkTime > 0) {
129
+ this.blinkTime -= dt;
130
+ this.blinkIntensity = Math.max(
131
+ 0,
132
+ this.blinkTime > 0
133
+ ? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
134
+ : 0
135
+ );
136
+ } else {
137
+ if (Math.random() < chance) {
138
+ this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
139
+ this.blinkIntensity = 1;
140
+ } else {
141
+ this.blinkIntensity = 0;
142
+ }
143
+ }
144
+ }
145
+
146
+ update(dt, scale) {
147
+ const result = this.stepFn(this.position, dt);
148
+
149
+ this.position = result.position;
150
+ this.speed = result.speed;
151
+
152
+ // Add to trail (reoriented and scaled for display)
153
+ // Swap Y and Z so the attractor's vertical axis aligns with screen vertical
154
+ this.trail.unshift({
155
+ x: this.position.x * scale,
156
+ y: this.position.z * scale, // Z becomes screen Y (vertical)
157
+ z: this.position.y * scale, // Y becomes depth
158
+ speed: this.speed,
159
+ });
160
+
161
+ if (this.trail.length > CONFIG.particles.trailLength) {
162
+ this.trail.pop();
163
+ }
164
+ }
165
+ }
166
+
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+ // DEMO CLASS
169
+ // ─────────────────────────────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Aizawa Attractor Demo
173
+ */
174
+ class AizawaDemo extends Game {
175
+ constructor(canvas) {
176
+ super(canvas);
177
+ this.backgroundColor = "#000";
178
+ this.enableFluidSize();
179
+ }
180
+
181
+ init() {
182
+ super.init();
183
+
184
+ // Get attractor info
185
+ this.attractor = Attractors.aizawa;
186
+ console.log(`Attractor: ${this.attractor.name}`);
187
+ console.log(`Equations:`, this.attractor.equations);
188
+
189
+ // Create stepper function
190
+ this.stepFn = this.attractor.createStepper();
191
+
192
+ // Calculate initial zoom
193
+ const { min, max, baseScreenSize } = CONFIG.zoom;
194
+ const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
195
+ this.zoom = initialZoom;
196
+ this.targetZoom = initialZoom;
197
+ this.defaultZoom = initialZoom;
198
+
199
+ // Camera with mouse control
200
+ this.camera = new Camera3D({
201
+ perspective: CONFIG.camera.perspective,
202
+ rotationX: CONFIG.camera.rotationX,
203
+ rotationY: CONFIG.camera.rotationY,
204
+ inertia: CONFIG.camera.inertia,
205
+ friction: CONFIG.camera.friction,
206
+ clampX: CONFIG.camera.clampX,
207
+ });
208
+ this.camera.enableMouseControl(this.canvas, {
209
+ invertX: true, // flip left/right
210
+ invertY: true, // flip up/down
211
+ });
212
+
213
+ // Gesture handler for zoom
214
+ this.gesture = new Gesture(this.canvas, {
215
+ onZoom: (delta) => {
216
+ this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
217
+ },
218
+ onPan: null,
219
+ });
220
+
221
+ // Double-click to reset
222
+ this.canvas.addEventListener("dblclick", () => {
223
+ this.targetZoom = this.defaultZoom;
224
+ });
225
+
226
+ // Log camera params on mouse release (for finding good starting angle)
227
+ this.canvas.addEventListener("mouseup", () => {
228
+ console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
229
+
230
+ // Calculate and log barycenter to help find the right center offset
231
+ let sumX = 0, sumY = 0, sumZ = 0, count = 0;
232
+ for (const p of this.particles) {
233
+ sumX += p.position.x;
234
+ sumY += p.position.y;
235
+ sumZ += p.position.z;
236
+ count++;
237
+ }
238
+ console.log(`Barycenter: x: ${(sumX/count).toFixed(3)}, y: ${(sumY/count).toFixed(3)}, z: ${(sumZ/count).toFixed(3)}`);
239
+ });
240
+
241
+ // Initialize particles
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 * (minHue - maxHue);
320
+ const hue = (baseHue + hueOffset + 360) % 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 * (minHue - maxHue);
371
+ const hue = (baseHue + hueOffset + 360) % 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 AizawaDemo(canvas);
424
+ demo.start();
425
+ });