Files
walkersim/src/renderer.js

213 lines
4.8 KiB
JavaScript

import { mechanismBounds } from './geometry.js';
import { radToDeg } from './math.js';
const COLORS = {
bg: '#0f1116',
grid: '#1a2230',
floor: '#3a465d',
body: '#ea0000',
link: '#000000',
crank: '#00ff00',
pivot: '#ffff00',
traceNear: '#6fd2ff',
traceFar: '#b483ff',
};
function drawLine(ctx, a, b, style = '#fff', width = 2) {
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = style;
ctx.lineWidth = width;
ctx.stroke();
}
function drawCircle(ctx, p, r, fill, stroke = null, lineWidth = 1) {
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
}
function drawPolyline(ctx, pts, style, width = 1.5, alpha = 1) {
if (!pts || pts.length < 2) return;
ctx.save();
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i += 1) {
ctx.lineTo(pts[i].x, pts[i].y);
}
ctx.strokeStyle = style;
ctx.lineWidth = width;
ctx.stroke();
ctx.restore();
}
function worldToScreenFactory(canvas) {
const b = mechanismBounds();
const worldW = b.maxX - b.minX;
const worldH = b.maxY - b.minY;
const pad = 30;
const scale = Math.min(
(canvas.width - pad * 2) / worldW,
(canvas.height - pad * 2) / worldH,
);
// Rotate full scene by 180° in screen space (flip X and Y)
// so the platform orientation matches the expected floor-facing legs.
const ox = (canvas.width - worldW * scale) * 0.5 + b.maxX * scale;
const oy = (canvas.height - worldH * scale) * 0.5 - b.minY * scale;
return {
scale,
toScreen(p) {
return {
x: ox - p.x * scale,
y: oy + p.y * scale,
};
},
};
}
function drawGrid(ctx, canvas) {
const step = 40;
ctx.save();
ctx.strokeStyle = COLORS.grid;
ctx.lineWidth = 1;
for (let x = 0; x <= canvas.width; x += step) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y <= canvas.height; y += step) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
ctx.restore();
}
function drawFloor(ctx, canvas, tf, floorY) {
if (!Number.isFinite(floorY)) return;
const y = tf.toScreen({ x: 0, y: floorY }).y;
ctx.save();
ctx.strokeStyle = COLORS.floor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
ctx.restore();
}
function drawSide(ctx, tf, side, opacity, thickness = 2.5) {
if (!side) return;
ctx.save();
ctx.globalAlpha = opacity;
const A = tf.toScreen(side.A);
const B = tf.toScreen(side.B);
const O = tf.toScreen(side.O);
const C = tf.toScreen(side.C);
const D = tf.toScreen(side.D);
const E = tf.toScreen(side.E);
const F = tf.toScreen(side.F);
const G = tf.toScreen(side.G);
// Body
drawLine(ctx, A, B, COLORS.body, thickness);
// Crank
drawCircle(ctx, O, 7.5 * tf.scale / 8, COLORS.crank, '#000', 1);
drawLine(ctx, O, C, COLORS.crank, thickness);
// Links
drawLine(ctx, A, D, COLORS.link, thickness);
drawLine(ctx, C, D, COLORS.link, thickness);
drawLine(ctx, C, F, COLORS.link, thickness);
drawLine(ctx, B, E, COLORS.link, thickness);
drawLine(ctx, C, E, COLORS.link, thickness);
drawLine(ctx, C, G, COLORS.link, thickness);
// Pivots
for (const p of [A, B, C, D, E]) {
drawCircle(ctx, p, 4, COLORS.pivot);
}
ctx.restore();
}
function drawHUD(ctx, data) {
ctx.save();
ctx.fillStyle = '#dfe7f6';
ctx.font = '13px ui-monospace, SFMono-Regular, Menlo, monospace';
const tDeg = radToDeg(data.theta).toFixed(1);
const p = (data.theta / (Math.PI * 2)).toFixed(3);
ctx.fillText(`theta: ${tDeg} deg`, 16, 24);
ctx.fillText(`phase: ${p}`, 16, 42);
ctx.restore();
}
export function renderScene(canvas, state) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = COLORS.bg;
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGrid(ctx, canvas);
const tf = worldToScreenFactory(canvas);
drawFloor(ctx, canvas, tf, state.ground?.floorY ?? NaN);
if (state.showTrace) {
drawPolyline(
ctx,
state.trace.nearF.points.map(tf.toScreen),
COLORS.traceNear,
1.8,
0.9,
);
drawPolyline(
ctx,
state.trace.nearG.points.map(tf.toScreen),
COLORS.traceNear,
1.8,
0.5,
);
drawPolyline(
ctx,
state.trace.farF.points.map(tf.toScreen),
COLORS.traceFar,
1.4,
0.7,
);
drawPolyline(
ctx,
state.trace.farG.points.map(tf.toScreen),
COLORS.traceFar,
1.4,
0.4,
);
}
if (state.showFar) {
drawSide(ctx, tf, state.pose.far, 0.5, 2);
}
if (state.showNear) {
drawSide(ctx, tf, state.pose.near, 1, 2.6);
}
drawHUD(ctx, state.pose);
}