feat: CLK toggles on Step, add pan/zoom (arrows, +/-, wheel, 0=reset)

This commit is contained in:
Jose Luis Montañes
2026-03-19 22:06:03 +01:00
parent 7409a96cf1
commit d5de328898
5 changed files with 141 additions and 27 deletions

View File

@@ -20,6 +20,14 @@ export function resize() {
canvas.height = window.innerHeight - 48 - waveH;
}
// Convert screen coords to world coords (accounting for pan/zoom)
export function screenToWorld(sx, sy) {
return {
x: (sx - state.camX) / state.zoom,
y: (sy - state.camY) / state.zoom
};
}
function drawGate(gate) {
const color = GATE_COLORS[gate.type];
const isHovered = state.hoveredGate === gate;
@@ -27,12 +35,12 @@ function drawGate(gate) {
if (isActive) {
ctx.shadowColor = color;
ctx.shadowBlur = 20;
ctx.shadowBlur = 20 * state.zoom;
}
ctx.fillStyle = isActive ? color + '22' : '#14141e';
ctx.strokeStyle = isHovered ? '#fff' : color;
ctx.lineWidth = isHovered ? 2.5 : 1.5;
ctx.lineWidth = (isHovered ? 2.5 : 1.5);
ctx.beginPath();
ctx.roundRect(gate.x, gate.y, GATE_W, GATE_H, 8);
@@ -42,7 +50,7 @@ function drawGate(gate) {
// Label
ctx.fillStyle = isActive ? '#fff' : color;
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
ctx.font = `bold 14px "Segoe UI", system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
@@ -127,13 +135,22 @@ function drawConnection(conn) {
}
function drawGrid() {
const gridSize = 40;
ctx.strokeStyle = '#111118';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
// Calculate visible grid range in world coords
const topLeft = screenToWorld(0, 0);
const bottomRight = screenToWorld(canvas.width, canvas.height);
const startX = Math.floor(topLeft.x / gridSize) * gridSize;
const startY = Math.floor(topLeft.y / gridSize) * gridSize;
for (let x = startX; x < bottomRight.x; x += gridSize) {
ctx.beginPath(); ctx.moveTo(x, topLeft.y); ctx.lineTo(x, bottomRight.y); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
for (let y = startY; y < bottomRight.y; y += gridSize) {
ctx.beginPath(); ctx.moveTo(topLeft.x, y); ctx.lineTo(bottomRight.x, y); ctx.stroke();
}
}
@@ -145,10 +162,12 @@ function drawConnectingWire() {
: getInputPorts(gate)[state.connecting.portIndex];
if (!port) return;
const midX = (port.x + state.mouseX) / 2;
// Convert mouse to world coords for the wire endpoint
const world = screenToWorld(state.mouseX, state.mouseY);
const midX = (port.x + world.x) / 2;
ctx.beginPath();
ctx.moveTo(port.x, port.y);
ctx.bezierCurveTo(midX, port.y, midX, state.mouseY, state.mouseX, state.mouseY);
ctx.bezierCurveTo(midX, port.y, midX, world.y, world.x, world.y);
ctx.strokeStyle = '#00e59988';
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
@@ -159,9 +178,10 @@ function drawConnectingWire() {
function drawPlacingGhost() {
if (!state.placingGate) return;
ctx.globalAlpha = 0.5;
const world = screenToWorld(state.mouseX, state.mouseY);
const ghost = {
x: state.mouseX - GATE_W / 2,
y: state.mouseY - GATE_H / 2,
x: world.x - GATE_W / 2,
y: world.y - GATE_H / 2,
type: state.placingGate,
value: 0,
id: -1
@@ -170,15 +190,36 @@ function drawPlacingGhost() {
ctx.globalAlpha = 1;
}
function drawZoomIndicator() {
const pct = Math.round(state.zoom * 100);
ctx.save();
ctx.resetTransform();
ctx.fillStyle = '#333';
ctx.font = '11px monospace';
ctx.textAlign = 'right';
ctx.fillText(`${pct}%`, canvas.width - 10, canvas.height - 10);
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Apply camera transform
ctx.save();
ctx.translate(state.camX, state.camY);
ctx.scale(state.zoom, state.zoom);
drawGrid();
state.connections.forEach(drawConnection);
state.gates.forEach(drawGate);
drawConnectingWire();
drawPlacingGhost();
ctx.restore();
// HUD (drawn without transform)
drawZoomIndicator();
if (state.waveformVisible) {
drawWaveLabels();
drawWaveforms();