@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,405 @@
1
+ /**
2
+ * Dadras Attractor 3D Visualization
3
+ *
4
+ * A 3D chaotic attractor visualization where particles follow the Dadras
5
+ * dynamical system equations. Trails are colored by velocity (blue=slow,
6
+ * red=fast) with additive blending for a glowing effect.
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.dadras for equations)
22
+ attractor: {
23
+ dt: 0.01, // Integration time step
24
+ scale: 50, // Scale factor for display
25
+ },
26
+
27
+ // Particle settings
28
+ particles: {
29
+ count: 500,
30
+ trailLength: 200,
31
+ spawnRange: 5, // Initial position range around origin
32
+ },
33
+
34
+ // Camera settings
35
+ camera: {
36
+ perspective: 800,
37
+ rotationX: 0.3,
38
+ rotationY: 0,
39
+ inertia: true,
40
+ friction: 0.95,
41
+ clampX: false,
42
+ },
43
+
44
+ // Visual settings
45
+ visual: {
46
+ minHue: 60, // Red (fast)
47
+ maxHue: 240, // Blue (slow)
48
+ maxSpeed: 30, // Speed normalization threshold
49
+ saturation: 80,
50
+ lightness: 50,
51
+ maxAlpha: 0.9,
52
+ hueShiftSpeed: 20, // Degrees per second (0 to disable)
53
+ },
54
+
55
+ // Glitch/blink effect
56
+ blink: {
57
+ chance: 0.02,
58
+ minDuration: 0.05,
59
+ maxDuration: 0.3,
60
+ intensityBoost: 1.5,
61
+ saturationBoost: 1.2,
62
+ alphaBoost: 1.3,
63
+ },
64
+
65
+ // Zoom settings
66
+ zoom: {
67
+ min: 0.3,
68
+ max: 3.0,
69
+ speed: 0.5,
70
+ easing: 0.12,
71
+ baseScreenSize: 600,
72
+ },
73
+ };
74
+
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+ // HELPER FUNCTIONS
77
+ // ─────────────────────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Convert HSL to RGB
81
+ */
82
+ function hslToRgb(h, s, l) {
83
+ s /= 100;
84
+ l /= 100;
85
+ const k = (n) => (n + h / 30) % 12;
86
+ const a = s * Math.min(l, 1 - l);
87
+ const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
88
+ return {
89
+ r: Math.round(255 * f(0)),
90
+ g: Math.round(255 * f(8)),
91
+ b: Math.round(255 * f(4)),
92
+ };
93
+ }
94
+
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // ATTRACTOR PARTICLE
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * A particle following attractor dynamics
101
+ */
102
+ class AttractorParticle {
103
+ /**
104
+ * @param {Function} stepFn - Attractor step function
105
+ * @param {number} spawnRange - Initial position range
106
+ */
107
+ constructor(stepFn, spawnRange) {
108
+ this.stepFn = stepFn;
109
+ this.position = {
110
+ x: (Math.random() - 0.5) * spawnRange,
111
+ y: (Math.random() - 0.5) * spawnRange,
112
+ z: (Math.random() - 0.5) * spawnRange,
113
+ };
114
+ this.trail = [];
115
+ this.speed = 0;
116
+
117
+ // Blink/glitch state
118
+ this.blinkTime = 0;
119
+ this.blinkIntensity = 0;
120
+ }
121
+
122
+ /**
123
+ * Update blink state
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
+ /**
147
+ * Update particle position using attractor
148
+ */
149
+ update(dt, scale) {
150
+ // Use the attractor step function
151
+ const result = this.stepFn(this.position, dt);
152
+
153
+ // Update position
154
+ this.position = result.position;
155
+ this.speed = result.speed;
156
+
157
+ // Add to trail (scaled for display)
158
+ this.trail.unshift({
159
+ x: this.position.x * scale,
160
+ y: this.position.y * scale,
161
+ z: this.position.z * scale,
162
+ speed: this.speed,
163
+ });
164
+
165
+ // Trim trail
166
+ if (this.trail.length > CONFIG.particles.trailLength) {
167
+ this.trail.pop();
168
+ }
169
+ }
170
+ }
171
+
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+ // DEMO CLASS
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Dadras Attractor Demo
178
+ */
179
+ class DadrasDemo extends Game {
180
+ constructor(canvas) {
181
+ super(canvas);
182
+ this.backgroundColor = "#000";
183
+ this.enableFluidSize();
184
+ }
185
+
186
+ init() {
187
+ super.init();
188
+
189
+ // Get attractor info for display
190
+ this.attractor = Attractors.dadras;
191
+ console.log(`Attractor: ${this.attractor.name}`);
192
+ console.log(`Equations:`, this.attractor.equations);
193
+
194
+ // Create stepper function with default params
195
+ this.stepFn = this.attractor.createStepper();
196
+
197
+ // Calculate initial zoom
198
+ const { min, max, baseScreenSize } = CONFIG.zoom;
199
+ const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
200
+ this.zoom = initialZoom;
201
+ this.targetZoom = initialZoom;
202
+ this.defaultZoom = initialZoom;
203
+
204
+ // Camera with mouse control
205
+ this.camera = new Camera3D({
206
+ perspective: CONFIG.camera.perspective,
207
+ rotationX: CONFIG.camera.rotationX,
208
+ rotationY: CONFIG.camera.rotationY,
209
+ inertia: CONFIG.camera.inertia,
210
+ friction: CONFIG.camera.friction,
211
+ clampX: CONFIG.camera.clampX,
212
+ });
213
+ this.camera.enableMouseControl(this.canvas);
214
+
215
+ // Gesture handler for zoom
216
+ this.gesture = new Gesture(this.canvas, {
217
+ onZoom: (delta) => {
218
+ this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
219
+ },
220
+ onPan: null,
221
+ });
222
+
223
+ // Double-click to reset
224
+ this.canvas.addEventListener("dblclick", () => {
225
+ this.targetZoom = this.defaultZoom;
226
+ });
227
+
228
+ // Initialize particles using the attractor step function
229
+ this.particles = [];
230
+ for (let i = 0; i < CONFIG.particles.count; i++) {
231
+ this.particles.push(new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange));
232
+ }
233
+
234
+ // WebGL line renderer
235
+ const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
236
+ this.lineRenderer = new WebGLLineRenderer(maxSegments, {
237
+ width: this.width,
238
+ height: this.height,
239
+ blendMode: "additive",
240
+ });
241
+
242
+ this.segments = [];
243
+
244
+ if (!this.lineRenderer.isAvailable()) {
245
+ console.warn("WebGL not available, falling back to Canvas 2D");
246
+ this.useWebGL = false;
247
+ } else {
248
+ this.useWebGL = true;
249
+ console.log(`WebGL enabled, ${maxSegments} max segments`);
250
+ }
251
+
252
+ this.time = 0;
253
+ }
254
+
255
+ onResize() {
256
+ if (this.lineRenderer?.isAvailable()) {
257
+ this.lineRenderer.resize(this.width, this.height);
258
+ }
259
+ const { min, max, baseScreenSize } = CONFIG.zoom;
260
+ this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
261
+ }
262
+
263
+ update(dt) {
264
+ super.update(dt);
265
+ this.camera.update(dt);
266
+ this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
267
+ this.time += dt;
268
+
269
+ for (const particle of this.particles) {
270
+ particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale);
271
+ particle.updateBlink(dt);
272
+ }
273
+ }
274
+
275
+ collectSegments(cx, cy) {
276
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
277
+ CONFIG.visual;
278
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
279
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
280
+
281
+ this.segments.length = 0;
282
+
283
+ for (const particle of this.particles) {
284
+ if (particle.trail.length < 2) continue;
285
+
286
+ const blink = particle.blinkIntensity;
287
+
288
+ for (let i = 1; i < particle.trail.length; i++) {
289
+ const curr = particle.trail[i];
290
+ const prev = particle.trail[i - 1];
291
+
292
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
293
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
294
+
295
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
296
+
297
+ const age = i / particle.trail.length;
298
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
299
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
300
+ const hue = (baseHue + hueOffset) % 360;
301
+
302
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
303
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
304
+ const rgb = hslToRgb(hue, sat, lit);
305
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
306
+
307
+ this.segments.push({
308
+ x1: cx + p1.x * this.zoom,
309
+ y1: cy + p1.y * this.zoom,
310
+ x2: cx + p2.x * this.zoom,
311
+ y2: cy + p2.y * this.zoom,
312
+ r: rgb.r,
313
+ g: rgb.g,
314
+ b: rgb.b,
315
+ a: alpha,
316
+ });
317
+ }
318
+ }
319
+
320
+ return this.segments.length;
321
+ }
322
+
323
+ renderCanvas2D(cx, cy) {
324
+ const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
325
+ CONFIG.visual;
326
+ const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
327
+ const hueOffset = (this.time * hueShiftSpeed) % 360;
328
+
329
+ const ctx = this.ctx;
330
+ ctx.save();
331
+ ctx.globalCompositeOperation = "lighter";
332
+ ctx.lineCap = "round";
333
+
334
+ for (const particle of this.particles) {
335
+ if (particle.trail.length < 2) continue;
336
+
337
+ const blink = particle.blinkIntensity;
338
+
339
+ for (let i = 1; i < particle.trail.length; i++) {
340
+ const curr = particle.trail[i];
341
+ const prev = particle.trail[i - 1];
342
+
343
+ const p1 = this.camera.project(prev.x, prev.y, prev.z);
344
+ const p2 = this.camera.project(curr.x, curr.y, curr.z);
345
+
346
+ if (p1.scale <= 0 || p2.scale <= 0) continue;
347
+
348
+ const age = i / particle.trail.length;
349
+ const speedNorm = Math.min(curr.speed / maxSpeed, 1);
350
+ const baseHue = maxHue - speedNorm * (maxHue - minHue);
351
+ const hue = (baseHue + hueOffset) % 360;
352
+
353
+ const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
354
+ const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
355
+ const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
356
+
357
+ ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
358
+ ctx.lineWidth = 1;
359
+
360
+ ctx.beginPath();
361
+ ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
362
+ ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
363
+ ctx.stroke();
364
+ }
365
+ }
366
+
367
+ ctx.restore();
368
+ }
369
+
370
+ render() {
371
+ super.render();
372
+ if (!this.particles) return;
373
+
374
+ const cx = this.width / 2;
375
+ const cy = this.height / 2;
376
+
377
+ if (this.useWebGL && this.lineRenderer.isAvailable()) {
378
+ const segmentCount = this.collectSegments(cx, cy);
379
+ if (segmentCount > 0) {
380
+ this.lineRenderer.clear();
381
+ this.lineRenderer.updateLines(this.segments);
382
+ this.lineRenderer.render(segmentCount);
383
+ this.lineRenderer.compositeOnto(this.ctx, 0, 0);
384
+ }
385
+ } else {
386
+ this.renderCanvas2D(cx, cy);
387
+ }
388
+ }
389
+
390
+ destroy() {
391
+ this.gesture?.destroy();
392
+ this.lineRenderer?.destroy();
393
+ super.destroy?.();
394
+ }
395
+ }
396
+
397
+ // ─────────────────────────────────────────────────────────────────────────────
398
+ // INITIALIZATION
399
+ // ─────────────────────────────────────────────────────────────────────────────
400
+
401
+ window.addEventListener("load", () => {
402
+ const canvas = document.getElementById("game");
403
+ const demo = new DadrasDemo(canvas);
404
+ demo.start();
405
+ });
@@ -0,0 +1,257 @@
1
+ /**
2
+ * De Jong Attractor 2D Visualization
3
+ *
4
+ * A 2D iterative attractor by Peter de Jong, similar to Clifford but
5
+ * creating different swirling patterns with its sin/cos structure.
6
+ *
7
+ * Engine-aligned procedural WebGL approach:
8
+ * - Seed buffer stays on the GPU
9
+ * - Vertex shader iterates the De Jong map (like the reference project)
10
+ * - Output is composited onto the main 2D canvas for easy trail accumulation
11
+ */
12
+
13
+ import { Game, Gesture, Screen, Attractors, Painter, WebGLDeJongRenderer } from "/gcanvas.es.min.js";
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // CONFIGURATION
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ const CONFIG = {
20
+ // Attractor settings
21
+ attractor: {
22
+ // GPU shader iterations per point
23
+ iterations: 100,
24
+
25
+ // Parameter animation (mirrors the reference project where 'a' drifts over time)
26
+ params: {
27
+ aBase: -2.0,
28
+ aWobble: 0.6,
29
+ aPeriodSeconds: 8.0, // seconds for one full wobble cycle
30
+ b: -2.0,
31
+ c: -1.2,
32
+ d: 2.0,
33
+ },
34
+ },
35
+
36
+ // Procedural point settings
37
+ points: {
38
+ seedCount: 1 << 18, // 262144 (matches reference performance profile)
39
+ pointSize: 1.0,
40
+ pointScale: 0.5, // maps attractor space to clip space (reference uses 0.5)
41
+ shape: "glow", // 'circle' | 'glow' | 'square' | 'softSquare'
42
+ blendMode: "additive", // 'alpha' | 'additive' (WebGL)
43
+ compositeBlendMode: "lighter", // Canvas 2D blend for compositing
44
+ color: { r: 1, g: 1, b: 1, a: 0.12 }, // RGBA 0..1
45
+ },
46
+
47
+ // Visual settings - warm palette for De Jong
48
+ visual: {
49
+ // Lorenz-style: map speed → hue (slow=cyan, fast=orange) + hue shifting
50
+ minHue: 30, // fast
51
+ maxHue: 200, // slow
52
+ maxSpeed: 0.8, // speed normalization threshold (tune per-attractor)
53
+ saturation: 85,
54
+ lightness: 55,
55
+ alpha: 0.14,
56
+ hueShiftSpeed: 15, // degrees per second
57
+ fadeSpeed: 0.02, // 0 = no fade (infinite trails), higher = faster fade
58
+ },
59
+
60
+ // Zoom settings
61
+ zoom: {
62
+ min: 0.3,
63
+ max: 3.0,
64
+ speed: 0.5,
65
+ easing: 0.12,
66
+ baseScreenSize: 600,
67
+ initialMultiplier: 0.75, // lower = zoom out more initially
68
+ },
69
+
70
+ // Rotation settings (drag to rotate)
71
+ rotation: {
72
+ speed: 0.01, // radians per pixel
73
+ easing: 0.15,
74
+ autoSpeed: 0.18, // radians/sec (continuous rotation)
75
+ },
76
+ };
77
+
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+ // DEMO CLASS
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+
82
+ class DeJongDemo extends Game {
83
+ constructor(canvas) {
84
+ super(canvas);
85
+ this.backgroundColor = "#000";
86
+ this.enableFluidSize();
87
+ }
88
+
89
+ init() {
90
+ super.init();
91
+
92
+ this.attractor = Attractors.deJong;
93
+ console.log(`Attractor: ${this.attractor.name}`);
94
+ console.log(`Equations:`, this.attractor.equations);
95
+
96
+ const { min, max, baseScreenSize } = CONFIG.zoom;
97
+ const initialZoomRaw = Screen.minDimension() / baseScreenSize;
98
+ const initialZoom = Math.min(
99
+ max,
100
+ Math.max(min, initialZoomRaw * CONFIG.zoom.initialMultiplier)
101
+ );
102
+ this.zoom = initialZoom;
103
+ this.targetZoom = initialZoom;
104
+ this.defaultZoom = initialZoom;
105
+
106
+ // Continuous auto-rotation + user-controlled offset (drag)
107
+ this.baseRotation = 0;
108
+ this.userRotation = 0;
109
+ this.targetUserRotation = 0;
110
+
111
+ // Gesture handler for zoom + rotation
112
+ this.gesture = new Gesture(this.canvas, {
113
+ onZoom: (delta) => {
114
+ this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
115
+ },
116
+ onPan: (dx) => {
117
+ this.targetUserRotation += dx * CONFIG.rotation.speed;
118
+ },
119
+ });
120
+
121
+ // Double-click to reset
122
+ this.canvas.addEventListener("dblclick", () => {
123
+ this.targetZoom = this.defaultZoom;
124
+ this.baseRotation = 0;
125
+ this.targetUserRotation = 0;
126
+ this._didFirstClear = false;
127
+ this.renderer?.regenerateSeeds();
128
+ });
129
+
130
+ this.time = 0;
131
+
132
+ // Fade-clear state (first frame fills solid black)
133
+ this._didFirstClear = false;
134
+
135
+ this.renderer = new WebGLDeJongRenderer(CONFIG.points.seedCount, {
136
+ width: this.width,
137
+ height: this.height,
138
+ shape: CONFIG.points.shape,
139
+ blendMode: CONFIG.points.blendMode,
140
+ pointSize: CONFIG.points.pointSize,
141
+ pointScale: CONFIG.points.pointScale,
142
+ iterations: CONFIG.attractor.iterations,
143
+ color: CONFIG.points.color,
144
+ colorMode: 1,
145
+ hueRange: { minHue: CONFIG.visual.minHue, maxHue: CONFIG.visual.maxHue },
146
+ maxSpeed: CONFIG.visual.maxSpeed,
147
+ saturation: CONFIG.visual.saturation / 100,
148
+ lightness: CONFIG.visual.lightness / 100,
149
+ alpha: CONFIG.visual.alpha,
150
+ hueShiftSpeed: CONFIG.visual.hueShiftSpeed,
151
+ params: {
152
+ a: CONFIG.attractor.params.aBase,
153
+ b: CONFIG.attractor.params.b,
154
+ c: CONFIG.attractor.params.c,
155
+ d: CONFIG.attractor.params.d,
156
+ },
157
+ });
158
+
159
+ if (!this.renderer.isAvailable()) {
160
+ console.warn("WebGL not available for DeJong demo");
161
+ }
162
+ }
163
+
164
+ onResize() {
165
+ const { min, max, baseScreenSize } = CONFIG.zoom;
166
+ const initialZoomRaw = Screen.minDimension() / baseScreenSize;
167
+ this.defaultZoom = Math.min(
168
+ max,
169
+ Math.max(min, initialZoomRaw * CONFIG.zoom.initialMultiplier)
170
+ );
171
+ this._didFirstClear = false;
172
+ this.renderer?.resize(this.width, this.height);
173
+ }
174
+
175
+ update(dt) {
176
+ this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
177
+ this.time += dt;
178
+
179
+ // Continuous rotation (never reverses unless user drags against it)
180
+ const TAU = Math.PI * 2;
181
+ this.baseRotation = (this.baseRotation + CONFIG.rotation.autoSpeed * dt) % TAU;
182
+
183
+ // Smooth user rotation offset
184
+ this.userRotation +=
185
+ (this.targetUserRotation - this.userRotation) * CONFIG.rotation.easing;
186
+ super.update(dt);
187
+ }
188
+
189
+ clear() {
190
+ // Fade the canvas to create persistent trails.
191
+ // Use a solid first clear to avoid a "transparent start" look.
192
+ if (!this._didFirstClear) {
193
+ Painter.useCtx((ctx) => {
194
+ ctx.fillStyle = "#000";
195
+ ctx.fillRect(0, 0, this.width, this.height);
196
+ });
197
+ this._didFirstClear = true;
198
+ return;
199
+ }
200
+
201
+ const fade = CONFIG.visual.fadeSpeed;
202
+ if (fade <= 0) return;
203
+
204
+ Painter.useCtx((ctx) => {
205
+ ctx.globalCompositeOperation = "source-over";
206
+ ctx.fillStyle = `rgba(0, 0, 0, ${fade})`;
207
+ ctx.fillRect(0, 0, this.width, this.height);
208
+ });
209
+ }
210
+
211
+ render() {
212
+ // Custom render: fade-clear (via clear()), then composite WebGL output.
213
+ Painter.setContext(this.ctx);
214
+ if (this.running) this.clear();
215
+
216
+ if (this.renderer?.isAvailable()) {
217
+ // Animate params like the reference (a wobbles over time)
218
+ const p = CONFIG.attractor.params;
219
+ const omega = (2 * Math.PI) / Math.max(0.001, p.aPeriodSeconds);
220
+ const a = p.aBase + Math.sin(this.time * omega) * p.aWobble;
221
+
222
+ this.renderer.setParams({ a, b: p.b, c: p.c, d: p.d });
223
+ this.renderer.setIterations(CONFIG.attractor.iterations);
224
+ this.renderer.setZoom(this.zoom);
225
+ this.renderer.setTransform(
226
+ WebGLDeJongRenderer.rotationMat3(this.baseRotation + this.userRotation)
227
+ );
228
+
229
+ // Draw points to the offscreen WebGL canvas, then composite
230
+ this.renderer.clear(0, 0, 0, 0);
231
+ this.renderer.render(this.time);
232
+
233
+ Painter.useCtx((ctx) => {
234
+ const prev = ctx.globalCompositeOperation;
235
+ ctx.globalCompositeOperation = CONFIG.points.compositeBlendMode;
236
+ this.renderer.compositeOnto(ctx, 0, 0);
237
+ ctx.globalCompositeOperation = prev;
238
+ });
239
+ }
240
+ }
241
+
242
+ destroy() {
243
+ this.gesture?.destroy();
244
+ this.renderer?.destroy();
245
+ super.destroy?.();
246
+ }
247
+ }
248
+
249
+ // ─────────────────────────────────────────────────────────────────────────────
250
+ // INITIALIZATION
251
+ // ─────────────────────────────────────────────────────────────────────────────
252
+
253
+ window.addEventListener("load", () => {
254
+ const canvas = document.getElementById("game");
255
+ const demo = new DeJongDemo(canvas);
256
+ demo.start();
257
+ });