Peake

Fields of intersecting hatches.

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 helpers from "/scratchpad/_lib/line.asset.mjs"
import posterize from "/scratchpad/posterize/posterize.asset.mjs"

const dpi = window.devicePixelRatio

export default async (canvas) => {
  canvas.width = canvas.offsetWidth * dpi
  canvas.height = canvas.offsetHeight * dpi

  const ctx = canvas.getContext("2d")

  const lines = []
  const boxes = []

  const t1 = random.wobble(Math.PI / 4)
  const t2 = random.wobble(Math.PI / 2, Math.PI / 4)

  const buffer = Math.min(canvas.width, canvas.height) * 0.05
  const cellSize = Math.min(canvas.width, canvas.height) / random.number(60, 80)

  const width = canvas.width - buffer * 2
  const height = canvas.height - buffer * 2

  let grid, colCount, rowCount

  return sequence([
    () => {
      const padX = Math.cos(t1) * height
      const padY = Math.sin(t2) * width

      const gridW = width + padX
      const gridH = height + padY

      const dx = buffer - padX / 2
      const dy = buffer - padY / 2

      colCount = Math.floor(gridW / cellSize)
      rowCount = Math.floor(gridH / cellSize)

      grid = helpers.makeGrid(gridW, gridH, cellSize)

      grid.forEach((p) => {
        p.x += dx + Math.cos(t2) * ((p.row - rowCount / 2) * cellSize)
        p.y += dy + Math.sin(t1) * ((p.col - colCount / 2) * cellSize)
      })
    },

    () => {
      const freq = random.number(1, 4)
      const amp = random.number(4, 12)

      grid.forEach((p) => {
        const tc = -0.5 + p.col / (colCount - 1)
        const tr = -0.5 + p.row / (rowCount - 1)

        p.x += Math.cos(freq * Math.PI * tr) * cellSize * amp
        p.y += Math.sin(freq * Math.PI * tc) * cellSize * amp
      })
    },

    // Construct horizontals
    () => {
      const minInset = 1
      const minLength = 1
      const maxLength = Math.floor(colCount * 0.6)

      lines.push([0, 0, colCount - 1, 0])

      for (let row = 1; row < rowCount - 1; row++) {
        let c1, c2

        if (random.maybe(0.3)) continue

        while (true) {
          c1 = random.integer(
            (c2 || 0) + minInset,
            colCount - minInset - minLength
          )

          c2 = Math.min(
            random.integer(c1 + minLength, colCount - minInset),
            c1 + maxLength
          )

          lines.push([c1, row, c2, row])

          if (c2 >= colCount - minInset - minLength) break
          if (random.maybe(0.3)) break
        }
      }

      lines.push([0, rowCount - 1, colCount - 1, rowCount - 1])
    },

    // Determine boxes
    () => {
      const lastLine = lines[lines.length - 1]

      lines.slice(0, -1).forEach((line, i) => {
        const startRow = line[1]
        const others = lines.slice(i + 1)

        let next = lastLine
        let startCol

        for (let endCol = line[0]; endCol <= line[2]; endCol++) {
          // Find the first line beneath that overlaps the current line
          const match = others.find(
            (other) =>
              other[0] <= endCol &&
              endCol < other[2] &&
              other[1] > startRow &&
              ((next === lastLine && endCol === line[0]) || other[1] < next[1])
          )

          if (
            // There is a match
            (match ||
              // The current line is starting
              endCol === line[2] ||
              // The next line is starting
              next[2] === endCol) &&
            startCol !== undefined
          ) {
            boxes.push([
              startCol,
              startRow,
              endCol - startCol,
              next[1] - startRow,
            ])
          }

          // If line exists for this column, set row and column
          if (match) {
            startCol = endCol
            next = match
          }

          // Otherwise, reset once that line ends
          else if (next[2] === endCol) {
            startCol = endCol
            next = others.find(
              (other) =>
                other[0] <= endCol && endCol < other[2] && other[1] > startRow
            )
          }
        }
      })
    },

    () => {
      ctx.lineCap = "round"
      ctx.lineJoin = "round"
      ctx.lineWidth = cellSize / 5
      ctx.strokeStyle = palette.dark

      ctx.clearRect(0, 0, canvas.width, canvas.height)

      ctx.save()
      ctx.beginPath()

      const rect = new Path2D()

      const hatchLength = cellSize * 2
      const hatchOffset = ctx.lineWidth * 2

      rect.rect(buffer, buffer, width, height)

      ctx.stroke(rect)
      ctx.clip(rect)

      ctx.beginPath()

      const matrix = Array.from({ length: colCount }, () => [])
      grid.forEach((p) => {
        matrix[p.col][p.row] = p
      })

      boxes.forEach(([c, r, w, h]) => {
        const t = random.wobble(w < h ? t1 : t2, Math.PI / 3)

        const p1 = matrix[c][r]
        const p2 = matrix[c + w][r]
        const p3 = matrix[c + w][r + h]
        const p4 = matrix[c][r + h]

        const center = [p1.x + (p3.x - p1.x) / 2, p1.y + (p3.y - p1.y) / 2]

        const path = new Path2D()
        let hasOverlap

        for (let i = 0; i < w; i++) {
          const p = matrix[c + i][r]
          path.lineTo(p.x, p.y)
          if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
        }

        for (let i = 0; i < h; i++) {
          const p = matrix[c + w][r + i]
          path.lineTo(p.x, p.y)
          if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
        }

        for (let i = w; i > 0; i--) {
          const p = matrix[c + i][r + h]
          path.lineTo(p.x, p.y)
          if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
        }

        for (let i = h; i > 0; i--) {
          const p = matrix[c][r + i]
          path.lineTo(p.x, p.y)
          if (!hasOverlap) hasOverlap = ctx.isPointInPath(rect, p.x, p.y)
        }

        if (!hasOverlap) return

        path.closePath()

        ctx.save()
        ctx.clip(path)
        ctx.beginPath()

        const radius =
          Math.sqrt(
            Math.max(
              Math.pow(p3.x - p1.x, 2) + Math.pow(p3.y - p1.y, 2),
              Math.pow(p4.x - p2.x, 2) + Math.pow(p4.y - p2.y, 2)
            )
          ) / 2

        const rows = radius / hatchLength
        const cols = radius / hatchOffset

        for (let i = -rows; i < rows; i++) {
          const dx = center[0] + Math.cos(t) * (i * hatchLength)
          const dy = center[1] + Math.sin(t) * (i * hatchLength)

          for (let j = -cols + random.number(1); j < cols; j++) {
            const x =
              dx +
              Math.cos(-Math.PI / 2 + t) *
                (j * hatchOffset + random.wobble(ctx.lineWidth))

            const y =
              dy +
              Math.sin(-Math.PI / 2 + t) *
                (j * hatchOffset + random.wobble(ctx.lineWidth))

            const r = (hatchLength - ctx.lineWidth) * random.number(0.4, 0.5)
            const ti = t + random.wobble(Math.PI / 20)
            const x1 = x + Math.cos(ti) * r
            const x2 = x - Math.cos(ti) * r
            const y1 = y + Math.sin(ti) * r
            const y2 = y - Math.sin(ti) * r

            if (
              (ctx.isPointInPath(path, x, y) &&
                ctx.isPointInPath(rect, x, y)) ||
              (ctx.isPointInPath(path, x1, y1) &&
                ctx.isPointInPath(rect, x1, y1)) ||
              (ctx.isPointInPath(path, x2, y2) &&
                ctx.isPointInPath(rect, x2, y2))
            ) {
              ctx.moveTo(x1, y1)
              ctx.lineTo(x2, y2)
            }
          }
        }

        ctx.stroke()
        ctx.restore()
      })
    },

    () =>
      posterize({
        ctx,
        radius: ctx.lineWidth,
        ramp: ctx.lineWidth * 3,
      }),
  ])
}