Spin Cycle

Primitive, two-tone Twombly scrawls.

Loading...
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,
      })
    },
  ])
}