SESSION — 02

Backgrounds

Starting with one background, adding more over time. Every one is a different experiment. The code and prompt are yours to take, copy, or break. Tag me on socials when you do something with it.

'use client'

// Background 01 — Animated grid with line tracers

import { useEffect, useRef } from 'react'

interface Tracer {
  axis: 'h' | 'v'
  linePos: number
  rangeStart: number
  rangeEnd: number
  headPos: number
  tailLength: number
  speed: number
  done: boolean
}

export default function Background01() {
  const canvasRef = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return
    const ctx = canvas.getContext('2d')
    if (!ctx) return

    // Non-null locals so nested closures satisfy TypeScript
    const c: HTMLCanvasElement = canvas
    const cx: CanvasRenderingContext2D = ctx

    const CELL = 32
    const GRID_COLOR = '#efefef'
    const TRACE_COLOR = '#b7b7b7'
    const MAX_TRACERS = 14
    const SPAWN_INTERVAL = 350 // ms

    let tracers: Tracer[] = []
    let animationId: number
    let lastSpawn = 0

    function resize() {
      c.width = window.innerWidth
      c.height = window.innerHeight
    }

    function spawnTracer() {
      const cols = Math.floor(c.width / CELL)
      const rows = Math.floor(c.height / CELL)
      const axis = Math.random() > 0.5 ? 'h' : 'v'

      if (axis === 'h') {
        const row = Math.floor(Math.random() * (rows + 1))
        const linePos = row * CELL
        const cellLen = Math.floor(Math.random() * 5) + 3
        const cellStart = Math.floor(Math.random() * Math.max(1, cols - cellLen))
        const rangeStart = cellStart * CELL
        const rangeEnd = Math.min((cellStart + cellLen) * CELL, c.width)
        const tailLength = (rangeEnd - rangeStart) * 0.45

        tracers.push({ axis, linePos, rangeStart, rangeEnd, headPos: rangeStart, tailLength, speed: 1.2 + Math.random() * 1.8, done: false })
      } else {
        const col = Math.floor(Math.random() * (cols + 1))
        const linePos = col * CELL
        const cellLen = Math.floor(Math.random() * 5) + 3
        const cellStart = Math.floor(Math.random() * Math.max(1, rows - cellLen))
        const rangeStart = cellStart * CELL
        const rangeEnd = Math.min((cellStart + cellLen) * CELL, c.height)
        const tailLength = (rangeEnd - rangeStart) * 0.45

        tracers.push({ axis, linePos, rangeStart, rangeEnd, headPos: rangeStart, tailLength, speed: 1.2 + Math.random() * 1.8, done: false })
      }
    }

    function drawGrid() {
      cx.fillStyle = '#ffffff'
      cx.fillRect(0, 0, c.width, c.height)
      cx.strokeStyle = GRID_COLOR
      cx.lineWidth = 1

      for (let x = 0; x <= c.width; x += CELL) {
        cx.beginPath()
        cx.moveTo(x + 0.5, 0)
        cx.lineTo(x + 0.5, c.height)
        cx.stroke()
      }

      for (let y = 0; y <= c.height; y += CELL) {
        cx.beginPath()
        cx.moveTo(0, y + 0.5)
        cx.lineTo(c.width, y + 0.5)
        cx.stroke()
      }
    }

    function updateAndDrawTracers() {
      cx.strokeStyle = TRACE_COLOR
      cx.lineWidth = 1

      tracers = tracers.filter((t) => !t.done)

      for (const t of tracers) {
        t.headPos += t.speed

        const tailPos = Math.max(t.rangeStart, t.headPos - t.tailLength)

        if (tailPos >= t.rangeEnd) {
          t.done = true
          continue
        }

        const drawHead = Math.min(t.headPos, t.rangeEnd)

        cx.beginPath()
        if (t.axis === 'h') {
          cx.moveTo(tailPos, t.linePos + 0.5)
          cx.lineTo(drawHead, t.linePos + 0.5)
        } else {
          cx.moveTo(t.linePos + 0.5, tailPos)
          cx.lineTo(t.linePos + 0.5, drawHead)
        }
        cx.stroke()
      }
    }

    function animate(timestamp: number) {
      drawGrid()

      if (timestamp - lastSpawn > SPAWN_INTERVAL && tracers.length < MAX_TRACERS) {
        spawnTracer()
        lastSpawn = timestamp
      }

      updateAndDrawTracers()
      animationId = requestAnimationFrame(animate)
    }

    resize()
    window.addEventListener('resize', resize)
    animationId = requestAnimationFrame(animate)

    return () => {
      cancelAnimationFrame(animationId)
      window.removeEventListener('resize', resize)
    }
  }, [])

  return <canvas ref={canvasRef} className="fixed inset-0 -z-10" />
}
Background 01 — Animated Grid with Line Tracers