Split monolithic index.html into: - js/constants.js - gate config, colors, dimensions - js/state.js - shared application state - js/gates.js - evaluation logic, port geometry - js/renderer.js - canvas drawing - js/waveform.js - GTKWave-style signal viewer - js/simulation.js - clock tick engine - js/events.js - mouse, keyboard, UI handlers - js/app.js - entry point - css/style.css - all styles
189 lines
5.7 KiB
JavaScript
189 lines
5.7 KiB
JavaScript
// Canvas rendering — gates, connections, grid
|
|
import { GATE_W, GATE_H, PORT_R, GATE_COLORS } from './constants.js';
|
|
import { state } from './state.js';
|
|
import { getInputPorts, getOutputPorts } from './gates.js';
|
|
import { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js';
|
|
|
|
let canvas, ctx;
|
|
|
|
export function initRenderer() {
|
|
canvas = document.getElementById('canvas');
|
|
ctx = canvas.getContext('2d');
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
requestAnimationFrame(draw);
|
|
}
|
|
|
|
export function resize() {
|
|
canvas.width = window.innerWidth;
|
|
const waveH = state.waveformVisible ? state.waveformHeight : 0;
|
|
canvas.height = window.innerHeight - 48 - waveH;
|
|
}
|
|
|
|
function drawGate(gate) {
|
|
const color = GATE_COLORS[gate.type];
|
|
const isHovered = state.hoveredGate === gate;
|
|
const isActive = gate.value === 1;
|
|
|
|
if (isActive) {
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = 20;
|
|
}
|
|
|
|
ctx.fillStyle = isActive ? color + '22' : '#14141e';
|
|
ctx.strokeStyle = isHovered ? '#fff' : color;
|
|
ctx.lineWidth = isHovered ? 2.5 : 1.5;
|
|
|
|
ctx.beginPath();
|
|
ctx.roundRect(gate.x, gate.y, GATE_W, GATE_H, 8);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.shadowBlur = 0;
|
|
|
|
// Label
|
|
ctx.fillStyle = isActive ? '#fff' : color;
|
|
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
|
|
const isIOType = gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK';
|
|
ctx.fillText(
|
|
gate.type === 'CLOCK' ? '⏱ CLK' : gate.type,
|
|
gate.x + GATE_W / 2,
|
|
gate.y + GATE_H / 2 - (isIOType ? 8 : 0)
|
|
);
|
|
|
|
// Small ID label
|
|
ctx.font = '9px monospace';
|
|
ctx.fillStyle = '#444';
|
|
ctx.fillText(getGateLabel(gate), gate.x + GATE_W / 2, gate.y + GATE_H - 6);
|
|
|
|
// Value for INPUT/OUTPUT/CLOCK
|
|
if (isIOType) {
|
|
ctx.font = 'bold 16px monospace';
|
|
ctx.fillStyle = gate.value ? '#00ff88' : '#ff4444';
|
|
ctx.fillText(gate.value ? '1' : '0', gate.x + GATE_W / 2, gate.y + GATE_H / 2 + 10);
|
|
}
|
|
|
|
// Input ports
|
|
getInputPorts(gate).forEach(p => {
|
|
const isPortHovered = state.hoveredPort &&
|
|
state.hoveredPort.gate === gate &&
|
|
state.hoveredPort.index === p.index &&
|
|
state.hoveredPort.type === 'input';
|
|
const conn = state.connections.find(c => c.to === gate.id && c.toPort === p.index);
|
|
const portActive = conn ? state.gates.find(g => g.id === conn.from)?.value : 0;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
|
ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e');
|
|
ctx.fill();
|
|
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
|
ctx.lineWidth = 1.5;
|
|
ctx.stroke();
|
|
});
|
|
|
|
// Output ports
|
|
getOutputPorts(gate).forEach(p => {
|
|
const isPortHovered = state.hoveredPort &&
|
|
state.hoveredPort.gate === gate &&
|
|
state.hoveredPort.index === p.index &&
|
|
state.hoveredPort.type === 'output';
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
|
ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e');
|
|
ctx.fill();
|
|
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
|
ctx.lineWidth = 1.5;
|
|
ctx.stroke();
|
|
});
|
|
}
|
|
|
|
function drawConnection(conn) {
|
|
const fromGate = state.gates.find(g => g.id === conn.from);
|
|
const toGate = state.gates.find(g => g.id === conn.to);
|
|
if (!fromGate || !toGate) return;
|
|
|
|
const fromPort = getOutputPorts(fromGate)[conn.fromPort];
|
|
const toPort = getInputPorts(toGate)[conn.toPort];
|
|
if (!fromPort || !toPort) return;
|
|
|
|
const active = fromGate.value === 1;
|
|
const midX = (fromPort.x + toPort.x) / 2;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(fromPort.x, fromPort.y);
|
|
ctx.bezierCurveTo(midX, fromPort.y, midX, toPort.y, toPort.x, toPort.y);
|
|
ctx.strokeStyle = active ? '#00ff88' : '#333';
|
|
ctx.lineWidth = active ? 2.5 : 1.5;
|
|
ctx.stroke();
|
|
|
|
if (active) {
|
|
ctx.strokeStyle = '#00ff8844';
|
|
ctx.lineWidth = 6;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function drawGrid() {
|
|
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();
|
|
}
|
|
for (let y = 0; y < canvas.height; y += 40) {
|
|
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function drawConnectingWire() {
|
|
if (!state.connecting) return;
|
|
const gate = state.connecting.gate;
|
|
const port = state.connecting.portType === 'output'
|
|
? getOutputPorts(gate)[state.connecting.portIndex]
|
|
: getInputPorts(gate)[state.connecting.portIndex];
|
|
if (!port) return;
|
|
|
|
const midX = (port.x + state.mouseX) / 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(port.x, port.y);
|
|
ctx.bezierCurveTo(midX, port.y, midX, state.mouseY, state.mouseX, state.mouseY);
|
|
ctx.strokeStyle = '#00e59988';
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([6, 4]);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
|
|
function drawPlacingGhost() {
|
|
if (!state.placingGate) return;
|
|
ctx.globalAlpha = 0.5;
|
|
const ghost = {
|
|
x: state.mouseX - GATE_W / 2,
|
|
y: state.mouseY - GATE_H / 2,
|
|
type: state.placingGate,
|
|
value: 0,
|
|
id: -1
|
|
};
|
|
drawGate(ghost);
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
function draw() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
drawGrid();
|
|
state.connections.forEach(drawConnection);
|
|
state.gates.forEach(drawGate);
|
|
drawConnectingWire();
|
|
drawPlacingGhost();
|
|
|
|
if (state.waveformVisible) {
|
|
drawWaveLabels();
|
|
drawWaveforms();
|
|
}
|
|
|
|
requestAnimationFrame(draw);
|
|
}
|