@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
+ * Lorenz Attractor 3D Visualization
3
+ *
4
+ * The classic "butterfly effect" attractor discovered by Edward Lorenz (1963)
5
+ * while studying atmospheric convection. Particles follow the chaotic
6
+ * trajectories colored by velocity (blue=slow, red=fast).
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.lorenz for equations)
22
+ attractor: {
23
+ dt: 0.005, // Integration time step
24
+ scale: 12, // Scale factor for display (Lorenz is larger)
25
+ },
26
+
27
+ // Particle settings
28
+ particles: {
29
+ count: 400,
30
+ trailLength: 250,
31
+ spawnRange: 2, // Initial position range around origin
32
+ },
33
+
34
+ // Center offset - Lorenz attractor orbits around z≈27 (ρ-1)
35
+ center: {
36
+ x: 5,
37
+ y: 0,
38
+ z: 27,
39
+ },
40
+
41
+ // Camera settings - angled to show butterfly shape
42
+ camera: {
43
+ perspective: 800,
44
+ rotationX: -2, // Tilt to see butterfly spread
45
+ rotationY: -3, // Rotated to face the wings
46
+ inertia: true,
47
+ friction: 0.95,
48
+ clampX: false,
49
+ },
50
+
51
+ // Visual settings
52
+ visual: {
53
+ minHue: 30, // Orange-red (fast)
54
+ maxHue: 200, // Cyan-blue (slow)
55
+ maxSpeed: 50, // Speed normalization threshold
56
+ saturation: 85,
57
+ lightness: 55,
58
+ maxAlpha: 0.85,
59
+ hueShiftSpeed: 15, // Degrees per second (0 to disable)
60
+ },
61
+
62
+ // Glitch/blink effect
63
+ blink: {
64
+ chance: 0.015,
65
+ minDuration: 0.05,
66
+ maxDuration: 0.25,
67
+ intensityBoost: 1.4,
68
+ saturationBoost: 1.15,
69
+ alphaBoost: 1.25,
70
+ },
71
+
72
+ // Zoom settings
73
+ zoom: {
74
+ min: 0.2,
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
+ /**
111
+ * @param {Function} stepFn - Attractor step function
112
+ * @param {number} spawnRange - Initial position range
113
+ */
114
+ constructor(stepFn, spawnRange) {
115
+ this.stepFn = stepFn;
116
+ this.position = {
117
+ x: (Math.random() - 0.5) * spawnRange,
118
+ y: (Math.random() - 0.5) * spawnRange,
119
+ z: (Math.random() - 0.5) * spawnRange + CONFIG.center.z, // Start near attractor
120
+ };
121
+ this.trail = [];
122
+ this.speed = 0;
123
+
124
+ // Blink/glitch state
125
+ this.blinkTime = 0;
126
+ this.blinkIntensity = 0;
127
+ }
128
+
129
+ /**
130
+ * Update blink state
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
+ /**
154
+ * Update particle position using attractor
155
+ */
156
+ update(dt, scale) {
157
+ // Use the attractor step function
158
+ const result = this.stepFn(this.position, dt);
159
+
160
+ // Update position
161
+ this.position = result.position;
162
+ this.speed = result.speed;
163
+
164
+ // Add to trail (scaled and centered for display)
165
+ // Subtract center offset so attractor rotates around its center
166
+ this.trail.unshift({
167
+ x: (this.position.x - CONFIG.center.x) * scale,
168
+ y: (this.position.y - CONFIG.center.y) * scale,
169
+ z: (this.position.z - CONFIG.center.z) * scale,
170
+ speed: this.speed,
171
+ });
172
+
173
+ // Trim trail
174
+ if (this.trail.length > CONFIG.particles.trailLength) {
175
+ this.trail.pop();
176
+ }
177
+ }
178
+ }
179
+
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+ // DEMO CLASS
182
+ // ─────────────────────────────────────────────────────────────────────────────
183
+
184
+ /**
185
+ * Lorenz Attractor Demo
186
+ */
187
+ class LorenzDemo extends Game {
188
+ constructor(canvas) {
189
+ super(canvas);
190
+ this.backgroundColor = "#000";
191
+ this.enableFluidSize();
192
+ }
193
+
194
+ init() {
195
+ super.init();
196
+
197
+ // Get attractor info for display
198
+ this.attractor = Attractors.lorenz;
199
+ console.log(`Attractor: ${this.attractor.name}`);
200
+ console.log(`Equations:`, this.attractor.equations);
201
+
202
+ // Create stepper function with default params
203
+ this.stepFn = this.attractor.createStepper();
204
+
205
+ // Calculate initial zoom
206
+ const { min, max, baseScreenSize } = CONFIG.zoom;
207
+ const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
208
+ this.zoom = initialZoom;
209
+ this.targetZoom = initialZoom;
210
+ this.defaultZoom = initialZoom;
211
+
212
+ // Camera with mouse control
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
+ // Gesture handler for zoom
224
+ this.gesture = new Gesture(this.canvas, {
225
+ onZoom: (delta) => {
226
+ this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
227
+ },
228
+ onPan: null,
229
+ });
230
+
231
+ // Double-click to reset
232
+ this.canvas.addEventListener("dblclick", () => {
233
+ this.targetZoom = this.defaultZoom;
234
+ });
235
+
236
+ // Log camera params on mouse release (for finding good starting angle)
237
+ this.canvas.addEventListener("mouseup", () => {
238
+ console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
239
+ });
240
+
241
+ // Initialize particles using the attractor step function
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 * (maxHue - minHue);
320
+ const hue = (baseHue + hueOffset) % 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 * (maxHue - minHue);
371
+ const hue = (baseHue + hueOffset) % 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 LorenzDemo(canvas);
424
+ demo.start();
425
+ });
@@ -0,0 +1,285 @@
1
+ import {
2
+ Button,
3
+ Game,
4
+ GridLayout,
5
+ HorizontalLayout,
6
+ Rectangle,
7
+ Scene,
8
+ ToggleButton,
9
+ VerticalLayout,
10
+ TextShape,
11
+ Position,
12
+ ShapeGOFactory,
13
+ Easing,
14
+ Tweenetik,
15
+ } from "/gcanvas.es.min.js";
16
+
17
+ /**
18
+ * MondrianDemo - Interactive grid-based composition
19
+ * Inspired by the works of Piet Mondrian
20
+ */
21
+ export class MondrianDemo extends Game {
22
+ constructor(canvas) {
23
+ super(canvas);
24
+ this.enableFluidSize();
25
+ }
26
+
27
+ init() {
28
+ super.init();
29
+
30
+ // Setup
31
+ this.backgroundColor = "#ffffff";
32
+ this.gridScene = new Scene(this, { debugColor: "black" });
33
+
34
+ // Configure grid scene
35
+ const margin = 20;
36
+ this.gridScene.width = this.width - margin * 2;
37
+ this.gridScene.height = this.height - margin * 2;
38
+ this.gridScene.x = margin;
39
+ this.gridScene.y = margin;
40
+
41
+ // Generate initial composition
42
+ this.generateMondrianRectangles(
43
+ this.gridScene.width,
44
+ this.gridScene.height
45
+ );
46
+
47
+ // Add to pipeline
48
+ this.pipeline.add(this.gridScene);
49
+
50
+ // Handle click events
51
+ this.events.on("click", () => this.animateExplosion());
52
+ }
53
+
54
+ animateExplosion() {
55
+ const centerX = this.width / 2;
56
+ const centerY = this.height / 2;
57
+ const maxDimension = Math.max(this.width, this.height);
58
+ const children = this.gridScene._collection.getSortedChildren();
59
+
60
+ children.forEach((rect) => {
61
+ // Calculate angle and distance from center
62
+ const angle = Math.atan2(rect.y - centerY, rect.x - centerX);
63
+ const distanceFromCenter = Math.sqrt(
64
+ Math.pow(rect.x - centerX, 2) + Math.pow(rect.y - centerY, 2)
65
+ );
66
+ const normalizedDistance = Math.min(
67
+ 1,
68
+ distanceFromCenter / (Math.min(this.width, this.height) / 2)
69
+ );
70
+
71
+ // Calculate animation parameters
72
+ const flyDistance = maxDimension * 1.5;
73
+ const destX = centerX + Math.cos(angle) * flyDistance;
74
+ const destY = centerY + Math.sin(angle) * flyDistance;
75
+ const delay = (1 - normalizedDistance) * 0.5;
76
+ const rotation = (Math.random() * 2 - 1) * Math.PI * 4;
77
+
78
+ // Animate rectangle flying away
79
+ Tweenetik.to(
80
+ rect,
81
+ {
82
+ opacity: 0,
83
+ x: destX,
84
+ y: destY,
85
+ scaleX: 0.2,
86
+ scaleY: 0.2,
87
+ rotation: rotation,
88
+ },
89
+ 0.5 + normalizedDistance * 0.5,
90
+ Easing.easeInSine,
91
+ {
92
+ delay: delay,
93
+ onComplete: () => {
94
+ this.generateMondrianRectangles(
95
+ this.gridScene.width,
96
+ this.gridScene.height
97
+ );
98
+ },
99
+ }
100
+ );
101
+ });
102
+ }
103
+
104
+ generateMondrianRectangles(totalWidth, totalHeight, options = {}) {
105
+ // Clear existing rectangles
106
+ this.gridScene.clear();
107
+
108
+ // Default options
109
+ const {
110
+ lineWidth = 8,
111
+ step = totalHeight / 6,
112
+ splitProbability = 0.5,
113
+ } = options;
114
+
115
+ // Colors
116
+ const white = "#F2F5F1";
117
+ const colors = ["#D40920", "#1356A2", "#F7D842", "#999999"];
118
+
119
+ // Start with one rectangle covering the entire area
120
+ let squares = [
121
+ {
122
+ x: 0,
123
+ y: 0,
124
+ width: totalWidth,
125
+ height: totalHeight,
126
+ },
127
+ ];
128
+
129
+ // Generate split points based on grid
130
+ const splitPoints = Array.from(
131
+ { length: Math.ceil(Math.max(totalWidth, totalHeight) / step) },
132
+ (_, i) => i * step
133
+ );
134
+
135
+ // Split function
136
+ const splitSquaresAt = (coord) => {
137
+ const { x, y } = coord;
138
+
139
+ for (let i = squares.length - 1; i >= 0; i--) {
140
+ const square = squares[i];
141
+
142
+ // Split on x-coordinate
143
+ if (
144
+ x &&
145
+ x > square.x &&
146
+ x < square.x + square.width &&
147
+ Math.random() < splitProbability
148
+ ) {
149
+ squares.splice(i, 1);
150
+
151
+ squares.push(
152
+ {
153
+ x: square.x,
154
+ y: square.y,
155
+ width: x - square.x,
156
+ height: square.height,
157
+ },
158
+ {
159
+ x: x,
160
+ y: square.y,
161
+ width: square.width - (x - square.x),
162
+ height: square.height,
163
+ }
164
+ );
165
+ }
166
+
167
+ // Split on y-coordinate
168
+ if (
169
+ y &&
170
+ y > square.y &&
171
+ y < square.y + square.height &&
172
+ Math.random() < splitProbability
173
+ ) {
174
+ squares.splice(i, 1);
175
+
176
+ squares.push(
177
+ {
178
+ x: square.x,
179
+ y: square.y,
180
+ width: square.width,
181
+ height: y - square.y,
182
+ },
183
+ {
184
+ x: square.x,
185
+ y: y,
186
+ width: square.width,
187
+ height: square.height - (y - square.y),
188
+ }
189
+ );
190
+ }
191
+ }
192
+ };
193
+
194
+ // Apply splits
195
+ splitPoints.forEach((point) => {
196
+ splitSquaresAt({ y: point });
197
+ splitSquaresAt({ x: point });
198
+ });
199
+
200
+ // Assign colors to some squares
201
+ for (let i = 0; i < colors.length * 3; i++) {
202
+ const randomIndex = Math.floor(Math.random() * squares.length);
203
+ squares[randomIndex].color =
204
+ Math.random() < 0.8 ? colors[i % colors.length] : "black";
205
+ }
206
+
207
+ // Calculate center for animations
208
+ const centerX = this.width / 2;
209
+ const centerY = this.height / 2;
210
+ const maxDimension = Math.max(this.width, this.height);
211
+
212
+ // Create and animate rectangles
213
+ squares.forEach((square, i, allSquares) => {
214
+ // Create rectangle
215
+ const rect = new Rectangle({
216
+ width: square.width - lineWidth,
217
+ height: square.height - lineWidth,
218
+ color: square.color || white,
219
+ stroke: "#000000",
220
+ lineWidth: lineWidth,
221
+ crisp: false,
222
+ });
223
+
224
+ // Calculate positions and animation parameters
225
+ const finalX = square.x + square.width / 2;
226
+ const finalY = square.y + square.height / 2;
227
+ const angle = Math.atan2(finalY - centerY, finalX - centerX);
228
+ const flyDistance = maxDimension * 1.5;
229
+ const startX = centerX + Math.cos(angle) * flyDistance;
230
+ const startY = centerY + Math.sin(angle) * flyDistance;
231
+
232
+ // Create game object
233
+ const go = ShapeGOFactory.create(this, rect, {
234
+ x: startX,
235
+ y: startY,
236
+ scaleX: 0.0,
237
+ scaleY: 0.0,
238
+ crisp: false,
239
+ rotation: (Math.random() * 2 - 1) * Math.PI,
240
+ });
241
+
242
+ this.gridScene.add(go);
243
+
244
+ // Calculate animation timing
245
+ const distance = Math.sqrt(
246
+ Math.pow(finalX - centerX, 2) + Math.pow(finalY - centerY, 2)
247
+ );
248
+ const normalizedDistance = Math.min(
249
+ 1,
250
+ distance / (Math.min(this.width, this.height) / 2)
251
+ );
252
+
253
+ const middle = allSquares.length / 2;
254
+ const distanceFromMiddle = Math.abs(i - middle);
255
+ const delay = (distanceFromMiddle / middle) * 0.1;
256
+
257
+ // Animate rectangle flying in
258
+ Tweenetik.to(
259
+ go,
260
+ {
261
+ x: finalX,
262
+ y: finalY,
263
+ scaleX: 1,
264
+ scaleY: 1,
265
+ rotation: 0,
266
+ },
267
+ 0.5 + normalizedDistance * 0.5,
268
+ Easing.easeOutCirc,
269
+ { delay: delay }
270
+ );
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Update function - called each frame
276
+ */
277
+ update(dt) {
278
+ if (this.boundsDirty) {
279
+ this.gridScene.width = this.width - 40;
280
+ this.gridScene.height = this.height - 40;
281
+ }
282
+
283
+ super.update(dt);
284
+ }
285
+ }