Layered waves of turbulent dips and crests.
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
// Each signal is a function that calculates a value at time `t`
// from an array of stacked sine waves of varying frequency (f),
// phase (φ), and amplitude (A).
const makeSignal = () => {
const len = random.integer(3, 8)
const amp = 0.85 / len
const waves = Array.from({ length: len }, () => ({
f: τ * random.number(2, 10),
A: random.wobble(amp, amp / 10),
φ: random.wobble(τ),
}))
return (t, limit = waves.length) =>
waves
.slice(0, limit)
.map(({ f, A, φ }) => A * Math.sin(f * t + φ))
.reduce((a, b) => a + b, 0)
}
// Create a path object bounding the signal wave
const makeOutline = (signal, w, h) => {
const path = new Path2D()
const steps = w / 3
const extra = 2.5
path.moveTo(0, h * extra)
for (let i = 0; i < steps; i++) {
const t = i / (steps - 1)
const x = t * w
const y = h * signal(t)
path.lineTo(x, y)
}
path.lineTo(w, h * extra)
path.closePath()
return path
}
// Create a path object hatching the signal wave
const makeHatches = (signal, w, h) => {
const width = random.number(8, 16)
const steps = w / (width * 2.2)
const dy = random.number(0.5, 0.8) * h
const f = random.number(2, 7)
const φ = random.wobble(π)
const A = h / 5
const dx = (t) => A * Math.sin(τ * f * t + φ)
const path = new Path2D()
for (let i = 0; i < steps; i++) {
const t = i / (steps - 1)
const y = h * signal(t)
const fx = (h * signal(t, -1)) / 1.5
const x0 = t * w
const y0 = y + dy
const x2 = x0 + dx(t)
const y2 = y0 + h * 3
const x1 = x0 + (x2 - x0) / 2 + fx
const y1 = y0 + (y2 - y0) / 2
path.moveTo(x0, y0)
path.quadraticCurveTo(x1, y1, x2, y2)
}
path.lineWidth = width
return path
}
export default async (canvas) => {
canvas.width = canvas.offsetWidth * dpi
canvas.height = canvas.offsetHeight * dpi
const ctx = canvas.getContext("2d")
const rows = random.integer(7, 14)
const inset = random.number(15, 40)
const frame = new Path2D()
frame.rect(inset, inset, canvas.width - inset * 2, canvas.height - inset * 2)
return sequence([
() => {
ctx.lineCap = "round"
ctx.strokeStyle = palette.dark
ctx.fillStyle = palette.canvas
ctx.lineWidth = inset / 2
ctx.stroke(frame)
ctx.save()
ctx.clip(frame)
},
Array.from({ length: rows }, (n, i) => () => {
const t = (i - 1) / (rows - 2)
const w = canvas.width
const h = canvas.height / rows
const x = 0
const y = canvas.height * random.wobble(t, 0.03)
const signal = makeSignal()
const outline = makeOutline(signal, w, h)
const hatches = makeHatches(signal, w, h)
ctx.save()
ctx.translate(x, y)
ctx.lineWidth = random.number(6, 18)
ctx.fill(outline)
ctx.stroke(outline)
ctx.save()
ctx.clip(outline)
ctx.lineWidth = hatches.lineWidth
ctx.stroke(hatches)
ctx.restore()
ctx.restore()
}),
() => {
ctx.restore()
ctx.lineWidth = inset / 2
ctx.stroke(frame)
},
() => {
posterize({
ctx,
iterations: 8,
radius: 2,
ramp: 12,
})
},
])
}