Banded Frames

Skewed grids of angular striations.

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"

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(10, 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)
      })
    },

    // Show orientation
    () => {
      ctx.beginPath()
      ctx.moveTo(100, 100)
      ctx.lineTo(100 + Math.cos(t1) * 100, 100 + Math.sin(t1) * 100)
      ctx.moveTo(100, 100)
      ctx.lineTo(100 + Math.cos(t2) * 100, 100 + Math.sin(t2) * 100)
      ctx.stroke()
    },

    // Draw grid
    () => {
      const r = 2
      ctx.beginPath()

      grid.forEach((p, i) => {
        ctx.moveTo(p.x + r, p.y)
        ctx.arc(p.x, p.y, r, 0, Math.PI * 2)
      })

      ctx.fill()
    },

    // Draw bounding boxes
    () => {
      ctx.strokeStyle = palette.dark

      const minX = Math.min(...grid.map((p) => p.x))
      const maxX = Math.max(...grid.map((p) => p.x))
      const minY = Math.min(...grid.map((p) => p.y))
      const maxY = Math.max(...grid.map((p) => p.y))

      ctx.beginPath()
      ctx.rect(minX, minY, maxX - minX, maxY - minY)
      ctx.rect(buffer, buffer, width, height)
      ctx.stroke()
    },

    // 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.lineWidth = cellSize / 3

      const offset = ctx.lineWidth * 2
      const rect = new Path2D()

      rect.rect(
        buffer + ctx.lineWidth,
        buffer + ctx.lineWidth,
        width - 2 * ctx.lineWidth,
        height - 2 * ctx.lineWidth
      )

      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.save()
      ctx.stroke(rect)
      ctx.clip(rect)

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

        const p1 = grid[(r + 0) * colCount + (c + 0)]
        const p2 = grid[(r + 0) * colCount + (c + w)]
        const p3 = grid[(r + h) * colCount + (c + w)]
        const p4 = grid[(r + h) * colCount + (c + 0)]

        ctx.save()

        ctx.beginPath()
        ctx.moveTo(p1.x, p1.y)
        ctx.lineTo(p2.x, p2.y)
        ctx.lineTo(p3.x, p3.y)
        ctx.lineTo(p4.x, p4.y)
        ctx.closePath()
        ctx.stroke()
        ctx.clip()

        ctx.translate(p1.x + (p3.x - p1.x) / 2, p1.y + (p3.y - p1.y) / 2)
        ctx.rotate(t)

        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 lineCount = radius / offset

        ctx.beginPath()

        for (let i = -lineCount; i < lineCount; i++) {
          ctx.moveTo(0 - radius, offset * i)
          ctx.lineTo(0 + radius, offset * i)
        }

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