Layers of broad, painterly strokes.
Get occasional updates about new fonts, designs and other interesting things.
import helpers from "/scratchpad/_lib/line.asset.mjs"
import sequence from "/scratchpad/_lib/sequence.asset.mjs"
import badge from "/scratchpad/_lib/badge.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
export default (canvas) => {
const scale = dpi
canvas.width = canvas.offsetWidth * scale
canvas.height = canvas.offsetHeight * scale
const ctx = canvas.getContext("2d")
const colors = [palette.canvas, palette.dark]
const buffer = canvas.width * 0.03
const w = canvas.width - buffer * 2
const h = canvas.height - buffer * 2
const iterations = random.integer(3, 20)
const ratios = [0.5, 0.8]
let rects
let spline
return sequence([
Array.from({ length: iterations }, () => [
// Generate a grid by iteratively splitting the canvas
() => {
const divisions = random.integer(3, 7)
rects = [{ w, h, x: buffer, y: buffer }]
for (let i = 0; i < divisions; i++) {
let next = []
rects.forEach((rect) => {
let div = random.sample(ratios)
let r1 = { rect }
let r2 = { rect }
if (rect.w / rect.h > 4 / 3) {
r1.w *= div
r2.x += div * rect.w
r2.w -= r1.w
} else {
r1.h *= div
r2.y += div * rect.h
r2.h -= r1.h
}
next = next.concat(random.shuffle([r1, r2]))
})
rects = next
if (random.maybe()) rects.reverse()
}
},
// Draw the grid
() => {
if (!debug) return
ctx.beginPath()
ctx.strokeStyle = palette.dark
rects.forEach(({ x, y, w, h }) => {
ctx.rect(x, y, w, h)
ctx.moveTo(x, y)
ctx.lineTo(x + w, y + h)
ctx.moveTo(x + w, y)
ctx.lineTo(x, y + h)
})
ctx.stroke()
},
// Generate a spline connecting the rectangle midpoints
() => {
let line = rects.slice(0, -1).map(({ x, y, w, h }, i) => ({
x: x + w / 2,
y: y + h / 2,
}))
line.unshift({
x: line[0].x - (line[1].x - line[0].x),
y: line[0].y - (line[1].y - line[0].y),
})
line.push({
x: line.slice(-2)[0].x + (line.slice(-2)[1].x - line.slice(-2)[0].x),
y: line.slice(-2)[0].y + (line.slice(-2)[1].y - line.slice(-2)[0].y),
})
spline = helpers.spline(line, 80).slice(1, -1)
spline = helpers.simplify(spline, 0.5, true)
},
// Shift the canvas so that the spline is centered
() => {
const xMin = Math.min(spline.map((p) => p.x))
const xMax = Math.min(spline.map((p) => canvas.width - p.x))
const yMin = Math.min(spline.map((p) => p.y))
const yMax = Math.min(spline.map((p) => canvas.height - p.y))
ctx.save()
ctx.translate((xMax - xMin) / 2, (yMax - yMin) / 2)
},
// Draw spline
() => {
ctx.lineCap = "round"
ctx.lineJoin = "round"
const lineBase = random.integer(3, 8)
const lineWide = random.integer(6, 40)
const min = 10
const max = 60
const len = random.integer(min, max)
colors.forEach((clr, c) => {
for (let i = 0; i < len; i++) {
const f = Math.sin(Math.PI * (i / len))
ctx.beginPath()
spline.slice(0, -1).forEach(({ x, y }, j) => {
if (j % len === i) {
ctx.moveTo(x, y)
ctx.lineTo(spline[j + 1].x, spline[j + 1].y)
}
})
ctx.lineWidth = (2 - c) * (lineBase + f * lineWide)
ctx.strokeStyle = clr
ctx.stroke()
}
})
},
// Undo the translation
() => {
ctx.restore()
},
]).flat(),
() => {
posterize({
ctx,
colors,
radius: 4,
ramp: 50,
overshoot: 0.2,
iterations: 8,
})
},
() => badge(ctx, { colors }),
])
}