const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')! interface Grid extends Iterable<[number, number, T]> { has(point: [number, number]): boolean get(point: [number, number]): T set(point: [number, number], value: T): void } function createGrid(): Grid { const cells: Record = {} return { has([x, y]) { return cells[`${x},${y}`] !== undefined }, get([x, y]) { return cells[`${x},${y}`] }, set([x, y], value) { cells[`${x},${y}`] = value }, *[Symbol.iterator]() { for (const [coord, value] of Object.entries(cells)) { const [x, y] = coord.split(',').map(s => parseInt(s)) yield [x, y, value] } }, } } type WireCell = 'down' | 'down-left' | 'down-right' | 'dot' type WireDirection = 'down' | 'down-left' | 'down-right' type WireSteps = { position: Point2; direction: WireCell }[] type Point2 = [number, number] type State = { grid: Grid queuedWire: { index: number steps: WireSteps } | null badTries: 0 } type Renderer = { timer: number } let renderer: Renderer | null = null const RENDERER_FPS = 30 function setup() { console.log('Setting up circuits art...') $canvas.width = $canvas.clientWidth $canvas.height = $canvas.clientHeight const g = $canvas.getContext('2d')! const state: State = { grid: createGrid(), queuedWire: null, badTries: 0, } const startTime = new Date() const handle = setInterval(() => { const time = new Date().getTime() - startTime.getTime() update(state, g.canvas.width, g.canvas.height, time) render(g, state, time) }, 1000 / RENDERER_FPS) renderer = { timer: handle } } function update(state: State, width: number, height: number, time: number) { const w = (width / CELL_SIZE) | 0 // const h = (height / CELL_SIZE) | 0 if (state.badTries > 1000) { console.log('finished') clearInterval(renderer!.timer) } if (!state.queuedWire) { const sx = randomInt(0, w) if (!state.grid.has([sx, 0])) { const steps = generateWire(state.grid, [sx, 0]) if (steps.length < 7) { state.badTries++ return } state.queuedWire = { index: 0, steps, } state.grid.set(state.queuedWire.steps[0].position, 'dot') return } state.badTries++ } else { const wire = state.queuedWire const step = wire.steps[wire.index] state.grid.set(step.position, step.direction) if (wire.index + 1 < wire.steps.length) { state.grid.set(wire.steps[wire.index + 1].position, 'dot') } wire.index++ if (wire.index >= wire.steps.length) { state.queuedWire = null } } } const CELL_SIZE = 27 const BACKGROUND_COLOR = '#ecffe3' const WIRE_COLOR = '#a6ce94' const RENDER_CELL: Record void> = { 'down': g => { g.strokeStyle = WIRE_COLOR g.beginPath() g.moveTo(0, 0) g.lineTo(0, 1) g.stroke() }, 'down-left': g => { g.strokeStyle = WIRE_COLOR g.beginPath() g.moveTo(0, 0) g.lineTo(-1, 1) g.stroke() }, 'down-right': g => { g.strokeStyle = WIRE_COLOR g.beginPath() g.moveTo(0, 0) g.lineTo(+1, 1) g.stroke() }, 'dot': g => { g.fillStyle = BACKGROUND_COLOR g.beginPath() g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI) g.fill() g.strokeStyle = WIRE_COLOR g.beginPath() g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI) g.stroke() }, } function render(g: CanvasRenderingContext2D, state: State, time: number) { g.clearRect(0, 0, g.canvas.width, g.canvas.height) g.resetTransform() g.scale(CELL_SIZE, CELL_SIZE) g.lineWidth = 3 / CELL_SIZE const w = (g.canvas.width / CELL_SIZE) | 0 const h = (g.canvas.height / CELL_SIZE) | 0 for (let y = 0; y <= h + 1; y++) { for (let x = 0; x <= w + 1; x++) { if (!state.grid.has([x, y])) continue const cell = state.grid.get([x, y]) g.save() g.translate(x, y) // g.fillStyle = '#f008' // g.beginPath() // g.rect(-0.25, -0.25, 0.5, 0.5) // g.fill() RENDER_CELL[cell](g) g.restore() } } // if (state.queuedWire) { // for (const step of state.queuedWire.steps) { // const [x, y] = step.position // g.fillStyle = '#00f8' // g.save() // g.translate(x, y) // g.beginPath() // g.rect(-0.25, -0.25, 0.5, 0.5) // g.fill() // g.restore() // } // } // const [mx, my] = state.mouse // g.save() // g.fillStyle = '#0008' // g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE)) // g.beginPath() // g.rect(0, 0, 1, 1) // g.fill() // g.restore() } setup() window.addEventListener('resize', () => { if (renderer) { clearInterval(renderer.timer) } setup() }) function randomInt(from: number, to: number) { return Math.floor(Math.random() * (to - from + 1)) } function randomChoice(choices: T[]): T { return choices[randomInt(0, choices.length - 1)] } function randomWeightedChoice(choices: [T, number][]) { return } // 3 + 4 + 5 + 6 randomWeightedChoice([ ['a', 3], ['b', 4], ['c', 5], ['d', 6], ]) const DIR_TO_VEC: Record = { ['down']: [0, 1], ['down-left']: [-1, 1], ['down-right']: [+1, 1], } type ShortCircuitBoolean = boolean | (() => boolean) const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v()) const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b) /** * Tells whether a given direction is not blocked by some other cell in a given grid and starting position */ const DIR_AVAILABLE_PREDICATE: Record) => boolean> = { ['down']: ([x, y], grid) => !grid.has([x, y + 1]) && implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') && implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'), ['down-left']: ([x, y], grid) => !grid.has([x - 1, y + 1]) && implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'), ['down-right']: ([x, y], grid) => !grid.has([x + 1, y + 1]) && implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'), } function pruneDirections(grid: Grid, position: Point2, directions: WireDirection[]): WireDirection[] { return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid)) } function generateWire(grid: Grid, startingPoint: Point2): { position: Point2; direction: WireCell }[] { const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30 let currentPosition = startingPoint let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right']) const steps: { position: Point2; direction: WireCell }[] = [] for (let i = 0; i < segmentLength; i++) { const availableDirections = pruneDirections(grid, currentPosition, ['down', 'down-left', 'down-right']) if (availableDirections.length === 0) { break } else { const dir = availableDirections.includes(currentDirection) && Math.random() < 0.25 ? currentDirection : randomChoice(availableDirections) if ((currentDirection === 'down-left' && dir === 'down-right') || (currentDirection === 'down-right' && dir === 'down-left')) { break } const [x, y] = currentPosition const [dx, dy] = DIR_TO_VEC[dir] steps.push({ position: [x, y], direction: dir, }) currentPosition = [x + dx, y + dy] currentDirection = dir } } const last = steps.at(-1) if (last) { last.direction = 'dot' } return steps }