function pointInInterval(a, b, x) { return a <= x && x <= b; } function intervalIntersectsInterval(a, b, c, d) { if (c <= a) { return a <= d; } return pointInInterval(a, b, c); } function rectIntersectRect(x1, y1, x2, y2, x3, y3, x4, y4) { return intervalIntersectsInterval(x1, x2, x3, x4) && intervalIntersectsInterval(y1, y2, y3, y4); } function intersects(x1, y1, x2, y2, x3, y3, x4, y4) { const a_minx = Math.min(x1, x2); const a_maxx = Math.max(x1, x2); const a_miny = Math.min(y1, y2); const a_maxy = Math.max(y1, y2); const b_minx = Math.min(x3, x4); const b_maxx = Math.max(x3, x4); const b_miny = Math.min(y3, y4); const b_maxy = Math.max(y3, y4); if (!rectIntersectRect(a_minx, a_miny, a_maxx, a_maxy, b_minx, b_miny, b_maxx, b_maxy)) return false; const a_dx = x2 - x1; const a_dy = y2 - y1; const b_dx = x4 - x3; const b_dy = y4 - y3; const det = -b_dx * a_dy + a_dx * b_dy; if (Math.abs(det) === 0 && -a_dy * (x3 - x1) + a_dx * (y3 - y1) === 0) { return y1 <= y3 <= y2 || y1 <= y4 <= y2; } const s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / det; const t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / det; return s >= 0 && s <= 1 && t >= 0 && t <= 1; } const $canvas = document.querySelector('#circuit-pattern'); let g = $canvas.getContext('2d'); let WIDTH = $canvas.offsetWidth; let HEIGHT = $canvas.offsetHeight; const TS = 20; let ROWS = HEIGHT / TS; let COLS = WIDTH / TS; function updateCanvasDimensions() { g = $canvas.getContext('2d'); WIDTH = $canvas.offsetWidth; HEIGHT = $canvas.offsetHeight; ROWS = HEIGHT / TS; COLS = WIDTH / TS; $canvas.width = WIDTH * devicePixelRatio; $canvas.height = HEIGHT * devicePixelRatio; g.scale(devicePixelRatio, devicePixelRatio); } window.addEventListener('resize', () => updateCanvasDimensions()); updateCanvasDimensions(); const state = { wires: [], // :: [(Int, Int)] nextWire: null, nextWireIndex: 0, nextSegmentLen: 0, t: 0, }; function randomFloat(min, max) { return Math.random() * (max - min) + min; } function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); } function randomChoice(list) { return list[randomInt(0, list.length - 1)]; } const DIRS = { left: [-1, 1], center: [0, 1], right: [+1, 1], }; const NEXT_DIRS = { left: ['center'], center: ['left', 'right'], right: ['center'], }; function canPlaceSegment([[p1x, p1y], [p2x, p2y]]) { return state.wires.every(wire => { return everyWireSegment(wire, ([[sx, sy], [lx, ly]]) => { return !intersects(sx, sy, lx, ly, p1x, p1y, p2x, p2y); }); }) } function everyWireSegment(wire, segmentFn) { let [start, ...rest] = wire; return rest.every((pt) => { const r = segmentFn([start, pt]); start = pt; return r; }); } function generateWire() { const pieceCount = Math.pow(randomFloat(1, 2.5), 2) | 0; const startPoint = [ randomInt(0, COLS), Math.pow(randomFloat(0, ROWS / 12), 2) | 0, ]; const sizes = Array(pieceCount).fill(0).map(() => randomInt(2, 4) | 0); const dirs = [randomChoice(Object.keys(DIRS))]; for (let i = 0; i < pieceCount - 1; i++) { const last = dirs[dirs.length - 1]; dirs.push(randomChoice(NEXT_DIRS[last])); } const wire = [startPoint]; for (let i = 0; i < pieceCount; i++) { const [lpx, lpy] = wire[wire.length - 1]; const [dx, dy] = DIRS[dirs[i]]; const s = sizes[i]; const s_bonus = (dirs[i] === 'center' ? s * 2 : s) | 0; wire.push([lpx + dx * s_bonus, lpy + dy * s_bonus]); } const ok = everyWireSegment(wire, segment => { return canPlaceSegment(segment); }); if (!ok) return null; return wire; } // // LinearintERPolation function lerp(a, b, t) { return t * (b - a) + a; } function lerpPoint([x1, y1], [x2, y2], t) { return [lerp(x1, x2, t), lerp(y1, y2, t)]; } const CELLS_PER_SECOND = 15; let lastTime = new Date().getTime(); function update() { const now = new Date().getTime(); const delta = (now - lastTime) / 1000; lastTime = now; if (state.nextWire) { state.t += CELLS_PER_SECOND * delta; if (state.t >= 1.0) { const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex); const dx = Math.abs(x2 - x1); const dy = Math.abs(y2 - y1); state.t = 0; state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy); state.nextWireIndex++; } if (state.nextWireIndex > state.nextWire.length) { state.wires.push(state.nextWire); state.nextWire = null; } } else { const wire = generateWire(); if (wire) { state.nextWire = wire; state.nextWireIndex = 2; const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex); const dx = Math.abs(x2 - x1); const dy = Math.abs(y2 - y1); state.t = 0; state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy); } } } function getColors() { if (document.body.classList.contains('dark-mode')) { return { BACKGROUND: '#282828', CIRCUIT: '#38302e', } } else { return { BACKGROUND: '#eaeaea', CIRCUIT: '#d4d4d4', }; } } function render() { const { BACKGROUND, CIRCUIT } = getColors(); g.clearRect(0, 0, WIDTH, HEIGHT); g.strokeStyle = CIRCUIT; g.lineWidth = 3; state.wires.forEach(wire => { const [[sx, sy], ...rest] = wire; function renderDot(x, y) { if (y === 0) { return; } g.beginPath(); g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2); g.stroke(); } renderDot(sx, sy); g.beginPath(); g.moveTo(sx * TS, sy * TS); rest.forEach(([x, y]) => { g.lineTo(x * TS, y * TS); }); g.stroke(); const [lx, ly] = wire[wire.length - 1]; renderDot(lx, ly); [wire[0], wire[wire.length - 1]].forEach(([x, y]) => { if (y === 0) { return; } g.fillStyle = BACKGROUND; g.beginPath(); g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2); g.fill(); }); }); if (state.nextWire) { const wirePart = state.nextWire.slice(0, state.nextWireIndex); const wire = [ ...wirePart.slice(0, wirePart.length - 1), lerpPoint( wirePart[wirePart.length - 2], wirePart[wirePart.length - 1], state.t ) ]; const [[sx, sy], ...rest] = wire; function renderDot(x, y) { if (y === 0) { return; } g.beginPath(); g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2); g.stroke(); } renderDot(sx, sy); g.beginPath(); g.moveTo(sx * TS, sy * TS); rest.forEach(([x, y]) => { g.lineTo(x * TS, y * TS); }); g.stroke(); const [lx, ly] = wire[wire.length - 1]; renderDot(lx, ly); [wire[0], wire[wire.length - 1]].forEach(([x, y]) => { if (y === 0) { return; } g.fillStyle = BACKGROUND; g.beginPath(); g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2); g.fill(); }); } requestAnimationFrame(render); } // g.translate(0.5, 0.5); window.addEventListener('DOMContentLoaded', () => { // $canvas.classList.remove('hide'); render(); let i = 20; while (i > 0) { const wire = generateWire(); if (wire) { state.wires.push(wire); i--; } } setInterval(() => { update(); }, 1000 / 30); })