Divide

The calligraphic stroke of a many-nibbed rastrum connects iterative subdivisions.

Loading...
import helpers from "/scratchpad/_lib/line.asset.mjs"
import sequence from "/scratchpad/_lib/sequence.asset.mjs"
import posterize from "/scratchpad/posterize/posterize.asset.mjs"
import badge from "/scratchpad/_lib/badge.asset.mjs"
import palette from "/scratchpad/_lib/palette.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 buffer = canvas.width * 0.04
  const w = canvas.width - buffer * 2
  const h = canvas.height - buffer * 2

  let iterations = random.integer(6, 12)
  let divisions = [0.5, 0.8]

  let x = buffer
  let y = buffer

  let rects = [{ w, h, x, y }]
  let spline

  return sequence([
    () => {
      ctx.fillStyle = palette.canvas
      ctx.beginPath()
      ctx.rect(0, 0, canvas.width, canvas.height)
      ctx.fill()
    },

    // Generate grid
    () => {
      for (let i = 0; i < iterations; i++) {
        let next = []

        rects.forEach((rect) => {
          let div = random.sample(divisions)
          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.push(r1)
          next.push(r2)
        })

        rects = next
      }
    },

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

      ctx.beginPath()
      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 spline
    () => {
      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, 32 - iterations).slice(1, -1)
      spline = helpers.simplify(spline, 0.5, true)
    },

    // Draw spline
    () => {
      if (!debug) return

      ctx.lineWidth = 10

      ctx.beginPath()
      spline.forEach(({ x, y }) => {
        ctx.lineTo(x, y)
      })
      ctx.stroke()
    },

    () => {
      ctx.strokeStyle = palette.dark
      ctx.fillStyle = palette.canvas
      ctx.lineCap = "round"
      ctx.lineJoin = "round"

      const l = Math.min(...spline.map((p) => p.x))
      const r = Math.min(...spline.map((p) => canvas.width - p.x))

      const t = Math.min(...spline.map((p) => p.y))
      const b = Math.min(...spline.map((p) => canvas.height - p.y))

      ctx.save()
      ctx.translate((r - l) / 2, (b - t) / 2)

      return sequence(
        spline.slice(0, -1).map((e, i) => () => {
          ctx.lineWidth = 8
          let bf = ctx.lineWidth * 16

          bf = Math.min(Math.max(bf, buffer / 30), buffer)

          const [p1, p2] = spline.slice(i, i + 2)

          ctx.beginPath()
          ctx.moveTo(p1.x - bf / 2, p1.y - bf / 2)
          ctx.lineTo(p2.x - bf / 2, p2.y - bf / 2)
          ctx.lineTo(p2.x + bf / 2, p2.y + bf / 2)
          ctx.lineTo(p1.x + bf / 2, p1.y + bf / 2)

          ctx.fill()

          const n = ctx.lineWidth * 2
          const n1 = bf / -(2 * n)
          const n2 = bf / (2 * n)

          for (let j = n1; j < n2; j++) {
            ctx.beginPath()
            ctx.moveTo(p1.x + j * n, p1.y + j * n)
            ctx.lineTo(p2.x + j * n, p2.y + j * n)
            ctx.lineWidth = 1 + 7 * (1 - (j * n) / bf)
            ctx.stroke()
          }
        })
      )
    },

    () => {
      ctx.restore()

      posterize({
        ctx,
        radius: 5,
        ramp: 10,
        overshoot: 0.5,
      })
    },

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