Skewed grids of angular striations.
Get occasional updates about new fonts, designs and other interesting things.
import * as random from "/scratchpad/_lib/random.asset.mjs"
import sequence from "/scratchpad/_lib/sequence.asset.mjs"
import palette from "/scratchpad/_lib/palette.asset.mjs"
import helpers from "/scratchpad/_lib/line.asset.mjs"
const dpi = window.devicePixelRatio
export default async (canvas) => {
canvas.width = canvas.offsetWidth * dpi
canvas.height = canvas.offsetHeight * dpi
const ctx = canvas.getContext("2d")
const lines = []
const boxes = []
const t1 = random.wobble(Math.PI / 4)
const t2 = random.wobble(Math.PI / 2, Math.PI / 4)
const buffer = Math.min(canvas.width, canvas.height) * 0.05
const cellSize = Math.min(canvas.width, canvas.height) / random.number(10, 80)
const width = canvas.width - buffer * 2
const height = canvas.height - buffer * 2
let grid, colCount, rowCount
return sequence([
() => {
const padX = Math.cos(t1) * height
const padY = Math.sin(t2) * width
const gridW = width + padX
const gridH = height + padY
const dx = buffer - padX / 2
const dy = buffer - padY / 2
colCount = Math.floor(gridW / cellSize)
rowCount = Math.floor(gridH / cellSize)
grid = helpers.makeGrid(gridW, gridH, cellSize)
grid.forEach((p) => {
p.x += dx + Math.cos(t2) * ((p.row - rowCount / 2) * cellSize)
p.y += dy + Math.sin(t1) * ((p.col - colCount / 2) * cellSize)
})
},
// Show orientation
() => {
ctx.beginPath()
ctx.moveTo(100, 100)
ctx.lineTo(100 + Math.cos(t1) * 100, 100 + Math.sin(t1) * 100)
ctx.moveTo(100, 100)
ctx.lineTo(100 + Math.cos(t2) * 100, 100 + Math.sin(t2) * 100)
ctx.stroke()
},
// Draw grid
() => {
const r = 2
ctx.beginPath()
grid.forEach((p, i) => {
ctx.moveTo(p.x + r, p.y)
ctx.arc(p.x, p.y, r, 0, Math.PI * 2)
})
ctx.fill()
},
// Draw bounding boxes
() => {
ctx.strokeStyle = palette.dark
const minX = Math.min(grid.map((p) => p.x))
const maxX = Math.max(grid.map((p) => p.x))
const minY = Math.min(grid.map((p) => p.y))
const maxY = Math.max(grid.map((p) => p.y))
ctx.beginPath()
ctx.rect(minX, minY, maxX - minX, maxY - minY)
ctx.rect(buffer, buffer, width, height)
ctx.stroke()
},
// Construct horizontals
() => {
const minInset = 1
const minLength = 1
const maxLength = Math.floor(colCount * 0.6)
lines.push([0, 0, colCount - 1, 0])
for (let row = 1; row < rowCount - 1; row++) {
let c1, c2
if (random.maybe(0.3)) continue
while (true) {
c1 = random.integer(
(c2 || 0) + minInset,
colCount - minInset - minLength
)
c2 = Math.min(
random.integer(c1 + minLength, colCount - minInset),
c1 + maxLength
)
lines.push([c1, row, c2, row])
if (c2 >= colCount - minInset - minLength) break
if (random.maybe(0.3)) break
}
}
lines.push([0, rowCount - 1, colCount - 1, rowCount - 1])
},
// Determine boxes
() => {
const lastLine = lines[lines.length - 1]
lines.slice(0, -1).forEach((line, i) => {
const startRow = line[1]
const others = lines.slice(i + 1)
let next = lastLine
let startCol
for (let endCol = line[0]; endCol <= line[2]; endCol++) {
// Find the first line beneath that overlaps the current line
const match = others.find(
(other) =>
other[0] <= endCol &&
endCol < other[2] &&
other[1] > startRow &&
((next === lastLine && endCol === line[0]) || other[1] < next[1])
)
if (
// There is a match
(match ||
// The current line is starting
endCol === line[2] ||
// The next line is starting
next[2] === endCol) &&
startCol !== undefined
) {
boxes.push([
startCol,
startRow,
endCol - startCol,
next[1] - startRow,
])
}
// If line exists for this column, set row and column
if (match) {
startCol = endCol
next = match
}
// Otherwise, reset once that line ends
else if (next[2] === endCol) {
startCol = endCol
next = others.find(
(other) =>
other[0] <= endCol && endCol < other[2] && other[1] > startRow
)
}
}
})
},
() => {
ctx.lineWidth = cellSize / 3
const offset = ctx.lineWidth * 2
const rect = new Path2D()
rect.rect(
buffer + ctx.lineWidth,
buffer + ctx.lineWidth,
width - 2 * ctx.lineWidth,
height - 2 * ctx.lineWidth
)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.save()
ctx.stroke(rect)
ctx.clip(rect)
boxes.forEach(([c, r, w, h]) => {
const t = random.wobble(w < h ? t1 : t2, Math.PI / 3)
const p1 = grid[(r + 0) * colCount + (c + 0)]
const p2 = grid[(r + 0) * colCount + (c + w)]
const p3 = grid[(r + h) * colCount + (c + w)]
const p4 = grid[(r + h) * colCount + (c + 0)]
ctx.save()
ctx.beginPath()
ctx.moveTo(p1.x, p1.y)
ctx.lineTo(p2.x, p2.y)
ctx.lineTo(p3.x, p3.y)
ctx.lineTo(p4.x, p4.y)
ctx.closePath()
ctx.stroke()
ctx.clip()
ctx.translate(p1.x + (p3.x - p1.x) / 2, p1.y + (p3.y - p1.y) / 2)
ctx.rotate(t)
const radius =
Math.sqrt(
Math.max(
Math.pow(p3.x - p1.x, 2) + Math.pow(p3.y - p1.y, 2),
Math.pow(p4.x - p2.x, 2) + Math.pow(p4.y - p2.y, 2)
)
) / 2
const lineCount = radius / offset
ctx.beginPath()
for (let i = -lineCount; i < lineCount; i++) {
ctx.moveTo(0 - radius, offset * i)
ctx.lineTo(0 + radius, offset * i)
}
ctx.stroke()
ctx.restore()
})
},
])
}