Recursive backtracking through a grid in three directions.
Get occasional updates about new fonts, designs and other interesting things.
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,
})
},
])
}