pixelated-grid

PreviousNext
Docs
aliimamcomponent

Preview

Loading preview…
registry/default/components/pixelated-grid.tsx
"use client"

import { useEffect, useRef, useState } from "react"

interface PixelGridProps {
  bgColor?: string
  pixelColor?: string
  numPixelsX?: number
  numPixelsY?: number
  pixelSize?: number
  pixelSpacing?: number
  pixelDeathFade?: number
  pixelBornFade?: number
  pixelMaxLife?: number
  pixelMinLife?: number
  pixelMaxOffLife?: number
  pixelMinOffLife?: number
  className?: string
  glow?: boolean
}

interface Pixel {
  xPos: number
  yPos: number
  alpha: number
  maxAlpha: number
  life: number
  offLife: number
  isLit: boolean
  dying: boolean
  deathFade: number
  bornFade: number
  randomizeSelf: () => void
}

export function PixelGrid({
  bgColor = "transparent",
  pixelColor = "#0000ff",
  numPixelsX = 10,
  numPixelsY = 10,
  pixelSize = 3,
  pixelSpacing = 3,
  pixelDeathFade = 10,
  pixelBornFade = 50,
  pixelMaxLife = 500,
  pixelMinLife = 250,
  pixelMaxOffLife = 500,
  pixelMinOffLife = 200,
  glow = false,
  className = "",
}: PixelGridProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const pixelsRef = useRef<Pixel[]>([])
  const [isAppeared, setIsAppeared] = useState(false)

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const c2d = canvas.getContext("2d", { alpha: true })
    if (!c2d) return

    const resizeCanvas = () => {
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
    }

    resizeCanvas()
    window.addEventListener("resize", resizeCanvas)

    const randomAlpha = () => {
      const rand = Math.random() * 100
      if (rand > 90) return 1
      if (rand > 80) return 0.5
      return 0.1
    }

    const randomizePixelAttrs = (x: number, y: number): Pixel => {
      const alpha = randomAlpha()
      const lit = alpha !== 0.1
      return {
        xPos: x * (pixelSize + pixelSpacing),
        yPos: y * (pixelSize + pixelSpacing),
        alpha: 0,
        maxAlpha: alpha,
        life: Math.floor(Math.random() * (pixelMaxLife - pixelMinLife + 1)) + pixelMinLife,
        offLife: Math.floor(Math.random() * (pixelMaxOffLife - pixelMinOffLife + 1)) + pixelMinOffLife,
        isLit: lit,
        dying: false,
        deathFade: pixelDeathFade,
        bornFade: pixelBornFade,
        randomizeSelf() {
          const newAlpha = randomAlpha()
          this.alpha = 0
          this.maxAlpha = newAlpha
          this.life = Math.floor(Math.random() * (pixelMaxLife - pixelMinLife + 1)) + pixelMinLife
          this.offLife = Math.floor(Math.random() * (pixelMaxOffLife - pixelMinOffLife + 1)) + pixelMinOffLife
          this.isLit = newAlpha !== 0.1
          this.dying = false
          this.deathFade = pixelDeathFade
          this.bornFade = pixelBornFade
        },
      }
    }

    // Initialize pixels dynamically based on viewport
    const initPixels = () => {
      const cols = Math.ceil(window.innerWidth / (pixelSize + pixelSpacing))
      const rows = Math.ceil(window.innerHeight / (pixelSize + pixelSpacing))
      pixelsRef.current = []
      for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
          pixelsRef.current.push(randomizePixelAttrs(x, y))
        }
      }
    }

    initPixels()
    setIsAppeared(true)

    const drawPixel = (pixel: Pixel) => {
      pixel.alpha = Math.min(Math.max(pixel.alpha, 0.1), pixel.maxAlpha)
      c2d.fillStyle = `${pixelColor}${Math.floor(pixel.alpha * 255)
        .toString(16)
        .padStart(2, "0")}`

      c2d.fillRect(pixel.xPos, pixel.yPos, pixelSize, pixelSize)

      if (pixel.isLit) {
        if (pixel.bornFade <= 0) {
          if (pixel.life <= 0) {
            pixel.dying = true
            if (pixel.deathFade <= 0) pixel.randomizeSelf()
            else {
              pixel.alpha = (pixel.deathFade / pixelDeathFade) * pixel.maxAlpha
              pixel.deathFade--
            }
          } else pixel.life--
        } else {
          pixel.alpha = pixel.maxAlpha - pixel.bornFade / pixelBornFade
          pixel.bornFade--
        }
      } else {
        if (pixel.offLife <= 0) pixel.isLit = true
        pixel.offLife--
      }
    }

    const renderLoop = () => {
      if (bgColor === "transparent") c2d.clearRect(0, 0, canvas.width, canvas.height)
      else {
        c2d.fillStyle = bgColor
        c2d.fillRect(0, 0, canvas.width, canvas.height)
      }

      if (glow) {
        c2d.shadowBlur = 8
        c2d.shadowColor = pixelColor
      } else {
        c2d.shadowBlur = 0
      }

      for (const pixel of pixelsRef.current) drawPixel(pixel)
      requestAnimationFrame(renderLoop)
    }

    renderLoop()

    return () => {
      window.removeEventListener("resize", resizeCanvas)
    }
  }, [
    bgColor,
    pixelColor,
    pixelSize,
    pixelSpacing,
    pixelDeathFade,
    pixelBornFade,
    pixelMaxLife,
    pixelMinLife,
    pixelMaxOffLife,
    pixelMinOffLife,
    glow,
  ])

  return (
    <canvas
      ref={canvasRef}
      className={`w-screen h-fit ${className}`}
      style={{ 
        backgroundColor: "transparent", 
      }}
    />
  )
}

Installation

npx shadcn@latest add @aliimam/pixelated-grid

Usage

import { PixelatedGrid } from "@/components/pixelated-grid"
<PixelatedGrid />