@guinetik/gcanvas 1.0.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/aizawa.html +27 -0
  2. package/dist/clifford.html +25 -0
  3. package/dist/cmb.html +24 -0
  4. package/dist/dadras.html +26 -0
  5. package/dist/dejong.html +25 -0
  6. package/dist/gcanvas.es.js +5130 -372
  7. package/dist/gcanvas.es.min.js +1 -1
  8. package/dist/gcanvas.umd.js +1 -1
  9. package/dist/gcanvas.umd.min.js +1 -1
  10. package/dist/halvorsen.html +27 -0
  11. package/dist/index.html +96 -48
  12. package/dist/js/aizawa.js +425 -0
  13. package/dist/js/bezier.js +5 -5
  14. package/dist/js/clifford.js +236 -0
  15. package/dist/js/cmb.js +594 -0
  16. package/dist/js/dadras.js +405 -0
  17. package/dist/js/dejong.js +257 -0
  18. package/dist/js/halvorsen.js +405 -0
  19. package/dist/js/isometric.js +34 -46
  20. package/dist/js/lorenz.js +425 -0
  21. package/dist/js/painter.js +8 -8
  22. package/dist/js/rossler.js +480 -0
  23. package/dist/js/schrodinger.js +314 -18
  24. package/dist/js/thomas.js +394 -0
  25. package/dist/lorenz.html +27 -0
  26. package/dist/rossler.html +27 -0
  27. package/dist/scene-interactivity-test.html +220 -0
  28. package/dist/thomas.html +27 -0
  29. package/package.json +1 -1
  30. package/readme.md +30 -22
  31. package/src/game/objects/go.js +7 -0
  32. package/src/game/objects/index.js +2 -0
  33. package/src/game/objects/isometric-scene.js +53 -3
  34. package/src/game/objects/layoutscene.js +57 -0
  35. package/src/game/objects/mask.js +241 -0
  36. package/src/game/objects/scene.js +19 -0
  37. package/src/game/objects/wrapper.js +14 -2
  38. package/src/game/pipeline.js +17 -0
  39. package/src/game/ui/button.js +101 -16
  40. package/src/game/ui/theme.js +0 -6
  41. package/src/game/ui/togglebutton.js +25 -14
  42. package/src/game/ui/tooltip.js +12 -4
  43. package/src/index.js +3 -0
  44. package/src/io/gesture.js +409 -0
  45. package/src/io/index.js +4 -1
  46. package/src/io/keys.js +9 -1
  47. package/src/io/screen.js +476 -0
  48. package/src/math/attractors.js +664 -0
  49. package/src/math/heat.js +106 -0
  50. package/src/math/index.js +1 -0
  51. package/src/mixins/draggable.js +15 -19
  52. package/src/painter/painter.shapes.js +11 -5
  53. package/src/particle/particle-system.js +165 -1
  54. package/src/physics/index.js +26 -0
  55. package/src/physics/physics-updaters.js +333 -0
  56. package/src/physics/physics.js +375 -0
  57. package/src/shapes/image.js +5 -5
  58. package/src/shapes/index.js +2 -0
  59. package/src/shapes/parallelogram.js +147 -0
  60. package/src/shapes/righttriangle.js +115 -0
  61. package/src/shapes/svg.js +281 -100
  62. package/src/shapes/text.js +22 -6
  63. package/src/shapes/transformable.js +5 -0
  64. package/src/sound/effects.js +807 -0
  65. package/src/sound/index.js +13 -0
  66. package/src/webgl/index.js +7 -0
  67. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  68. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  69. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  70. package/src/webgl/webgl-clifford-renderer.js +477 -0
  71. package/src/webgl/webgl-dejong-renderer.js +472 -0
  72. package/src/webgl/webgl-line-renderer.js +391 -0
  73. package/src/webgl/webgl-particle-renderer.js +410 -0
  74. package/types/index.d.ts +30 -2
  75. package/types/io.d.ts +217 -0
  76. package/types/physics.d.ts +299 -0
  77. package/types/shapes.d.ts +8 -0
  78. package/types/webgl.d.ts +188 -109
@@ -0,0 +1,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
+ });