Gestural

Layers of broad, painterly strokes.

Loading...
import helpers from "/scratchpad/_lib/line.asset.mjs"
import sequence from "/scratchpad/_lib/sequence.asset.mjs"
import badge from "/scratchpad/_lib/badge.asset.mjs"
import palette from "/scratchpad/_lib/palette.asset.mjs"
import posterize from "/scratchpad/posterize/posterize.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 colors = [palette.canvas, palette.dark]

  const buffer = canvas.width * 0.03
  const w = canvas.width - buffer * 2
  const h = canvas.height - buffer * 2

  const iterations = random.integer(3, 20)
  const ratios = [0.5, 0.8]

  let rects
  let spline

  return sequence([
    ...Array.from({ length: iterations }, () => [
      // Generate a grid by iteratively splitting the canvas
      () => {
        const divisions = random.integer(3, 7)

        rects = [{ w, h, x: buffer, y: buffer }]

        for (let i = 0; i < divisions; i++) {
          let next = []

          rects.forEach((rect) => {
            let div = random.sample(ratios)
            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 = next.concat(random.shuffle([r1, r2]))
          })

          rects = next
          if (random.maybe()) rects.reverse()
        }
      },

      // Draw the grid
      () => {
        if (!debug) return

        ctx.beginPath()
        ctx.strokeStyle = palette.dark

        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 a spline connecting the rectangle midpoints
      () => {
        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, 80).slice(1, -1)
        spline = helpers.simplify(spline, 0.5, true)
      },

      // Shift the canvas so that the spline is centered
      () => {
        const xMin = Math.min(...spline.map((p) => p.x))
        const xMax = Math.min(...spline.map((p) => canvas.width - p.x))

        const yMin = Math.min(...spline.map((p) => p.y))
        const yMax = Math.min(...spline.map((p) => canvas.height - p.y))

        ctx.save()
        ctx.translate((xMax - xMin) / 2, (yMax - yMin) / 2)
      },

      // Draw spline
      () => {
        ctx.lineCap = "round"
        ctx.lineJoin = "round"

        const lineBase = random.integer(3, 8)
        const lineWide = random.integer(6, 40)

        const min = 10
        const max = 60
        const len = random.integer(min, max)

        colors.forEach((clr, c) => {
          for (let i = 0; i < len; i++) {
            const f = Math.sin(Math.PI * (i / len))

            ctx.beginPath()

            spline.slice(0, -1).forEach(({ x, y }, j) => {
              if (j % len === i) {
                ctx.moveTo(x, y)
                ctx.lineTo(spline[j + 1].x, spline[j + 1].y)
              }
            })

            ctx.lineWidth = (2 - c) * (lineBase + f * lineWide)
            ctx.strokeStyle = clr
            ctx.stroke()
          }
        })
      },

      // Undo the translation
      () => {
        ctx.restore()
      },
    ]).flat(),

    () => {
      posterize({
        ctx,
        colors,
        radius: 4,
        ramp: 50,
        overshoot: 0.2,
        iterations: 8,
      })
    },

    () => badge(ctx, { colors }),
  ])
}