Sponge

Dense organic textures of spongey subaquatic coral.

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

export default (canvas) => {
  const scale = dpi

  canvas.width = canvas.offsetWidth * scale
  canvas.height = canvas.offsetHeight * scale

  const ctx = canvas.getContext("2d")
  const colors = [palette.dark]

  const options = {
    grid: 20,
    strokeWidth: 40,
    coverage: 0.1,
    iterations: 8,
    blurRadius: 8,
    limit: 500,
  }

  options.offset = options.strokeWidth * 1.1

  let points = null
  let strokeWidth = options.strokeWidth

  const iteration = (index) => [
    () => {
      points = []

      const ow = canvas.width - options.offset * 2
      const oh = canvas.height - options.offset * 2

      random
        .shuffle(helpers.makeGrid(ow, oh, options.grid))
        .filter(() => random.maybe(options.coverage))
        .forEach((point) => {
          const t = random.number(Math.PI)
          const x = Math.cos(t) * (options.grid / 3)
          const y = Math.sin(t) * (options.grid / 3)

          points.push({
            x: options.offset + point.x - x,
            y: options.offset + point.y - y,
          })
          points.push({
            x: options.offset + point.x + x,
            y: options.offset + point.y + y,
          })
        })
    },

    () => {
      const t = 1 - (options.iterations - index) / (options.iterations * 10)
      const remaining = [...points]

      let count = 0
      strokeWidth = options.strokeWidth * t

      ctx.beginPath()

      const fn = function (p0) {
        if (
          remaining.length < 4 ||
          count > options.limit ||
          (p0.y >= canvas.height - options.offset * 2 && random.maybe(0.15)) ||
          (p0.x >= canvas.width - options.offset * 2 && random.maybe(0.15))
        )
          return

        const [p1, p2] = helpers.nearest(remaining, p0)
        remaining.splice(remaining.indexOf(p2), 1)

        if (
          helpers.sqdist(p1, p2) <
          options.strokeWidth * options.strokeWidth * 10
        ) {
          ctx.moveTo(p0.x, p0.y)
          ctx.lineTo(p1.x, p1.y)
          ctx.lineTo(p2.x, p2.y)
        }

        count++
      }

      points.forEach(fn)
    },

    () => {
      ctx.lineCap = "round"
      ctx.lineJoin = "round"

      ctx.globalCompositeOperation = "source-atop"
      ctx.lineWidth = strokeWidth * 1.5
      ctx.strokeStyle = "rgba(0, 0, 0, 0.7)"
      ctx.stroke()

      ctx.globalCompositeOperation = "source-over"
      ctx.lineWidth = strokeWidth
      ctx.strokeStyle = palette.dark
      ctx.stroke()

      ctx.strokeStyle = palette.canvas
      ctx.lineWidth = strokeWidth * 0.5
      ctx.stroke()
    },

    () => {
      posterize({
        ctx,
        colors,
        radius: options.blurRadius,
        ramp: options.blurRadius * 2,
        overshoot: 0.5,
      })
    },
  ]

  const steps = Array.from({ length: options.iterations }, (e, i) =>
    iteration(i)
  )

  return sequence([...steps.flat(), () => badge(ctx, { colors })])
}