Skip to content

Commit 82d237a

Browse files
committed
feat: Implement 2.5D Volumetric Projection for ServicesCanvas (#8713)
1 parent a82bdd8 commit 82d237a

1 file changed

Lines changed: 103 additions & 23 deletions

File tree

apps/portal/canvas/ServicesCanvas.mjs

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class ServicesCanvas extends Base {
9292
superHexes = []
9393
scale = 1
9494
time = 0
95+
rotation = {x: -0.4, y: 0} // Base tilt (radians) - Floor Perspective
9596

9697
clearGraph() {
9798
let me = this;
@@ -119,20 +120,30 @@ class ServicesCanvas extends Base {
119120
this.theme = value
120121
}
121122

122-
drawHex(ctx, x, y, size) {
123+
drawHex(ctx, x, y, z, size, projection) {
123124
ctx.beginPath();
125+
let first = true;
126+
124127
for (let i = 0; i < 6; i++) {
125128
const angle_deg = 60 * i + 30;
126129
const angle_rad = Math.PI / 180 * angle_deg;
130+
127131
const px = x + size * Math.cos(angle_rad);
128132
const py = y + size * Math.sin(angle_rad);
129-
if (i === 0) ctx.moveTo(px, py);
130-
else ctx.lineTo(px, py);
133+
134+
let p = projection.project(px, py, z);
135+
136+
if (first) {
137+
ctx.moveTo(p.x, p.y);
138+
first = false;
139+
} else {
140+
ctx.lineTo(p.x, p.y);
141+
}
131142
}
132143
ctx.closePath();
133144
}
134145

135-
drawKernel(ctx, width, height) {
146+
drawKernel(ctx, width, height, projection) {
136147
let me = this;
137148
if (!me.kernelBuffer) return;
138149

@@ -154,12 +165,12 @@ class ServicesCanvas extends Base {
154165
for (let i = 0; i < count; i++) {
155166
let x = buffer[i * 2] + panX,
156167
y = buffer[i * 2 + 1] + panY;
157-
me.drawHex(ctx, x, y, size);
168+
me.drawHex(ctx, x, y, 400, size, projection);
158169
}
159170
ctx.stroke();
160171
}
161172

162-
drawGraph(ctx, width, height) {
173+
drawGraph(ctx, width, height, projection) {
163174
let me = this;
164175

165176
if (!me.cellBuffer) return;
@@ -187,7 +198,7 @@ class ServicesCanvas extends Base {
187198

188199
if (energy <= 0.01 && scale > 0.1) {
189200
let size = baseSize * 0.95 * scale;
190-
me.drawHex(ctx, x, y, size);
201+
me.drawHex(ctx, x, y, 0, size, projection);
191202
}
192203
}
193204
ctx.stroke();
@@ -206,7 +217,7 @@ class ServicesCanvas extends Base {
206217

207218
if (progress > 0) {
208219
ctx.beginPath();
209-
me.drawHex(ctx, x, y, baseSize * 2.5 * progress);
220+
me.drawHex(ctx, x, y, 0, baseSize * 2.5 * progress, projection);
210221

211222
ctx.strokeStyle = HIGHLIGHT;
212223
ctx.globalAlpha = 0.3 * progress;
@@ -215,8 +226,10 @@ class ServicesCanvas extends Base {
215226
ctx.fillStyle = themeColors.superHex;
216227
ctx.fill();
217228

229+
let p = projection.project(x, y, 0);
230+
218231
ctx.beginPath();
219-
ctx.arc(x, y, 4 * s * progress, 0, Math.PI * 2);
232+
ctx.arc(p.x, p.y, 4 * s * progress * p.scale, 0, Math.PI * 2);
220233
ctx.fillStyle = HIGHLIGHT;
221234
ctx.globalAlpha = 0.8 * progress;
222235
ctx.fill();
@@ -237,7 +250,7 @@ class ServicesCanvas extends Base {
237250
let currentSize = baseSize * (0.95 + (energy * 0.1));
238251

239252
ctx.beginPath();
240-
me.drawHex(ctx, x, y, currentSize);
253+
me.drawHex(ctx, x, y, 0, currentSize, projection);
241254

242255
ctx.fillStyle = themeColors.hexActive;
243256
ctx.globalAlpha = energy * 0.4;
@@ -254,7 +267,7 @@ class ServicesCanvas extends Base {
254267
}
255268
}
256269

257-
drawRunners(ctx) {
270+
drawRunners(ctx, projection) {
258271
let me = this;
259272
if (!me.runnerBuffer) return;
260273

@@ -286,16 +299,21 @@ class ServicesCanvas extends Base {
286299
let tailX = x - dirX * tailLen,
287300
tailY = y - dirY * tailLen;
288301

289-
let g = ctx.createLinearGradient(tailX, tailY, x, y);
302+
let p1 = projection.project(tailX, tailY, 0);
303+
let p2 = projection.project(x, y, 0);
304+
305+
if (!p1.visible || !p2.visible) continue;
306+
307+
let g = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
290308
g.addColorStop(0, 'rgba(0,0,0,0)');
291309
g.addColorStop(0.5, PRIMARY);
292310
g.addColorStop(1, '#FFFFFF');
293311

294312
ctx.beginPath();
295313
ctx.strokeStyle = g;
296-
ctx.lineWidth = 3 * s;
297-
ctx.moveTo(tailX, tailY);
298-
ctx.lineTo(x, y);
314+
ctx.lineWidth = 3 * s * p2.scale; // Scale thickness
315+
ctx.moveTo(p1.x, p1.y);
316+
ctx.lineTo(p2.x, p2.y);
299317
ctx.stroke();
300318
}
301319
}
@@ -304,7 +322,7 @@ class ServicesCanvas extends Base {
304322
/**
305323
* Draws particle debris (squares).
306324
*/
307-
drawDebris(ctx) {
325+
drawDebris(ctx, projection) {
308326
let me = this;
309327
if (!me.debrisBuffer) return;
310328

@@ -325,9 +343,12 @@ class ServicesCanvas extends Base {
325343
y = buffer[idx + 1],
326344
size = 3 * s * life; // Shrink as it dies
327345

328-
ctx.globalAlpha = life;
329-
// Draw Square (Data Bit)
330-
ctx.fillRect(x - size/2, y - size/2, size, size);
346+
let p = projection.project(x, y, 0);
347+
if (p.visible) {
348+
let scaledSize = size * p.scale;
349+
ctx.globalAlpha = life;
350+
ctx.fillRect(p.x - scaledSize/2, p.y - scaledSize/2, scaledSize, scaledSize);
351+
}
331352
}
332353
}
333354
ctx.globalAlpha = 1;
@@ -570,6 +591,7 @@ class ServicesCanvas extends Base {
570591
if (!me.debrisBuffer) me.initDebris();
571592

572593
me.updatePhysics(width, height);
594+
me.updateRotation(width, height);
573595
me.updateSuperHexes(width, height);
574596
me.updateRunners(width, height);
575597
me.updateDebris();
@@ -581,10 +603,12 @@ class ServicesCanvas extends Base {
581603
ctx.fillRect(0, 0, width, height)
582604
}
583605

584-
me.drawKernel(ctx, width, height);
585-
me.drawGraph(ctx, width, height);
586-
me.drawRunners(ctx);
587-
me.drawDebris(ctx); // Render particles on top
606+
let projection = me.getProjection(width, height);
607+
608+
me.drawKernel(ctx, width, height, projection);
609+
me.drawGraph(ctx, width, height, projection);
610+
me.drawRunners(ctx, projection);
611+
me.drawDebris(ctx, projection); // Render particles on top
588612

589613
if (hasRaf) {
590614
requestAnimationFrame(me.renderLoop)
@@ -832,6 +856,62 @@ class ServicesCanvas extends Base {
832856
}
833857
}
834858

859+
updateRotation(width, height) {
860+
let me = this,
861+
mx = me.mouse.x,
862+
my = me.mouse.y,
863+
tx = -0.4, // Base tilt X (pitch)
864+
ty = 0; // Base yaw
865+
866+
if (mx !== -1000) {
867+
// Map mouse X to Yaw (+/- 0.2 rad)
868+
ty = ((mx / width) - 0.5) * 0.4;
869+
// Map mouse Y to Pitch (-0.6 to -0.2 rad)
870+
tx = -0.4 + ((my / height) - 0.5) * 0.4;
871+
}
872+
873+
// Smooth interpolate
874+
me.rotation.x += (tx - me.rotation.x) * 0.05;
875+
me.rotation.y += (ty - me.rotation.y) * 0.05;
876+
}
877+
878+
getProjection(width, height) {
879+
let me = this,
880+
fov = 1000,
881+
cx = width / 2,
882+
cy = height / 2,
883+
cosX = Math.cos(me.rotation.x),
884+
sinX = Math.sin(me.rotation.x),
885+
cosY = Math.cos(me.rotation.y),
886+
sinY = Math.sin(me.rotation.y);
887+
888+
return {
889+
project(x, y, z) {
890+
let dx = x - cx,
891+
dy = y - cy,
892+
dz = z;
893+
894+
// Rotate Y
895+
let x1 = dx * cosY - dz * sinY;
896+
let z1 = dz * cosY + dx * sinY;
897+
898+
// Rotate X
899+
let y2 = dy * cosX - z1 * sinX;
900+
let z2 = z1 * cosX + dy * sinX;
901+
902+
// Project
903+
let scale = fov / (fov + z2);
904+
905+
return {
906+
x: x1 * scale + cx,
907+
y: y2 * scale + cy,
908+
scale: scale,
909+
visible: z2 > -fov // Clip if behind camera
910+
};
911+
}
912+
};
913+
}
914+
835915
updateResources(width, height) {
836916
let me = this,
837917
ctx = me.context;

0 commit comments

Comments
 (0)