@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,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
+ });