Circuit

Recursive backtracking through a grid in three directions.

Loading...
import sequence from "/scratchpad/_lib/sequence.asset.mjs"
import palette from "/scratchpad/_lib/palette.asset.mjs"
import posterize from "/scratchpad/posterize/posterize.asset.mjs"
import * as random from "/scratchpad/_lib/random.asset.mjs"

const dpi = window.devicePixelRatio || 1
const debug = false

const last = (arr) => arr[arr.length - 1]

const moves = {
  N: { r: -1, c: +1 },
  E: { r: +0, c: +1 },
  S: { r: +1, c: -1 },
  W: { r: +0, c: -1 },
  X: { r: -1, c: +0 },
  Y: { r: +1, c: +0 },
}

// Array of direction keys
const directions = Object.keys(moves)

const move = ({ r, c }, dir) => {
  const offset = moves[dir]
  return { r: r + offset.r, c: c + offset.c }
}

const getAvailableCells = (cell, cells) => {
  return directions.reduce((m, dir) => {
    const q = move(cell, dir)
    const p = cells.find((p) => p.r === q.r && p.c === q.c)

    if (p) m.push(p)

    return m
  }, [])
}

export default async (canvas) => {
  canvas.width = canvas.offsetWidth * dpi
  canvas.height = canvas.offsetHeight * dpi

  const ctx = canvas.getContext("2d")

  const chanceToBranch = random.number(0.05, 0.15)

  let cells = []

  const cellWidth = random.integer(20, 28) * dpi
  const cellHeight = random.integer(16, 24) * dpi

  const cols = Math.floor(canvas.width / cellWidth) - 4
  const rows = Math.floor(canvas.height / cellHeight) - 4

  let offset = [
    0.5 * (canvas.width - (cols - 1) * cellWidth),
    0.5 * (canvas.height - (rows - 1) * cellHeight),
  ]

  const lineWidth = cellHeight * 0.2

  return sequence([
    // Fill canvas
    () => {
      ctx.fillStyle = palette.dark

      ctx.beginPath()
      ctx.rect(0, 0, canvas.width, canvas.height)
      ctx.fill()

      ctx.lineCap = "round"
      ctx.lineJoin = "round"
    },

    // Create cells
    () => {
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          cells.push({
            r,
            c,
            i: cells.length + 1,
          })
        }
      }

      cells.forEach((p) => {
        p.x = offset[0] + p.c * cellWidth
        p.y = offset[1] + p.r * cellHeight
      })
    },

    // Label cells
    () => {
      if (!debug) return

      const fs = 6 * dpi
      ctx.font = `${fs}px sans`
      ctx.textAlign = "center"
      ctx.fillStyle = palette.canvas

      cells.forEach(({ i, x, y }) => ctx.fillText(i, x, y + fs * 0.4))
    },

    // Draw grid
    () => {
      ctx.beginPath()

      cells.forEach((cell) => {
        ctx.moveTo(cell.x + cellWidth * 0, cell.y - cellHeight / 2)
        ctx.lineTo(cell.x + cellWidth * 1, cell.y - cellHeight / 2)

        ctx.moveTo(cell.x + cellWidth * 1, cell.y - cellHeight * 0.5)
        ctx.lineTo(cell.x + cellWidth * 0, cell.y + cellHeight * 0.5)

        if (cell.c <= 0) {
          ctx.moveTo(cell.x - cellWidth * 0, cell.y - cellHeight * 0.5)
          ctx.lineTo(cell.x - cellWidth * 1, cell.y + cellHeight * 0.5)
        }

        if (cell.c <= 0 || cell.r === rows - 1) {
          ctx.moveTo(cell.x - cellWidth * 1, cell.y + cellHeight / 2)
          ctx.lineTo(cell.x - cellWidth * 0, cell.y + cellHeight / 2)
        }
      })

      ctx.lineWidth = lineWidth / 2
      ctx.strokeStyle = palette.canvas

      ctx.stroke()
    },

    () => {
      const stack = [cells[0]]
      const remaining = [...cells]

      let running = true

      ctx.beginPath()

      const promise = new Promise((res) => {
        const explore = () => {
          if (!running) return

          let cell

          if (random.maybe(chanceToBranch)) {
            cell = random.sample(stack.slice(0, -1)) || stack[0]
          } else {
            cell = last(stack)
          }

          const available = getAvailableCells(cell, remaining)

          if (available.length) {
            const next = random.sample(available)

            ctx.moveTo(cell.x, cell.y)
            ctx.lineTo(next.x, next.y)

            // Remove from remaining available tiles
            remaining.splice(remaining.indexOf(next), 1)

            stack.push(next)
          } else {
            stack.splice(stack.indexOf(cell), 1)
          }

          if (stack.length > 0) {
            explore()
          } else {
            res()
          }
        }

        explore()
      }).then(() => {
        ctx.stroke()
      })

      return {
        promise,
        cancel: () => {
          running = false
        },
      }
    },

    () => {
      ctx.lineWidth = lineWidth * 2
      ctx.strokeStyle = palette.dark
      ctx.stroke()

      ctx.lineWidth = lineWidth
      ctx.strokeStyle = palette.canvas
      ctx.stroke()
    },

    () => {
      posterize({
        ctx,
        radius: 5,
        ramp: 40,
        overshoot: 0.75,
      })
    },
  ])
}