# Mixed Signals

Layered waves of turbulent dips and crests.

``````import * as random from "/scratchpad/_lib/random.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.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,