@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.
- package/dist/aizawa.html +27 -0
- package/dist/clifford.html +25 -0
- package/dist/cmb.html +24 -0
- package/dist/dadras.html +26 -0
- package/dist/dejong.html +25 -0
- package/dist/gcanvas.es.js +5130 -372
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/halvorsen.html +27 -0
- package/dist/index.html +96 -48
- package/dist/js/aizawa.js +425 -0
- package/dist/js/bezier.js +5 -5
- package/dist/js/clifford.js +236 -0
- package/dist/js/cmb.js +594 -0
- package/dist/js/dadras.js +405 -0
- package/dist/js/dejong.js +257 -0
- package/dist/js/halvorsen.js +405 -0
- package/dist/js/isometric.js +34 -46
- package/dist/js/lorenz.js +425 -0
- package/dist/js/painter.js +8 -8
- package/dist/js/rossler.js +480 -0
- package/dist/js/schrodinger.js +314 -18
- package/dist/js/thomas.js +394 -0
- package/dist/lorenz.html +27 -0
- package/dist/rossler.html +27 -0
- package/dist/scene-interactivity-test.html +220 -0
- package/dist/thomas.html +27 -0
- package/package.json +1 -1
- package/readme.md +30 -22
- package/src/game/objects/go.js +7 -0
- package/src/game/objects/index.js +2 -0
- package/src/game/objects/isometric-scene.js +53 -3
- package/src/game/objects/layoutscene.js +57 -0
- package/src/game/objects/mask.js +241 -0
- package/src/game/objects/scene.js +19 -0
- package/src/game/objects/wrapper.js +14 -2
- package/src/game/pipeline.js +17 -0
- package/src/game/ui/button.js +101 -16
- package/src/game/ui/theme.js +0 -6
- package/src/game/ui/togglebutton.js +25 -14
- package/src/game/ui/tooltip.js +12 -4
- package/src/index.js +3 -0
- package/src/io/gesture.js +409 -0
- package/src/io/index.js +4 -1
- package/src/io/keys.js +9 -1
- package/src/io/screen.js +476 -0
- package/src/math/attractors.js +664 -0
- package/src/math/heat.js +106 -0
- package/src/math/index.js +1 -0
- package/src/mixins/draggable.js +15 -19
- package/src/painter/painter.shapes.js +11 -5
- package/src/particle/particle-system.js +165 -1
- package/src/physics/index.js +26 -0
- package/src/physics/physics-updaters.js +333 -0
- package/src/physics/physics.js +375 -0
- package/src/shapes/image.js +5 -5
- package/src/shapes/index.js +2 -0
- package/src/shapes/parallelogram.js +147 -0
- package/src/shapes/righttriangle.js +115 -0
- package/src/shapes/svg.js +281 -100
- package/src/shapes/text.js +22 -6
- package/src/shapes/transformable.js +5 -0
- package/src/sound/effects.js +807 -0
- package/src/sound/index.js +13 -0
- package/src/webgl/index.js +7 -0
- package/src/webgl/shaders/clifford-point-shaders.js +131 -0
- package/src/webgl/shaders/dejong-point-shaders.js +131 -0
- package/src/webgl/shaders/point-sprite-shaders.js +152 -0
- package/src/webgl/webgl-clifford-renderer.js +477 -0
- package/src/webgl/webgl-dejong-renderer.js +472 -0
- package/src/webgl/webgl-line-renderer.js +391 -0
- package/src/webgl/webgl-particle-renderer.js +410 -0
- package/types/index.d.ts +30 -2
- package/types/io.d.ts +217 -0
- package/types/physics.d.ts +299 -0
- package/types/shapes.d.ts +8 -0
- package/types/webgl.d.ts +188 -109
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Halvorsen Attractor 3D</title>
|
|
7
|
+
<link rel="stylesheet" href="demos.css" />
|
|
8
|
+
<script src="./js/info-toggle.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="info">
|
|
12
|
+
<strong>Halvorsen Attractor</strong><br/>
|
|
13
|
+
<span style="color:#CCC">
|
|
14
|
+
<li>dx/dt = -ax - 4y - 4z - y²</li>
|
|
15
|
+
<li>dy/dt = -ay - 4z - 4x - z²</li>
|
|
16
|
+
<li>dz/dt = -az - 4x - 4y - x²</li>
|
|
17
|
+
<li>a = 1.89</li>
|
|
18
|
+
<li>Three-fold rotational symmetry</li>
|
|
19
|
+
<li>Blue = slow, Pink = fast</li>
|
|
20
|
+
<li>Drag to rotate</li>
|
|
21
|
+
<li>Scroll/pinch to zoom</li>
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
<canvas id="game"></canvas>
|
|
25
|
+
<script type="module" src="./js/halvorsen.js"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
package/dist/index.html
CHANGED
|
@@ -6,58 +6,91 @@
|
|
|
6
6
|
|
|
7
7
|
<!-- Primary Meta Tags -->
|
|
8
8
|
<title>GCanvas Demos - Interactive Examples & Documentation</title>
|
|
9
|
-
<meta
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<meta
|
|
14
|
-
|
|
9
|
+
<meta
|
|
10
|
+
name="title"
|
|
11
|
+
content="GCanvas Demos - Interactive Examples & Documentation"
|
|
12
|
+
/>
|
|
13
|
+
<meta
|
|
14
|
+
name="description"
|
|
15
|
+
content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action."
|
|
16
|
+
/>
|
|
17
|
+
<meta
|
|
18
|
+
name="keywords"
|
|
19
|
+
content="gcanvas, canvas demos, 2d canvas examples, game framework demos, javascript canvas, html5 canvas examples, interactive demos, creative coding examples"
|
|
20
|
+
/>
|
|
21
|
+
<meta name="author" content="GCanvas" />
|
|
22
|
+
<meta name="robots" content="index, follow" />
|
|
23
|
+
<link rel="canonical" href="https://gcanvas.guinetik.com/" />
|
|
15
24
|
|
|
16
25
|
<!-- Open Graph / Facebook -->
|
|
17
|
-
<meta property="og:type" content="website"
|
|
18
|
-
<meta property="og:url" content="https://gcanvas.guinetik.com/"
|
|
19
|
-
<meta
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<meta
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
<meta property="og:type" content="website" />
|
|
27
|
+
<meta property="og:url" content="https://gcanvas.guinetik.com/" />
|
|
28
|
+
<meta
|
|
29
|
+
property="og:title"
|
|
30
|
+
content="GCanvas Demos - Interactive Examples & Documentation"
|
|
31
|
+
/>
|
|
32
|
+
<meta
|
|
33
|
+
property="og:description"
|
|
34
|
+
content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action."
|
|
35
|
+
/>
|
|
36
|
+
<meta
|
|
37
|
+
property="og:image"
|
|
38
|
+
content="https://gcanvas.guinetik.com/og_image.png"
|
|
39
|
+
/>
|
|
40
|
+
<meta property="og:image:width" content="1200" />
|
|
41
|
+
<meta property="og:image:height" content="630" />
|
|
42
|
+
<meta
|
|
43
|
+
property="og:image:alt"
|
|
44
|
+
content="GCanvas interactive demos and examples"
|
|
45
|
+
/>
|
|
46
|
+
<meta property="og:site_name" content="GCanvas" />
|
|
47
|
+
<meta property="og:locale" content="en_US" />
|
|
27
48
|
|
|
28
49
|
<!-- Twitter -->
|
|
29
|
-
<meta name="twitter:card" content="summary_large_image"
|
|
30
|
-
<meta name="twitter:url" content="https://gcanvas.guinetik.com/"
|
|
31
|
-
<meta
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<meta
|
|
50
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
51
|
+
<meta name="twitter:url" content="https://gcanvas.guinetik.com/" />
|
|
52
|
+
<meta
|
|
53
|
+
name="twitter:title"
|
|
54
|
+
content="GCanvas Demos - Interactive Examples & Documentation"
|
|
55
|
+
/>
|
|
56
|
+
<meta
|
|
57
|
+
name="twitter:description"
|
|
58
|
+
content="Explore interactive demos and examples of GCanvas, a modular 2D canvas rendering and game framework. See shapes, animations, physics simulations, games, and more in action."
|
|
59
|
+
/>
|
|
60
|
+
<meta
|
|
61
|
+
name="twitter:image"
|
|
62
|
+
content="https://gcanvas.guinetik.com/og_image.png"
|
|
63
|
+
/>
|
|
64
|
+
<meta
|
|
65
|
+
name="twitter:image:alt"
|
|
66
|
+
content="GCanvas interactive demos and examples"
|
|
67
|
+
/>
|
|
68
|
+
<meta name="twitter:creator" content="@guinetik" />
|
|
36
69
|
|
|
37
70
|
<!-- Favicon -->
|
|
38
|
-
<link rel="icon" type="image/svg+xml" href="./logo.svg"
|
|
39
|
-
<link rel="apple-touch-icon" href="./logo.svg"
|
|
40
|
-
<meta name="theme-color" content="#000000"
|
|
71
|
+
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
|
|
72
|
+
<link rel="apple-touch-icon" href="./logo.svg" />
|
|
73
|
+
<meta name="theme-color" content="#000000" />
|
|
41
74
|
|
|
42
75
|
<!-- Structured Data (JSON-LD) -->
|
|
43
76
|
<script type="application/ld+json">
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
{
|
|
78
|
+
"@context": "https://schema.org",
|
|
79
|
+
"@type": "WebSite",
|
|
80
|
+
"name": "GCanvas Demos",
|
|
81
|
+
"url": "https://gcanvas.guinetik.com/",
|
|
82
|
+
"description": "Interactive demos and examples for GCanvas, a modular 2D canvas rendering and game framework",
|
|
83
|
+
"publisher": {
|
|
84
|
+
"@type": "Organization",
|
|
85
|
+
"name": "GCanvas",
|
|
86
|
+
"url": "https://github.com/guinetik/gcanvas"
|
|
87
|
+
},
|
|
88
|
+
"potentialAction": {
|
|
89
|
+
"@type": "SearchAction",
|
|
90
|
+
"target": "https://gcanvas.guinetik.com/?q={search_term_string}",
|
|
91
|
+
"query-input": "required name=search_term_string"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
61
94
|
</script>
|
|
62
95
|
|
|
63
96
|
<link rel="stylesheet" href="demos.css" />
|
|
@@ -135,7 +168,7 @@
|
|
|
135
168
|
title="View on GitHub"
|
|
136
169
|
style="
|
|
137
170
|
display: inline-block;
|
|
138
|
-
background: #
|
|
171
|
+
background: #1f1f1f;
|
|
139
172
|
border-radius: 50%;
|
|
140
173
|
width: 36px;
|
|
141
174
|
height: 36px;
|
|
@@ -213,16 +246,22 @@
|
|
|
213
246
|
<a href="tiles.html" target="demo-frame">Tile Layout</a>
|
|
214
247
|
<a href="isometric.html" target="demo-frame">Isometric</a>
|
|
215
248
|
<a href="scenes.html" target="demo-frame">Scene Transforms</a>
|
|
249
|
+
<a href="scene-interactivity-test.html" target="demo-frame"
|
|
250
|
+
>Scene Interactivity</a
|
|
251
|
+
>
|
|
216
252
|
<hr />
|
|
217
253
|
<h2 style="margin-bottom: 0.3em">3D</h2>
|
|
218
254
|
<a href="sphere3d.html" target="demo-frame">Sphere3D Showcase</a>
|
|
219
255
|
<a href="plane3d.html" target="demo-frame">Plane3D Showcase</a>
|
|
220
256
|
<a href="cube3d.html" target="demo-frame">Rubik's Cube</a>
|
|
221
257
|
<hr />
|
|
222
|
-
<h2 style="margin-bottom: 0.3em; color: #0f0
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
258
|
+
<h2 style="margin-bottom: 0.3em; color: #0f0">
|
|
259
|
+
<a
|
|
260
|
+
href="../genuary26/"
|
|
261
|
+
style="color: #0f0; text-decoration: none"
|
|
262
|
+
>
|
|
263
|
+
Genuary 2026 →
|
|
264
|
+
</a>
|
|
226
265
|
</h2>
|
|
227
266
|
<hr />
|
|
228
267
|
<h2 style="margin-bottom: 0.3em">Generative Art</h2>
|
|
@@ -247,6 +286,7 @@
|
|
|
247
286
|
<a href="baskara.html" target="demo-frame">Root Dance</a>
|
|
248
287
|
<hr />
|
|
249
288
|
<h2 style="margin-bottom: 0.3em">Math & Physics</h2>
|
|
289
|
+
<a href="cmb.html" target="demo-frame">Cosmic Microwave Background</a>
|
|
250
290
|
<a href="fluid.html" target="demo-frame">Fluid Playground</a>
|
|
251
291
|
<a href="fluid-simple.html" target="demo-frame">Fluid System</a>
|
|
252
292
|
<a href="schrodinger.html" target="demo-frame">Schrodinger Wave</a>
|
|
@@ -257,6 +297,14 @@
|
|
|
257
297
|
<a href="kerr.html" target="demo-frame">Kerr Metric</a>
|
|
258
298
|
<a href="blackhole.html" target="demo-frame">Black Hole</a>
|
|
259
299
|
<a href="tde.html" target="demo-frame">Tidal Disruption</a>
|
|
300
|
+
<a href="dadras.html" target="demo-frame">Dadras Attractor</a>
|
|
301
|
+
<a href="lorenz.html" target="demo-frame">Lorenz Attractor</a>
|
|
302
|
+
<a href="aizawa.html" target="demo-frame">Aizawa Attractor</a>
|
|
303
|
+
<a href="thomas.html" target="demo-frame">Thomas Attractor</a>
|
|
304
|
+
<a href="rossler.html" target="demo-frame">Rössler Attractor</a>
|
|
305
|
+
<a href="halvorsen.html" target="demo-frame">Halvorsen Attractor</a>
|
|
306
|
+
<a href="clifford.html" target="demo-frame">Clifford Attractor</a>
|
|
307
|
+
<a href="dejong.html" target="demo-frame">De Jong Attractor</a>
|
|
260
308
|
<hr />
|
|
261
309
|
<h2 style="margin-bottom: 0.3em">Games</h2>
|
|
262
310
|
<a href="blob.html" target="demo-frame">Bezier Blob</a>
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aizawa Attractor 3D Visualization
|
|
3
|
+
*
|
|
4
|
+
* A 3D chaotic system with intricate folding structure, named after
|
|
5
|
+
* Japanese mathematician Tomohiko Aizawa. Features complex orbits
|
|
6
|
+
* with a distinctive torus-like shape.
|
|
7
|
+
*
|
|
8
|
+
* Uses the Attractors module for pure math functions and WebGL for
|
|
9
|
+
* high-performance line rendering.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Game, Gesture, Screen, Attractors } from "/gcanvas.es.min.js";
|
|
13
|
+
import { Camera3D } from "/gcanvas.es.min.js";
|
|
14
|
+
import { WebGLLineRenderer } from "/gcanvas.es.min.js";
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// CONFIGURATION
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
// Attractor settings (uses Attractors.aizawa for equations)
|
|
22
|
+
attractor: {
|
|
23
|
+
dt: 0.008, // Integration time step
|
|
24
|
+
scale: 120, // Scale factor for display (Aizawa is small)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Particle settings
|
|
28
|
+
particles: {
|
|
29
|
+
count: 350,
|
|
30
|
+
trailLength: 220,
|
|
31
|
+
spawnRange: 0.5, // Initial position range around origin
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Center offset - adjust to match attractor's visual barycenter
|
|
35
|
+
center: {
|
|
36
|
+
x: 0,
|
|
37
|
+
y: 0,
|
|
38
|
+
z: 0.5, // Aizawa orbits around z ≈ 0.5
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Camera settings
|
|
42
|
+
camera: {
|
|
43
|
+
perspective: 800,
|
|
44
|
+
rotationX: 0.4,
|
|
45
|
+
rotationY: 0,
|
|
46
|
+
inertia: true,
|
|
47
|
+
friction: 0.95,
|
|
48
|
+
clampX: false,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Visual settings
|
|
52
|
+
visual: {
|
|
53
|
+
minHue: 280, // Magenta (fast)
|
|
54
|
+
maxHue: 180, // Cyan (slow)
|
|
55
|
+
maxSpeed: 8, // Speed normalization threshold
|
|
56
|
+
saturation: 90,
|
|
57
|
+
lightness: 55,
|
|
58
|
+
maxAlpha: 0.85,
|
|
59
|
+
hueShiftSpeed: 12, // Degrees per second
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Glitch/blink effect
|
|
63
|
+
blink: {
|
|
64
|
+
chance: 0.018,
|
|
65
|
+
minDuration: 0.04,
|
|
66
|
+
maxDuration: 0.2,
|
|
67
|
+
intensityBoost: 1.5,
|
|
68
|
+
saturationBoost: 1.2,
|
|
69
|
+
alphaBoost: 1.3,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Zoom settings
|
|
73
|
+
zoom: {
|
|
74
|
+
min: 0.3,
|
|
75
|
+
max: 2.5,
|
|
76
|
+
speed: 0.5,
|
|
77
|
+
easing: 0.12,
|
|
78
|
+
baseScreenSize: 600,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// HELPER FUNCTIONS
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert HSL to RGB
|
|
88
|
+
*/
|
|
89
|
+
function hslToRgb(h, s, l) {
|
|
90
|
+
s /= 100;
|
|
91
|
+
l /= 100;
|
|
92
|
+
const k = (n) => (n + h / 30) % 12;
|
|
93
|
+
const a = s * Math.min(l, 1 - l);
|
|
94
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
95
|
+
return {
|
|
96
|
+
r: Math.round(255 * f(0)),
|
|
97
|
+
g: Math.round(255 * f(8)),
|
|
98
|
+
b: Math.round(255 * f(4)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// ATTRACTOR PARTICLE
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A particle following attractor dynamics
|
|
108
|
+
*/
|
|
109
|
+
class AttractorParticle {
|
|
110
|
+
constructor(stepFn, spawnRange) {
|
|
111
|
+
this.stepFn = stepFn;
|
|
112
|
+
this.position = {
|
|
113
|
+
x: (Math.random() - 0.5) * spawnRange,
|
|
114
|
+
y: (Math.random() - 0.5) * spawnRange,
|
|
115
|
+
z: (Math.random() - 0.5) * spawnRange,
|
|
116
|
+
};
|
|
117
|
+
this.trail = [];
|
|
118
|
+
this.speed = 0;
|
|
119
|
+
|
|
120
|
+
// Blink/glitch state
|
|
121
|
+
this.blinkTime = 0;
|
|
122
|
+
this.blinkIntensity = 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
updateBlink(dt) {
|
|
126
|
+
const { chance, minDuration, maxDuration } = CONFIG.blink;
|
|
127
|
+
|
|
128
|
+
if (this.blinkTime > 0) {
|
|
129
|
+
this.blinkTime -= dt;
|
|
130
|
+
this.blinkIntensity = Math.max(
|
|
131
|
+
0,
|
|
132
|
+
this.blinkTime > 0
|
|
133
|
+
? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
|
|
134
|
+
: 0
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
if (Math.random() < chance) {
|
|
138
|
+
this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
|
|
139
|
+
this.blinkIntensity = 1;
|
|
140
|
+
} else {
|
|
141
|
+
this.blinkIntensity = 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
update(dt, scale) {
|
|
147
|
+
const result = this.stepFn(this.position, dt);
|
|
148
|
+
|
|
149
|
+
this.position = result.position;
|
|
150
|
+
this.speed = result.speed;
|
|
151
|
+
|
|
152
|
+
// Add to trail (reoriented and scaled for display)
|
|
153
|
+
// Swap Y and Z so the attractor's vertical axis aligns with screen vertical
|
|
154
|
+
this.trail.unshift({
|
|
155
|
+
x: this.position.x * scale,
|
|
156
|
+
y: this.position.z * scale, // Z becomes screen Y (vertical)
|
|
157
|
+
z: this.position.y * scale, // Y becomes depth
|
|
158
|
+
speed: this.speed,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (this.trail.length > CONFIG.particles.trailLength) {
|
|
162
|
+
this.trail.pop();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
// DEMO CLASS
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Aizawa Attractor Demo
|
|
173
|
+
*/
|
|
174
|
+
class AizawaDemo extends Game {
|
|
175
|
+
constructor(canvas) {
|
|
176
|
+
super(canvas);
|
|
177
|
+
this.backgroundColor = "#000";
|
|
178
|
+
this.enableFluidSize();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
init() {
|
|
182
|
+
super.init();
|
|
183
|
+
|
|
184
|
+
// Get attractor info
|
|
185
|
+
this.attractor = Attractors.aizawa;
|
|
186
|
+
console.log(`Attractor: ${this.attractor.name}`);
|
|
187
|
+
console.log(`Equations:`, this.attractor.equations);
|
|
188
|
+
|
|
189
|
+
// Create stepper function
|
|
190
|
+
this.stepFn = this.attractor.createStepper();
|
|
191
|
+
|
|
192
|
+
// Calculate initial zoom
|
|
193
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
194
|
+
const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
195
|
+
this.zoom = initialZoom;
|
|
196
|
+
this.targetZoom = initialZoom;
|
|
197
|
+
this.defaultZoom = initialZoom;
|
|
198
|
+
|
|
199
|
+
// Camera with mouse control
|
|
200
|
+
this.camera = new Camera3D({
|
|
201
|
+
perspective: CONFIG.camera.perspective,
|
|
202
|
+
rotationX: CONFIG.camera.rotationX,
|
|
203
|
+
rotationY: CONFIG.camera.rotationY,
|
|
204
|
+
inertia: CONFIG.camera.inertia,
|
|
205
|
+
friction: CONFIG.camera.friction,
|
|
206
|
+
clampX: CONFIG.camera.clampX,
|
|
207
|
+
});
|
|
208
|
+
this.camera.enableMouseControl(this.canvas, {
|
|
209
|
+
invertX: true, // flip left/right
|
|
210
|
+
invertY: true, // flip up/down
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Gesture handler for zoom
|
|
214
|
+
this.gesture = new Gesture(this.canvas, {
|
|
215
|
+
onZoom: (delta) => {
|
|
216
|
+
this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
|
|
217
|
+
},
|
|
218
|
+
onPan: null,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Double-click to reset
|
|
222
|
+
this.canvas.addEventListener("dblclick", () => {
|
|
223
|
+
this.targetZoom = this.defaultZoom;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Log camera params on mouse release (for finding good starting angle)
|
|
227
|
+
this.canvas.addEventListener("mouseup", () => {
|
|
228
|
+
console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
|
|
229
|
+
|
|
230
|
+
// Calculate and log barycenter to help find the right center offset
|
|
231
|
+
let sumX = 0, sumY = 0, sumZ = 0, count = 0;
|
|
232
|
+
for (const p of this.particles) {
|
|
233
|
+
sumX += p.position.x;
|
|
234
|
+
sumY += p.position.y;
|
|
235
|
+
sumZ += p.position.z;
|
|
236
|
+
count++;
|
|
237
|
+
}
|
|
238
|
+
console.log(`Barycenter: x: ${(sumX/count).toFixed(3)}, y: ${(sumY/count).toFixed(3)}, z: ${(sumZ/count).toFixed(3)}`);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Initialize particles
|
|
242
|
+
this.particles = [];
|
|
243
|
+
for (let i = 0; i < CONFIG.particles.count; i++) {
|
|
244
|
+
this.particles.push(
|
|
245
|
+
new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// WebGL line renderer
|
|
250
|
+
const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
|
|
251
|
+
this.lineRenderer = new WebGLLineRenderer(maxSegments, {
|
|
252
|
+
width: this.width,
|
|
253
|
+
height: this.height,
|
|
254
|
+
blendMode: "additive",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
this.segments = [];
|
|
258
|
+
|
|
259
|
+
if (!this.lineRenderer.isAvailable()) {
|
|
260
|
+
console.warn("WebGL not available, falling back to Canvas 2D");
|
|
261
|
+
this.useWebGL = false;
|
|
262
|
+
} else {
|
|
263
|
+
this.useWebGL = true;
|
|
264
|
+
console.log(`WebGL enabled, ${maxSegments} max segments`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.time = 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
onResize() {
|
|
271
|
+
if (this.lineRenderer?.isAvailable()) {
|
|
272
|
+
this.lineRenderer.resize(this.width, this.height);
|
|
273
|
+
}
|
|
274
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
275
|
+
this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
update(dt) {
|
|
279
|
+
super.update(dt);
|
|
280
|
+
this.camera.update(dt);
|
|
281
|
+
|
|
282
|
+
// Normalize rotation to prevent unbounded values
|
|
283
|
+
const TAU = Math.PI * 2;
|
|
284
|
+
this.camera.rotationY = ((this.camera.rotationY % TAU) + TAU) % TAU;
|
|
285
|
+
|
|
286
|
+
this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
|
|
287
|
+
this.time += dt;
|
|
288
|
+
|
|
289
|
+
for (const particle of this.particles) {
|
|
290
|
+
particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale);
|
|
291
|
+
particle.updateBlink(dt);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
collectSegments(cx, cy) {
|
|
296
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
297
|
+
CONFIG.visual;
|
|
298
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
299
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
300
|
+
|
|
301
|
+
this.segments.length = 0;
|
|
302
|
+
|
|
303
|
+
for (const particle of this.particles) {
|
|
304
|
+
if (particle.trail.length < 2) continue;
|
|
305
|
+
|
|
306
|
+
const blink = particle.blinkIntensity;
|
|
307
|
+
|
|
308
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
309
|
+
const curr = particle.trail[i];
|
|
310
|
+
const prev = particle.trail[i - 1];
|
|
311
|
+
|
|
312
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
313
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
314
|
+
|
|
315
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
316
|
+
|
|
317
|
+
const age = i / particle.trail.length;
|
|
318
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
319
|
+
const baseHue = maxHue + speedNorm * (minHue - maxHue);
|
|
320
|
+
const hue = (baseHue + hueOffset + 360) % 360;
|
|
321
|
+
|
|
322
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
323
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
324
|
+
const rgb = hslToRgb(hue, sat, lit);
|
|
325
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
326
|
+
|
|
327
|
+
this.segments.push({
|
|
328
|
+
x1: cx + p1.x * this.zoom,
|
|
329
|
+
y1: cy + p1.y * this.zoom,
|
|
330
|
+
x2: cx + p2.x * this.zoom,
|
|
331
|
+
y2: cy + p2.y * this.zoom,
|
|
332
|
+
r: rgb.r,
|
|
333
|
+
g: rgb.g,
|
|
334
|
+
b: rgb.b,
|
|
335
|
+
a: alpha,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return this.segments.length;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
renderCanvas2D(cx, cy) {
|
|
344
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
345
|
+
CONFIG.visual;
|
|
346
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
347
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
348
|
+
|
|
349
|
+
const ctx = this.ctx;
|
|
350
|
+
ctx.save();
|
|
351
|
+
ctx.globalCompositeOperation = "lighter";
|
|
352
|
+
ctx.lineCap = "round";
|
|
353
|
+
|
|
354
|
+
for (const particle of this.particles) {
|
|
355
|
+
if (particle.trail.length < 2) continue;
|
|
356
|
+
|
|
357
|
+
const blink = particle.blinkIntensity;
|
|
358
|
+
|
|
359
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
360
|
+
const curr = particle.trail[i];
|
|
361
|
+
const prev = particle.trail[i - 1];
|
|
362
|
+
|
|
363
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
364
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
365
|
+
|
|
366
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
367
|
+
|
|
368
|
+
const age = i / particle.trail.length;
|
|
369
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
370
|
+
const baseHue = maxHue + speedNorm * (minHue - maxHue);
|
|
371
|
+
const hue = (baseHue + hueOffset + 360) % 360;
|
|
372
|
+
|
|
373
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
374
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
375
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
376
|
+
|
|
377
|
+
ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
|
|
378
|
+
ctx.lineWidth = 1;
|
|
379
|
+
|
|
380
|
+
ctx.beginPath();
|
|
381
|
+
ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
|
|
382
|
+
ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
|
|
383
|
+
ctx.stroke();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ctx.restore();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
render() {
|
|
391
|
+
super.render();
|
|
392
|
+
if (!this.particles) return;
|
|
393
|
+
|
|
394
|
+
const cx = this.width / 2;
|
|
395
|
+
const cy = this.height / 2;
|
|
396
|
+
|
|
397
|
+
if (this.useWebGL && this.lineRenderer.isAvailable()) {
|
|
398
|
+
const segmentCount = this.collectSegments(cx, cy);
|
|
399
|
+
if (segmentCount > 0) {
|
|
400
|
+
this.lineRenderer.clear();
|
|
401
|
+
this.lineRenderer.updateLines(this.segments);
|
|
402
|
+
this.lineRenderer.render(segmentCount);
|
|
403
|
+
this.lineRenderer.compositeOnto(this.ctx, 0, 0);
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
this.renderCanvas2D(cx, cy);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
destroy() {
|
|
411
|
+
this.gesture?.destroy();
|
|
412
|
+
this.lineRenderer?.destroy();
|
|
413
|
+
super.destroy?.();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
+
// INITIALIZATION
|
|
419
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
window.addEventListener("load", () => {
|
|
422
|
+
const canvas = document.getElementById("game");
|
|
423
|
+
const demo = new AizawaDemo(canvas);
|
|
424
|
+
demo.start();
|
|
425
|
+
});
|