@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,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Halvorsen Attractor 3D Visualization
|
|
3
|
+
*
|
|
4
|
+
* A symmetric chaotic attractor with three-fold rotational symmetry.
|
|
5
|
+
* Creates beautiful intertwined spiral structures.
|
|
6
|
+
*
|
|
7
|
+
* Uses the Attractors module for pure math functions and WebGL for
|
|
8
|
+
* high-performance line rendering.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Game, Gesture, Screen, Attractors } from "/gcanvas.es.min.js";
|
|
12
|
+
import { Camera3D } from "/gcanvas.es.min.js";
|
|
13
|
+
import { WebGLLineRenderer } from "/gcanvas.es.min.js";
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// CONFIGURATION
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const CONFIG = {
|
|
20
|
+
// Attractor settings (uses Attractors.halvorsen for equations)
|
|
21
|
+
attractor: {
|
|
22
|
+
dt: 0.004, // Integration time step
|
|
23
|
+
scale: 25, // Scale factor for display
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Particle settings
|
|
27
|
+
particles: {
|
|
28
|
+
count: 420,
|
|
29
|
+
trailLength: 280,
|
|
30
|
+
spawnRange: 1,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Center offset - Halvorsen barycenter is around (-5,-5,-5) due to symmetry
|
|
34
|
+
// With Y/Z swap: x→screen X, z→screen Y, y→depth
|
|
35
|
+
center: {
|
|
36
|
+
x: 0,
|
|
37
|
+
y: 0,
|
|
38
|
+
z: 0,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Camera settings
|
|
42
|
+
camera: {
|
|
43
|
+
perspective: 300,
|
|
44
|
+
rotationX: 0.615,
|
|
45
|
+
rotationY: 0.495,
|
|
46
|
+
inertia: true,
|
|
47
|
+
friction: 0.95,
|
|
48
|
+
clampX: false,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Visual settings - cool blue/purple palette
|
|
52
|
+
visual: {
|
|
53
|
+
minHue: 320, // Pink (fast)
|
|
54
|
+
maxHue: 220, // Blue (slow)
|
|
55
|
+
maxSpeed: 40,
|
|
56
|
+
saturation: 80,
|
|
57
|
+
lightness: 55,
|
|
58
|
+
maxAlpha: 0.85,
|
|
59
|
+
hueShiftSpeed: 15,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Glitch/blink effect
|
|
63
|
+
blink: {
|
|
64
|
+
chance: 0.02,
|
|
65
|
+
minDuration: 0.04,
|
|
66
|
+
maxDuration: 0.18,
|
|
67
|
+
intensityBoost: 1.5,
|
|
68
|
+
saturationBoost: 1.2,
|
|
69
|
+
alphaBoost: 1.3,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Zoom settings
|
|
73
|
+
zoom: {
|
|
74
|
+
min: 0.25,
|
|
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
|
+
function hslToRgb(h, s, l) {
|
|
87
|
+
s /= 100;
|
|
88
|
+
l /= 100;
|
|
89
|
+
const k = (n) => (n + h / 30) % 12;
|
|
90
|
+
const a = s * Math.min(l, 1 - l);
|
|
91
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
92
|
+
return {
|
|
93
|
+
r: Math.round(255 * f(0)),
|
|
94
|
+
g: Math.round(255 * f(8)),
|
|
95
|
+
b: Math.round(255 * f(4)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// ATTRACTOR PARTICLE
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
class AttractorParticle {
|
|
104
|
+
constructor(stepFn, spawnRange) {
|
|
105
|
+
this.stepFn = stepFn;
|
|
106
|
+
this.position = {
|
|
107
|
+
x: (Math.random() - 0.5) * spawnRange,
|
|
108
|
+
y: (Math.random() - 0.5) * spawnRange,
|
|
109
|
+
z: (Math.random() - 0.5) * spawnRange,
|
|
110
|
+
};
|
|
111
|
+
this.trail = [];
|
|
112
|
+
this.speed = 0;
|
|
113
|
+
this.blinkTime = 0;
|
|
114
|
+
this.blinkIntensity = 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updateBlink(dt) {
|
|
118
|
+
const { chance, minDuration, maxDuration } = CONFIG.blink;
|
|
119
|
+
|
|
120
|
+
if (this.blinkTime > 0) {
|
|
121
|
+
this.blinkTime -= dt;
|
|
122
|
+
this.blinkIntensity = Math.max(
|
|
123
|
+
0,
|
|
124
|
+
this.blinkTime > 0
|
|
125
|
+
? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
|
|
126
|
+
: 0
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
if (Math.random() < chance) {
|
|
130
|
+
this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
|
|
131
|
+
this.blinkIntensity = 1;
|
|
132
|
+
} else {
|
|
133
|
+
this.blinkIntensity = 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
update(dt, scale, spawnRange) {
|
|
139
|
+
const result = this.stepFn(this.position, dt);
|
|
140
|
+
this.position = result.position;
|
|
141
|
+
this.speed = result.speed;
|
|
142
|
+
|
|
143
|
+
// Small chance to respawn at random position (keeps transient "thickness")
|
|
144
|
+
if (Math.random() < 0.003) {
|
|
145
|
+
this.position = {
|
|
146
|
+
x: (Math.random() - 0.5) * spawnRange,
|
|
147
|
+
y: (Math.random() - 0.5) * spawnRange,
|
|
148
|
+
z: (Math.random() - 0.5) * spawnRange,
|
|
149
|
+
};
|
|
150
|
+
this.trail = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add to trail (centered and scaled for display)
|
|
154
|
+
// Swap Y/Z so vertical mouse drag rotates naturally
|
|
155
|
+
this.trail.unshift({
|
|
156
|
+
x: (this.position.x - CONFIG.center.x) * scale,
|
|
157
|
+
y: (this.position.z - CONFIG.center.z) * scale, // Z becomes screen Y (vertical)
|
|
158
|
+
z: (this.position.y - CONFIG.center.y) * scale, // Y becomes depth
|
|
159
|
+
speed: this.speed,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (this.trail.length > CONFIG.particles.trailLength) {
|
|
163
|
+
this.trail.pop();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// DEMO CLASS
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
class HalvorsenDemo extends Game {
|
|
173
|
+
constructor(canvas) {
|
|
174
|
+
super(canvas);
|
|
175
|
+
this.backgroundColor = "#000";
|
|
176
|
+
this.enableFluidSize();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
init() {
|
|
180
|
+
super.init();
|
|
181
|
+
|
|
182
|
+
this.attractor = Attractors.halvorsen;
|
|
183
|
+
console.log(`Attractor: ${this.attractor.name}`);
|
|
184
|
+
console.log(`Equations:`, this.attractor.equations);
|
|
185
|
+
|
|
186
|
+
this.stepFn = this.attractor.createStepper();
|
|
187
|
+
|
|
188
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
189
|
+
const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
190
|
+
this.zoom = initialZoom;
|
|
191
|
+
this.targetZoom = initialZoom;
|
|
192
|
+
this.defaultZoom = initialZoom;
|
|
193
|
+
|
|
194
|
+
this.camera = new Camera3D({
|
|
195
|
+
perspective: CONFIG.camera.perspective,
|
|
196
|
+
rotationX: CONFIG.camera.rotationX,
|
|
197
|
+
rotationY: CONFIG.camera.rotationY,
|
|
198
|
+
inertia: CONFIG.camera.inertia,
|
|
199
|
+
friction: CONFIG.camera.friction,
|
|
200
|
+
clampX: CONFIG.camera.clampX,
|
|
201
|
+
});
|
|
202
|
+
this.camera.enableMouseControl(this.canvas);
|
|
203
|
+
|
|
204
|
+
this.gesture = new Gesture(this.canvas, {
|
|
205
|
+
onZoom: (delta) => {
|
|
206
|
+
this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
|
|
207
|
+
},
|
|
208
|
+
onPan: null,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
this.canvas.addEventListener("dblclick", () => {
|
|
212
|
+
this.targetZoom = this.defaultZoom;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Log camera params and barycenter on mouse release
|
|
216
|
+
this.canvas.addEventListener("mouseup", () => {
|
|
217
|
+
console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
|
|
218
|
+
let sumX = 0, sumY = 0, sumZ = 0, count = 0;
|
|
219
|
+
for (const p of this.particles) {
|
|
220
|
+
sumX += p.position.x;
|
|
221
|
+
sumY += p.position.y;
|
|
222
|
+
sumZ += p.position.z;
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
console.log(`Barycenter: x: ${(sumX/count).toFixed(3)}, y: ${(sumY/count).toFixed(3)}, z: ${(sumZ/count).toFixed(3)}`);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.particles = [];
|
|
229
|
+
for (let i = 0; i < CONFIG.particles.count; i++) {
|
|
230
|
+
this.particles.push(
|
|
231
|
+
new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
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, CONFIG.particles.spawnRange);
|
|
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 * (minHue - maxHue);
|
|
300
|
+
const hue = (baseHue + hueOffset + 360) % 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 * (minHue - maxHue);
|
|
351
|
+
const hue = (baseHue + hueOffset + 360) % 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 HalvorsenDemo(canvas);
|
|
404
|
+
demo.start();
|
|
405
|
+
});
|
package/dist/js/isometric.js
CHANGED
|
@@ -99,12 +99,17 @@ class IsometricBox extends GameObject {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Custom depth value for sorting - uses front corner for proper overlap
|
|
102
|
+
* Custom depth value for sorting - uses rotated front corner for proper overlap at all camera angles
|
|
103
103
|
*/
|
|
104
104
|
get isoDepth() {
|
|
105
|
-
// Use the
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// Use the scene's helper to get depth based on rotated coordinates
|
|
106
|
+
const corners = [
|
|
107
|
+
{ x: this.x, y: this.y },
|
|
108
|
+
{ x: this.x + this.w, y: this.y },
|
|
109
|
+
{ x: this.x, y: this.y + this.d },
|
|
110
|
+
{ x: this.x + this.w, y: this.y + this.d },
|
|
111
|
+
];
|
|
112
|
+
return this.isoScene.getRotatedDepth(corners, this.h);
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
/**
|
|
@@ -150,15 +155,7 @@ class IsometricBox extends GameObject {
|
|
|
150
155
|
render() {
|
|
151
156
|
const scene = this.isoScene;
|
|
152
157
|
|
|
153
|
-
// Get
|
|
154
|
-
const cameraAngle = scene.camera ? scene.camera.angle : 0;
|
|
155
|
-
|
|
156
|
-
// Camera view direction (where camera is looking TOWARD)
|
|
157
|
-
// In isometric, default view looks toward +X +Y direction (angle π/4 from +X axis)
|
|
158
|
-
// Camera rotation rotates around Z axis
|
|
159
|
-
const viewDirection = Math.PI / 4 + cameraAngle;
|
|
160
|
-
|
|
161
|
-
// Get all 8 corners of the box
|
|
158
|
+
// Get all 8 corners of the box (camera rotation is applied inside toIsometric)
|
|
162
159
|
const topNW = scene.toIsometric(this.x, this.y, this.h);
|
|
163
160
|
const topNE = scene.toIsometric(this.x + this.w, this.y, this.h);
|
|
164
161
|
const topSE = scene.toIsometric(this.x + this.w, this.y + this.d, this.h);
|
|
@@ -192,21 +189,11 @@ class IsometricBox extends GameObject {
|
|
|
192
189
|
}
|
|
193
190
|
];
|
|
194
191
|
|
|
195
|
-
// Calculate
|
|
192
|
+
// Calculate screen-space center Y for depth sorting, and lighting for shading
|
|
196
193
|
for (const face of faces) {
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// Face visibility: a face is visible if its rotated normal
|
|
201
|
-
// has a component pointing toward the camera (away from view direction)
|
|
202
|
-
// In isometric, visible faces are those facing generally toward -Y screen direction
|
|
203
|
-
const normalToView = rotatedNormal - viewDirection;
|
|
204
|
-
face.facingCamera = Math.cos(normalToView) < 0;
|
|
205
|
-
|
|
206
|
-
// For depth sorting: faces with normals pointing more toward +Y screen
|
|
207
|
-
// (into the screen in isometric) should be drawn first
|
|
208
|
-
face.depth = Math.sin(rotatedNormal);
|
|
209
|
-
|
|
194
|
+
// Screen Y for depth sorting (lower Y = further back = draw first)
|
|
195
|
+
face.screenY = face.verts.reduce((sum, v) => sum + v.y, 0) / 4;
|
|
196
|
+
|
|
210
197
|
// Lighting: based on angle between world-space normal and light
|
|
211
198
|
const lightDiff = face.normalAngle - lightAngle;
|
|
212
199
|
const lightFactor = (Math.cos(lightDiff) + 1) / 2; // 0 to 1
|
|
@@ -214,16 +201,9 @@ class IsometricBox extends GameObject {
|
|
|
214
201
|
face.color = this.shadeColor(this.baseColor, shadeFactor);
|
|
215
202
|
}
|
|
216
203
|
|
|
217
|
-
// Sort faces
|
|
218
|
-
//
|
|
219
|
-
faces.sort((a, b) =>
|
|
220
|
-
// Back-facing faces drawn first
|
|
221
|
-
if (a.facingCamera !== b.facingCamera) {
|
|
222
|
-
return a.facingCamera ? 1 : -1;
|
|
223
|
-
}
|
|
224
|
-
// Then by depth (lower depth = further back = draw first)
|
|
225
|
-
return a.depth - b.depth;
|
|
226
|
-
});
|
|
204
|
+
// Sort all faces by screen Y (back to front: lower Y drawn first)
|
|
205
|
+
// This is the correct approach - no visibility culling needed
|
|
206
|
+
faces.sort((a, b) => a.screenY - b.screenY);
|
|
227
207
|
|
|
228
208
|
// Draw faces in order: back faces first (with strokes), then front faces (fill covers back strokes)
|
|
229
209
|
Painter.useCtx((ctx) => {
|
|
@@ -364,22 +344,30 @@ class Ball extends GameObject {
|
|
|
364
344
|
}
|
|
365
345
|
|
|
366
346
|
/**
|
|
367
|
-
* Custom depth value for sorting - ensures ball renders on top of platforms
|
|
347
|
+
* Custom depth value for sorting - ensures ball renders on top of platforms.
|
|
348
|
+
* Uses rotated coordinates for correct sorting at all camera angles.
|
|
368
349
|
*/
|
|
369
350
|
get isoDepth() {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
351
|
+
const angle = this.isoScene.camera ? this.isoScene.camera.angle : 0;
|
|
352
|
+
const cos = Math.cos(angle);
|
|
353
|
+
const sin = Math.sin(angle);
|
|
354
|
+
|
|
355
|
+
// Rotate ball position
|
|
356
|
+
const rotatedBallX = this.x * cos - this.y * sin;
|
|
357
|
+
const rotatedBallY = this.x * sin + this.y * cos;
|
|
358
|
+
let baseDepth = rotatedBallX + rotatedBallY;
|
|
359
|
+
|
|
360
|
+
// Find the platform we're over and use its rotated front corner
|
|
373
361
|
for (const platform of this.platforms) {
|
|
374
362
|
if (platform.containsPoint(this.x, this.y, 0)) {
|
|
375
|
-
//
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
378
|
-
baseDepth =
|
|
363
|
+
// Get platform's rotated depth (max corner)
|
|
364
|
+
const platformDepth = platform.isoDepth - platform.h * 0.01; // Remove height factor
|
|
365
|
+
if (platformDepth > baseDepth) {
|
|
366
|
+
baseDepth = platformDepth;
|
|
379
367
|
}
|
|
380
368
|
}
|
|
381
369
|
}
|
|
382
|
-
|
|
370
|
+
|
|
383
371
|
// Add height plus small offset to ensure we render on top of platforms
|
|
384
372
|
return baseDepth + this.z * 0.5 + 1;
|
|
385
373
|
}
|