Fields of intersecting hatches.
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"
import posterize from "/scratchpad/posterize/posterize.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(60, 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)
})
},
() => {
const freq = random.number(1, 4)
const amp = random.number(4, 12)
grid.forEach((p) => {
const tc = -0.5 + p.col / (colCount - 1)
const tr = -0.5 + p.row / (rowCount - 1)
p.x += Math.cos(freq * Math.PI * tr) * cellSize * amp
p.y += Math.sin(freq * Math.PI * tc) * cellSize * amp
})
},
// 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.lineCap = "round"
ctx.lineJoin = "round"
ctx.lineWidth = cellSize / 5
ctx.strokeStyle = palette.dark
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.save()
ctx.beginPath()
const rect = new Path2D()
const hatchLength = cellSize * 2
const hatchOffset = ctx.lineWidth * 2
rect.rect(buffer, buffer, width, height)
ctx.stroke(rect)
ctx.clip(rect)
ctx.beginPath()
const matrix = Array.from({ length: colCount }, () => [])
grid.forEach((p) => {
matrix[p.col][p.row] = p
})
boxes.forEach(([c, r, w, h]) => {
const t = random.wobble(w < h ? t1 : t2, Math.PI / 3)
const p1 = matrix[c][r]
const p2 = matrix[c + w][r]
const p3 = matrix[c + w][r + h]
const p4 = matrix[c][r + h]
const center = [p1.x + (p3.x - p1.x) / 2, p1.y + (p3.y - p1.y) / 2]
const path = new Path2D()
let hasOverlap
for (let i = 0; i < w; i++) {
const p = matrix[c + i][r]
path.lineTo(p.x, p.y)
if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
}
for (let i = 0; i < h; i++) {
const p = matrix[c + w][r + i]
path.lineTo(p.x, p.y)
if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
}
for (let i = w; i > 0; i--) {
const p = matrix[c + i][r + h]
path.lineTo(p.x, p.y)
if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
}
for (let i = h; i > 0; i--) {
const p = matrix[c][r + i]
path.lineTo(p.x, p.y)
if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
}
if (!hasOverlap) return
path.closePath()
ctx.save()
ctx.clip(path)
ctx.beginPath()
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 rows = radius / hatchLength
const cols = radius / hatchOffset
for (let i = -rows; i < rows; i++) {
const dx = center[0] + Math.cos(t) * (i * hatchLength)
const dy = center[1] + Math.sin(t) * (i * hatchLength)
for (let j = -cols + random.number(1); j < cols; j++) {
const x =
dx +
Math.cos(-Math.PI / 2 + t) *
(j * hatchOffset + random.wobble(ctx.lineWidth))
const y =
dy +
Math.sin(-Math.PI / 2 + t) *
(j * hatchOffset + random.wobble(ctx.lineWidth))
const r = (hatchLength - ctx.lineWidth) * random.number(0.4, 0.5)
const ti = t + random.wobble(Math.PI / 20)
const x1 = x + Math.cos(ti) * r
const x2 = x - Math.cos(ti) * r
const y1 = y + Math.sin(ti) * r
const y2 = y - Math.sin(ti) * r
if (
(ctx.isPointInPath(path, x, y) &&
ctx.isPointInPath(rect, x, y)) ||
(ctx.isPointInPath(path, x1, y1) &&
ctx.isPointInPath(rect, x1, y1)) ||
(ctx.isPointInPath(path, x2, y2) &&
ctx.isPointInPath(rect, x2, y2))
) {
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
}
}
}
ctx.stroke()
ctx.restore()
})
},
() =>
posterize({
ctx,
radius: ctx.lineWidth,
ramp: ctx.lineWidth * 3,
}),
])
}