The calligraphic stroke of a many-nibbed rastrum connects iterative subdivisions.
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 posterize from "/scratchpad/posterize/posterize.asset.mjs"
import badge from "/scratchpad/_lib/badge.asset.mjs"
import palette from "/scratchpad/_lib/palette.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 buffer = canvas.width * 0.04
const w = canvas.width - buffer * 2
const h = canvas.height - buffer * 2
let iterations = random.integer(6, 12)
let divisions = [0.5, 0.8]
let x = buffer
let y = buffer
let rects = [{ w, h, x, y }]
let spline
return sequence([
() => {
ctx.fillStyle = palette.canvas
ctx.beginPath()
ctx.rect(0, 0, canvas.width, canvas.height)
ctx.fill()
},
// Generate grid
() => {
for (let i = 0; i < iterations; i++) {
let next = []
rects.forEach((rect) => {
let div = random.sample(divisions)
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.push(r1)
next.push(r2)
})
rects = next
}
},
// Draw grid
() => {
if (!debug) return
ctx.beginPath()
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 spline
() => {
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, 32 - iterations).slice(1, -1)
spline = helpers.simplify(spline, 0.5, true)
},
// Draw spline
() => {
if (!debug) return
ctx.lineWidth = 10
ctx.beginPath()
spline.forEach(({ x, y }) => {
ctx.lineTo(x, y)
})
ctx.stroke()
},
() => {
ctx.strokeStyle = palette.dark
ctx.fillStyle = palette.canvas
ctx.lineCap = "round"
ctx.lineJoin = "round"
const l = Math.min(spline.map((p) => p.x))
const r = Math.min(spline.map((p) => canvas.width - p.x))
const t = Math.min(spline.map((p) => p.y))
const b = Math.min(spline.map((p) => canvas.height - p.y))
ctx.save()
ctx.translate((r - l) / 2, (b - t) / 2)
return sequence(
spline.slice(0, -1).map((e, i) => () => {
ctx.lineWidth = 8
let bf = ctx.lineWidth * 16
bf = Math.min(Math.max(bf, buffer / 30), buffer)
const [p1, p2] = spline.slice(i, i + 2)
ctx.beginPath()
ctx.moveTo(p1.x - bf / 2, p1.y - bf / 2)
ctx.lineTo(p2.x - bf / 2, p2.y - bf / 2)
ctx.lineTo(p2.x + bf / 2, p2.y + bf / 2)
ctx.lineTo(p1.x + bf / 2, p1.y + bf / 2)
ctx.fill()
const n = ctx.lineWidth * 2
const n1 = bf / -(2 * n)
const n2 = bf / (2 * n)
for (let j = n1; j < n2; j++) {
ctx.beginPath()
ctx.moveTo(p1.x + j * n, p1.y + j * n)
ctx.lineTo(p2.x + j * n, p2.y + j * n)
ctx.lineWidth = 1 + 7 * (1 - (j * n) / bf)
ctx.stroke()
}
})
)
},
() => {
ctx.restore()
posterize({
ctx,
radius: 5,
ramp: 10,
overshoot: 0.5,
})
},
() => badge(ctx),
])
}