@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,480 @@
1
+ /**
2
+ * Rössler Attractor 3D Visualization
3
+ *
4
+ * Discovered by Otto Rössler (1976). One of the simplest chaotic attractors,
5
+ * featuring a single spiral that folds back on itself - simpler than Lorenz
6
+ * but equally chaotic.
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.rossler for equations)
22
+ attractor: {
23
+ dt: 0.05, // Integration time step
24
+ scale: 15, // Scale factor for display
25
+ },
26
+
27
+ // Particle settings
28
+ particles: {
29
+ count: 400,
30
+ trailLength: 250,
31
+ spawnRange: 4, // Moderate range near origin
32
+ },
33
+
34
+ // Center offset - Rossler spirals in x-y, spikes in z
35
+ // No axis swap: x→horizontal, y→vertical, z→depth
36
+ center: {
37
+ x: 0,
38
+ y: 5,
39
+ z: 5, // Center the z-spike in depth
40
+ },
41
+
42
+ // Camera settings
43
+ camera: {
44
+ perspective: 500,
45
+ rotationX: 0.3, // Slight tilt
46
+ rotationY: 0,
47
+ inertia: true,
48
+ friction: 0.95,
49
+ clampX: false,
50
+ },
51
+
52
+ // Visual settings - warm orange/yellow palette
53
+ visual: {
54
+ minHue: 40, // Yellow-orange (fast)
55
+ maxHue: 280, // Purple (slow)
56
+ maxSpeed: 20,
57
+ saturation: 85,
58
+ lightness: 55,
59
+ maxAlpha: 0.85,
60
+ hueShiftSpeed: 10,
61
+ },
62
+
63
+ // Glitch/blink effect
64
+ blink: {
65
+ chance: 0.015,
66
+ minDuration: 0.05,
67
+ maxDuration: 0.2,
68
+ intensityBoost: 1.4,
69
+ saturationBoost: 1.15,
70
+ alphaBoost: 1.25,
71
+ },
72
+
73
+ // Zoom settings
74
+ zoom: {
75
+ min: 0.2,
76
+ max: 2.5,
77
+ speed: 0.5,
78
+ easing: 0.12,
79
+ baseScreenSize: 600,
80
+ },
81
+ };
82
+
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+ // HELPER FUNCTIONS
85
+ // ─────────────────────────────────────────────────────────────────────────────
86
+
87
+ function hslToRgb(h, s, l) {
88
+ s /= 100;
89
+ l /= 100;
90
+ const k = (n) => (n + h / 30) % 12;
91
+ const a = s * Math.min(l, 1 - l);
92
+ const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
93
+ return {
94
+ r: Math.round(255 * f(0)),
95
+ g: Math.round(255 * f(8)),
96
+ b: Math.round(255 * f(4)),
97
+ };
98
+ }
99
+
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+ // ATTRACTOR PARTICLE
102
+ // ─────────────────────────────────────────────────────────────────────────────
103
+
104
+ class AttractorParticle {
105
+ constructor(attractor, spawnRange, warmupSteps = 0) {
106
+ // Each particle gets slightly different parameters to prevent sync
107
+ const variation = 0.02; // 2% variation
108
+ this.stepFn = attractor.createStepper({
109
+ a: 0.2 * (1 + (Math.random() - 0.5) * variation),
110
+ b: 0.2 * (1 + (Math.random() - 0.5) * variation),
111
+ c: 5.7 * (1 + (Math.random() - 0.5) * variation),
112
+ });
113
+
114
+ this.position = {
115
+ x: (Math.random() - 0.5) * spawnRange,
116
+ y: (Math.random() - 0.5) * spawnRange,
117
+ z: (Math.random() - 0.5) * spawnRange,
118
+ };
119
+ this.trail = [];
120
+ this.speed = 0;
121
+ this.blinkTime = 0;
122
+ this.blinkIntensity = 0;
123
+
124
+ // Warmup: run particle for random steps to spread them across the attractor cycle
125
+ const steps = Math.floor(Math.random() * warmupSteps);
126
+ for (let i = 0; i < steps; i++) {
127
+ const result = this.stepFn(this.position, CONFIG.attractor.dt);
128
+ this.position = result.position;
129
+ }
130
+ }
131
+
132
+ updateBlink(dt) {
133
+ const { chance, minDuration, maxDuration } = CONFIG.blink;
134
+
135
+ if (this.blinkTime > 0) {
136
+ this.blinkTime -= dt;
137
+ this.blinkIntensity = Math.max(
138
+ 0,
139
+ this.blinkTime > 0
140
+ ? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
141
+ : 0
142
+ );
143
+ } else {
144
+ if (Math.random() < chance) {
145
+ this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
146
+ this.blinkIntensity = 1;
147
+ } else {
148
+ this.blinkIntensity = 0;
149
+ }
150
+ }
151
+ }
152
+
153
+ update(dt, scale, axisConfig, spawnRange) {
154
+ const result = this.stepFn(this.position, dt);
155
+ this.position = result.position;
156
+ this.speed = result.speed;
157
+
158
+ // Small chance to respawn at random position (keeps transient "thickness")
159
+ if (Math.random() < 0.003) {
160
+ this.position = {
161
+ x: (Math.random() - 0.5) * spawnRange,
162
+ y: (Math.random() - 0.5) * spawnRange,
163
+ z: (Math.random() - 0.5) * spawnRange,
164
+ };
165
+ this.trail = [];
166
+ }
167
+
168
+ const px = this.position.x - CONFIG.center.x;
169
+ const py = this.position.y - CONFIG.center.y;
170
+ const pz = this.position.z - CONFIG.center.z;
171
+
172
+ // Use configurable axis mapping
173
+ const coords = { x: px, y: py, z: pz };
174
+ this.trail.unshift({
175
+ x: coords[axisConfig.x] * scale * axisConfig.sx,
176
+ y: coords[axisConfig.y] * scale * axisConfig.sy,
177
+ z: coords[axisConfig.z] * scale * axisConfig.sz,
178
+ speed: this.speed,
179
+ });
180
+
181
+ if (this.trail.length > CONFIG.particles.trailLength) {
182
+ this.trail.pop();
183
+ }
184
+ }
185
+ }
186
+
187
+ // ─────────────────────────────────────────────────────────────────────────────
188
+ // DEMO CLASS
189
+ // ─────────────────────────────────────────────────────────────────────────────
190
+
191
+ class RosslerDemo extends Game {
192
+ constructor(canvas) {
193
+ super(canvas);
194
+ this.backgroundColor = "#000";
195
+ this.enableFluidSize();
196
+ }
197
+
198
+ init() {
199
+ super.init();
200
+
201
+ this.attractor = Attractors.rossler;
202
+ console.log(`Attractor: ${this.attractor.name}`);
203
+ console.log(`Equations:`, this.attractor.equations);
204
+
205
+ this.stepFn = this.attractor.createStepper();
206
+
207
+ const { min, max, baseScreenSize } = CONFIG.zoom;
208
+ const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
209
+ this.zoom = initialZoom;
210
+ this.targetZoom = initialZoom;
211
+ this.defaultZoom = initialZoom;
212
+
213
+ this.camera = new Camera3D({
214
+ perspective: CONFIG.camera.perspective,
215
+ rotationX: CONFIG.camera.rotationX,
216
+ rotationY: CONFIG.camera.rotationY,
217
+ inertia: CONFIG.camera.inertia,
218
+ friction: CONFIG.camera.friction,
219
+ clampX: CONFIG.camera.clampX,
220
+ });
221
+ this.camera.enableMouseControl(this.canvas);
222
+
223
+ this.gesture = new Gesture(this.canvas, {
224
+ onZoom: (delta) => {
225
+ this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
226
+ },
227
+ onPan: null,
228
+ });
229
+
230
+ this.canvas.addEventListener("dblclick", () => {
231
+ this.targetZoom = this.defaultZoom;
232
+ });
233
+
234
+ // Log camera params and barycenter on mouse release
235
+ this.canvas.addEventListener("mouseup", () => {
236
+ console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
237
+ let sumX = 0, sumY = 0, sumZ = 0, count = 0;
238
+ for (const p of this.particles) {
239
+ sumX += p.position.x;
240
+ sumY += p.position.y;
241
+ sumZ += p.position.z;
242
+ count++;
243
+ }
244
+ console.log(`Barycenter: x: ${(sumX/count).toFixed(3)}, y: ${(sumY/count).toFixed(3)}, z: ${(sumZ/count).toFixed(3)}`);
245
+ });
246
+
247
+ this.particles = [];
248
+ const warmupSteps = 2000; // Spread particles across attractor cycle
249
+ for (let i = 0; i < CONFIG.particles.count; i++) {
250
+ this.particles.push(
251
+ new AttractorParticle(this.attractor, CONFIG.particles.spawnRange, warmupSteps)
252
+ );
253
+ }
254
+
255
+ // All axis configurations to try (with sign variations)
256
+ this.axisConfigs = [
257
+ { x: 'x', y: 'y', z: 'z', sx: 1, sy: 1, sz: 1, name: 'XYZ +++' },
258
+ { x: 'x', y: 'y', z: 'z', sx: 1, sy: -1, sz: 1, name: 'XYZ +-+' },
259
+ { x: 'x', y: 'z', z: 'y', sx: 1, sy: 1, sz: 1, name: 'XZY +++' },
260
+ { x: 'x', y: 'z', z: 'y', sx: 1, sy: -1, sz: 1, name: 'XZY +-+' },
261
+ { x: 'y', y: 'x', z: 'z', sx: 1, sy: 1, sz: 1, name: 'YXZ +++' },
262
+ { x: 'y', y: 'x', z: 'z', sx: 1, sy: -1, sz: 1, name: 'YXZ +-+' },
263
+ { x: 'y', y: 'z', z: 'x', sx: 1, sy: 1, sz: 1, name: 'YZX +++' },
264
+ { x: 'y', y: 'z', z: 'x', sx: 1, sy: -1, sz: 1, name: 'YZX +-+' },
265
+ { x: 'z', y: 'x', z: 'y', sx: 1, sy: 1, sz: 1, name: 'ZXY +++' },
266
+ { x: 'z', y: 'x', z: 'y', sx: 1, sy: -1, sz: 1, name: 'ZXY +-+' },
267
+ { x: 'z', y: 'y', z: 'x', sx: 1, sy: 1, sz: 1, name: 'ZYX +++' },
268
+ { x: 'z', y: 'y', z: 'x', sx: 1, sy: -1, sz: 1, name: 'ZYX +-+' },
269
+ ];
270
+ this.axisIndex = 3; // XZY +-+ (config 3)
271
+ this.axisConfig = this.axisConfigs[this.axisIndex];
272
+
273
+ // Click to cycle through axis configurations (disabled - uncomment to test)
274
+ /*
275
+ this.canvas.addEventListener("click", () => {
276
+ this.axisIndex = (this.axisIndex + 1) % this.axisConfigs.length;
277
+ this.axisConfig = this.axisConfigs[this.axisIndex];
278
+ // Clear trails when switching
279
+ for (const p of this.particles) {
280
+ p.trail = [];
281
+ }
282
+ console.log(`=== Config ${this.axisIndex + 1}/${this.axisConfigs.length}: ${this.axisConfig.name} ===`);
283
+ console.log(` trailX = pos.${this.axisConfig.x} * ${this.axisConfig.sx}`);
284
+ console.log(` trailY = pos.${this.axisConfig.y} * ${this.axisConfig.sy}`);
285
+ console.log(` trailZ = pos.${this.axisConfig.z} * ${this.axisConfig.sz}`);
286
+ console.log(` Camera: rotX=${this.camera.rotationX.toFixed(3)}, rotY=${this.camera.rotationY.toFixed(3)}`);
287
+ });
288
+ */
289
+
290
+ console.log(`Axis config: ${this.axisConfig.name}`);
291
+
292
+ const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
293
+ this.lineRenderer = new WebGLLineRenderer(maxSegments, {
294
+ width: this.width,
295
+ height: this.height,
296
+ blendMode: "additive",
297
+ });
298
+
299
+ this.segments = [];
300
+
301
+ if (!this.lineRenderer.isAvailable()) {
302
+ console.warn("WebGL not available, falling back to Canvas 2D");
303
+ this.useWebGL = false;
304
+ } else {
305
+ this.useWebGL = true;
306
+ console.log(`WebGL enabled, ${maxSegments} max segments`);
307
+ }
308
+
309
+ this.time = 0;
310
+ }
311
+
312
+ onResize() {
313
+ if (this.lineRenderer?.isAvailable()) {
314
+ this.lineRenderer.resize(this.width, this.height);
315
+ }
316
+ const { min, max, baseScreenSize } = CONFIG.zoom;
317
+ this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
318
+ }
319
+
320
+ update(dt) {
321
+ super.update(dt);
322
+ this.camera.update(dt);
323
+ this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
324
+ this.time += dt;
325
+
326
+ for (const particle of this.particles) {
327
+ particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale, this.axisConfig, CONFIG.particles.spawnRange);
328
+ particle.updateBlink(dt);
329
+ }
330
+
331
+ // Debug: log position ranges every 2 seconds
332
+ this.debugTimer = (this.debugTimer || 0) + dt;
333
+ if (this.debugTimer > 2) {
334
+ this.debugTimer = 0;
335
+ let minX = Infinity, maxX = -Infinity;
336
+ let minY = Infinity, maxY = -Infinity;
337
+ let minZ = Infinity, maxZ = -Infinity;
338
+ for (const p of this.particles) {
339
+ minX = Math.min(minX, p.position.x);
340
+ maxX = Math.max(maxX, p.position.x);
341
+ minY = Math.min(minY, p.position.y);
342
+ maxY = Math.max(maxY, p.position.y);
343
+ minZ = Math.min(minZ, p.position.z);
344
+ maxZ = Math.max(maxZ, p.position.z);
345
+ }
346
+ console.log(`Position ranges - X: [${minX.toFixed(1)}, ${maxX.toFixed(1)}], Y: [${minY.toFixed(1)}, ${maxY.toFixed(1)}], Z: [${minZ.toFixed(1)}, ${maxZ.toFixed(1)}]`);
347
+ }
348
+ }
349
+
350
+ collectSegments(cx, cy) {
351
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
352
+ CONFIG.visual;
353
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
354
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
355
+
356
+ this.segments.length = 0;
357
+
358
+ for (const particle of this.particles) {
359
+ if (particle.trail.length < 2) continue;
360
+
361
+ const blink = particle.blinkIntensity;
362
+
363
+ for (let i = 1; i < particle.trail.length; i++) {
364
+ const curr = particle.trail[i];
365
+ const prev = particle.trail[i - 1];
366
+
367
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
368
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
369
+
370
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
371
+
372
+ const age = i / particle.trail.length;
373
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
374
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
375
+ const hue = (baseHue + hueOffset + 360) % 360;
376
+
377
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
378
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
379
+ const rgb = hslToRgb(hue, sat, lit);
380
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
381
+
382
+ this.segments.push({
383
+ x1: cx + p1.x * this.zoom,
384
+ y1: cy + p1.y * this.zoom,
385
+ x2: cx + p2.x * this.zoom,
386
+ y2: cy + p2.y * this.zoom,
387
+ r: rgb.r,
388
+ g: rgb.g,
389
+ b: rgb.b,
390
+ a: alpha,
391
+ });
392
+ }
393
+ }
394
+
395
+ return this.segments.length;
396
+ }
397
+
398
+ renderCanvas2D(cx, cy) {
399
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
400
+ CONFIG.visual;
401
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
402
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
403
+
404
+ const ctx = this.ctx;
405
+ ctx.save();
406
+ ctx.globalCompositeOperation = "lighter";
407
+ ctx.lineCap = "round";
408
+
409
+ for (const particle of this.particles) {
410
+ if (particle.trail.length < 2) continue;
411
+
412
+ const blink = particle.blinkIntensity;
413
+
414
+ for (let i = 1; i < particle.trail.length; i++) {
415
+ const curr = particle.trail[i];
416
+ const prev = particle.trail[i - 1];
417
+
418
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
419
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
420
+
421
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
422
+
423
+ const age = i / particle.trail.length;
424
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
425
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
426
+ const hue = (baseHue + hueOffset + 360) % 360;
427
+
428
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
429
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
430
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
431
+
432
+ ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
433
+ ctx.lineWidth = 1;
434
+
435
+ ctx.beginPath();
436
+ ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
437
+ ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
438
+ ctx.stroke();
439
+ }
440
+ }
441
+
442
+ ctx.restore();
443
+ }
444
+
445
+ render() {
446
+ super.render();
447
+ if (!this.particles) return;
448
+
449
+ const cx = this.width / 2;
450
+ const cy = this.height / 2;
451
+
452
+ if (this.useWebGL && this.lineRenderer.isAvailable()) {
453
+ const segmentCount = this.collectSegments(cx, cy);
454
+ if (segmentCount > 0) {
455
+ this.lineRenderer.clear();
456
+ this.lineRenderer.updateLines(this.segments);
457
+ this.lineRenderer.render(segmentCount);
458
+ this.lineRenderer.compositeOnto(this.ctx, 0, 0);
459
+ }
460
+ } else {
461
+ this.renderCanvas2D(cx, cy);
462
+ }
463
+ }
464
+
465
+ destroy() {
466
+ this.gesture?.destroy();
467
+ this.lineRenderer?.destroy();
468
+ super.destroy?.();
469
+ }
470
+ }
471
+
472
+ // ─────────────────────────────────────────────────────────────────────────────
473
+ // INITIALIZATION
474
+ // ─────────────────────────────────────────────────────────────────────────────
475
+
476
+ window.addEventListener("load", () => {
477
+ const canvas = document.getElementById("game");
478
+ const demo = new RosslerDemo(canvas);
479
+ demo.start();
480
+ });