Primitive, two-tone Twombly scrawls.
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 posterize from "/scratchpad/posterize/posterize.asset.mjs"
const dpi = window.devicePixelRatio
const π = Math.PI
const τ = π * 2
const stroke = (ctx, loopFactor) => {
const canvas = ctx.canvas
const buffer = canvas.width * 0.04
const distance = canvas.width - buffer * 2
const steps = canvas.width / 3
const rBase = canvas.height / 24
const rVary = rBase / 10
const rMin = rBase - rBase / 4
const rMax = rBase + rBase / 4
let loops = random.integer(
2 * loopFactor,
((0.8 * canvas.width) / rBase / 8) * loopFactor
)
if (random.maybe(0.06)) loops *= random.number(1, 2)
if (random.maybe(0.06)) loops *= random.number(1, 2)
let rHor = random.wobble(rBase, rBase / 30)
let rVer = random.wobble(rBase, rBase / 30)
let sigma = random.wobble(π / 2)
const sigmaChange = random.wobble((π / 90) * 0.2)
const lwVar = 2
const lwMin = 8
const lwMax = 32
let lineWidth = random.integer(lwMin, lwMax)
const fFreq = random.integer(3, 12)
const fBase = random.wobble(0.2, 0.1)
const fSigma = random.wobble(π / 2)
const dxDist = random.wobble(rBase / 5)
const dxFreq = random.integer(1, 80)
const dxSigma = random.wobble(π / 2)
const position = {}
ctx.save()
ctx.translate(buffer, rVer / 2)
Array.from({ length: steps }, (n, i) => {
const t = i / (steps - 1)
const f = Math.sin(π * t * fFreq + fSigma) * fBase + (1 - fBase)
const dy = (0.5 - f) * rVer * 0.5
rHor = random.wobble(rHor, rVary, rMin, rMax)
rVer = random.wobble(rVer, rVary, rMin, rMax)
sigma += sigmaChange * t
lineWidth = random.wobble(lineWidth, lwVar, lwMin, lwMax)
const theta = π + -τ * (loops + 0.5) * t
const cx = Math.cos(theta) * rHor * f
const cy = Math.sin(theta + sigma) * rVer * f
const dx = t * distance + Math.sin(π * t * dxFreq + dxSigma) * dxDist
const x = cx + dx
const y = cy + dy
if (i > 0) {
ctx.beginPath()
ctx.moveTo(position.x, position.y)
ctx.lineTo(x, y)
ctx.lineWidth = lineWidth
ctx.lineCap = "round"
ctx.strokeStyle = palette.dark
ctx.stroke()
// Paint drop
if (random.maybe(0.003) && loops > 10) {
const py = random.number((rBase / 20) * lineWidth)
const pr = lineWidth / 8
ctx.beginPath()
ctx.lineWidth = lineWidth / 2
ctx.moveTo(x, y)
ctx.lineTo(x, y + py)
ctx.moveTo(x + pr, y + py)
ctx.arc(x, y + py, pr, 0, π, 0)
ctx.stroke()
}
}
position.y = y
position.x = x
})
ctx.restore()
}
export default async (canvas) => {
canvas.width = canvas.offsetWidth * dpi
canvas.height = canvas.offsetHeight * dpi
const ctx = canvas.getContext("2d")
const rows = random.integer(3, 12)
const loopFactor = random.integer(1, 8)
return sequence([
() => {
ctx.strokeStyle = palette.dark
ctx.fillStyle = palette.canvas
ctx.fillRect(0, 0, canvas.width, canvas.height)
},
Array.from({ length: rows }, (n, i) => () => {
const t = (i + 1) / (rows + 1.1)
const o = 0.04
ctx.save()
ctx.translate(0, canvas.height * t)
ctx.translate(random.wobble(canvas.width * o), 0)
if (i > 0 && i < rows - 1) {
ctx.translate(0, random.wobble(canvas.height * 0.03))
}
ctx.translate(canvas.width / 2, 0)
ctx.scale(random.wobble(0.9, o), 1)
ctx.translate(-canvas.width / 2, 0)
stroke(ctx, loopFactor)
ctx.restore()
}),
() => {
posterize({
ctx,
radius: 8,
ramp: 40,
background: palette.canvas,
})
},
])
}