Lines, not long

Lines, not long, not heavy, rarely touching, drawn at random.

Loading...
import sequence from "/scratchpad/_lib/sequence.asset.mjs"
import palette from "/scratchpad/_lib/palette.asset.mjs"
import posterize from "/scratchpad/posterize/posterize.asset.mjs"
import helpers from "/scratchpad/_lib/line.asset.mjs"
import * as random from "/scratchpad/_lib/random.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 buffer = Math.min(canvas.width, canvas.height) * 0.05
  const gridSize = random.number(9, 11)
  const maxLinks = random.integer(7, 15)

  let grid
  let chains

  return sequence([
    () => {
      ctx.fillStyle = palette.canvas
      ctx.strokeStyle = palette.dark
      ctx.lineWidth = 4
      ctx.lineCap = "round"
      ctx.lineJoin = "round"

      ctx.fillRect(0, 0, canvas.width, canvas.height)
    },

    () => {
      const width = canvas.width - buffer * 2
      const height = canvas.height - buffer * 2

      grid = helpers.makeGrid(width, height, gridSize)
    },

    () => {
      const driftVar = 0.1
      const driftMax = gridSize / 2
      const scatter = gridSize / 2

      let driftX = 0
      let driftY = 0

      grid.forEach((point) => {
        driftX = random.wobble(driftX, driftVar, -driftMax, driftMax)
        driftY = random.wobble(driftY, driftVar, -driftMax, driftMax)

        point.x += buffer + random.wobble(scatter) + driftX
        point.y += buffer + random.wobble(scatter) + driftY

        if (point.row % 2 === 0) point.x += random.number(scatter)
        if (point.col % 2 === 0) point.y += random.number(scatter)

        point.neighbors = []
      })
    },

    () => {
      const r = 0.001

      ctx.beginPath()

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

      ctx.stroke()
    },

    // For each point, determine nearest neighbours.
    // Speed up search by partitioning into a matrix.
    () => {
      const sqDist = gridSize * gridSize * Math.sqrt(2)

      const partitions = []

      const neighbours = [
        // Same
        [0, 0],
        // Up, down, left, right
        [0, -1],
        [0, 1],
        [1, 0],
        [-1, 0],
        // Diagonals
        [1, 1],
        [1, -1],
        [-1, 1],
        [-1, -1],
      ]

      let columnCount = 0
      const getIndex = (r, c) => r * columnCount + c

      grid.forEach((p) => {
        p.c = Math.floor(p.x / gridSize)
        p.r = Math.floor(p.y / gridSize)
        columnCount = Math.max(columnCount, p.c + 1)
      })

      grid.forEach((p) => {
        const i = getIndex(p.r, p.c)
        if (!partitions[i]) partitions[i] = [p]
        else partitions[i].push(p)
      })

      grid.forEach((p1) => {
        neighbours.forEach(([r, c]) => {
          const list = partitions[getIndex(p1.r + r, p1.c + c)]

          if (list) {
            p1.neighbors.push(
              ...list.filter(
                (p2) => p1 !== p2 && helpers.sqdist(p1, p2) <= sqDist
              )
            )
          }
        })
      })
    },

    () => {
      const stack = random.shuffle([...grid])
      chains = [[stack.pop()]]

      // Check whether the point starts or ends the chain
      const isTerminal = (p) =>
        p.chain[0] === p || p.chain[p.chain.length - 1] === p

      while (stack.length) {
        const chain = chains[0]

        chain[0].chain = chain

        if (chain.length < maxLinks) {
          const p1 = chain[0]
          const p2 = random.sample(
            p1.neighbors.filter(
              (p) =>
                !p.chain ||
                // If a chain exists, it must be different, the combined
                // length must not be greater than the limit and the point
                // must be a terminal on the chain.
                (p.chain.length + chain.length <= maxLinks &&
                  isTerminal(p) &&
                  !p.chain.includes(p1))
            )
          )

          if (p2) {
            if (p2.chain) {
              // Ensure the new points are contiguous and add to chain
              if (p2.chain[0] === p2) p2.chain.reverse()
              chain.unshift(...p2.chain)

              // Remove all items from the old chain
              p2.chain.splice(0, p2.chain.length)

              // Update chain reference for added points
              chain.forEach((p) => (p.chain = chain))
            } else {
              p2.chain = chain
              chain.unshift(p2)
            }
            continue
          }
        }

        let next = stack.pop()

        while (next) {
          if (!next.chain) break
          if (isTerminal(next)) break
          next = stack.pop()
        }

        if (next) chains.unshift([next])
      }
    },

    () => {
      // Remove terminals that sit on other lines
      // This is a hack, it would ideally be avoided in the previous step
      chains.forEach((chain) => {
        if (chain.length)
          chain[0].neighbors
            .filter((p) => p.chain && !chain.includes(p))
            .forEach((p) => {
              if (p.chain.includes(chain[0])) chain.shift()
            })

        if (chain.length)
          chain[chain.length - 1].neighbors
            .filter((p) => p.chain && !chain.includes(p))
            .forEach((p) => {
              if (p.chain.includes(chain[chain.length - 1])) chain.pop()
            })
      })
    },

    () => {
      // Tiny chains are synthetically expanded
      const mx = (x) => Math.max(Math.min(x, canvas.width - buffer), buffer)
      const my = (y) => Math.max(Math.min(y, canvas.height - buffer), buffer)

      chains
        .filter((c) => c.length === 1)
        .forEach((c) => {
          c.push({
            x: mx(random.wobble(c[0].x, gridSize / 200)),
            y: my(random.wobble(c[0].y, gridSize / 200)),
          })
          c.unshift({
            x: mx(random.wobble(c[0].x, gridSize / 200)),
            y: my(random.wobble(c[0].y, gridSize / 200)),
          })
        })

      chains = chains.filter((c) => c.length >= 2)
    },

    () => {
      ctx.fillRect(0, 0, canvas.width, canvas.height)

      ctx.beginPath()

      chains.forEach((chain) => {
        const a = chain[0]
        const b = chain[1]
        const y = chain[chain.length - 2]
        const z = chain[chain.length - 1]

        const x1 = (a.x - b.x) * 0.1
        const y1 = (a.y - b.y) * 0.1

        const x2 = (z.x - y.x) * 0.1
        const y2 = (z.y - y.y) * 0.1

        chain.unshift({ x: a.x + x1, y: a.y + y1 })
        chain.push({ x: z.x + x2, y: z.y + y2 })

        const spline = helpers.spline(chain, 40)

        ctx.moveTo(spline[0].x, spline[0].y)
        spline.slice(1).forEach((p) => ctx.lineTo(p.x, p.y))
      })

      ctx.stroke()
    },

    () => {
      const d = buffer - gridSize / 4

      posterize({
        ctx,
        radius: 3,
        ramp: 30,
        overshoot: 0.4,
        colors: [ctx.strokeStyle],
        background: ctx.fillStyle,
        x: d,
        y: d,
        width: canvas.width - 2 * d,
        height: canvas.height - 2 * d,
      })
    },
  ])
}