@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,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
|
+
});
|