'use client'; import React, { useRef, useState, useEffect, useCallback } from 'react'; import { motion } from 'framer-motion'; import clsx from 'clsx'; import { convertToRgba } from '@/lib/utils'; export const LandingFlickeringGridCtaBg = ({ className, variant = 'default', squareSize = 4, gridGap = 6, flickerChance = 0.1, maxOpacity = 0.3, }: { className?: string; variant?: 'default' | 'primary' | 'secondary'; squareSize?: number; gridGap?: number; flickerChance?: number; maxOpacity?: number; }) => { const domRef = useRef(null); const canvasRef = useRef(null); const containerRef = useRef(null); const colorsRef = useRef([ 'rgba(177, 177, 177,', 'rgba(34, 34, 34,', 'rgba(100, 100, 100,', ]); const [isInView, setIsInView] = useState(false); const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); const [colors, setColors] = useState({ color1: 'rgb(177, 177, 177)', color2: 'rgb(34, 34, 34)', color3: 'rgb(100, 100, 100)', }); const generateNewColors = useCallback(() => { if (!domRef.current) return; const computedStyle = getComputedStyle(domRef.current); let newColors; switch (variant) { case 'primary': { const primaryLighter = computedStyle .getPropertyValue('--primary-lighter') .trim(); const primaryMain = computedStyle .getPropertyValue('--primary-main') .trim(); const primaryDarker = computedStyle .getPropertyValue('--primary-darker') .trim(); newColors = { color1: convertToRgba({ color: primaryLighter, opacity: 1 }) || 'rgb(177, 177, 177)', color2: convertToRgba({ color: primaryMain, opacity: 1 }) || 'rgb(100, 100, 100)', color3: convertToRgba({ color: primaryDarker, opacity: 1 }) || 'rgb(34, 34, 34)', }; break; } case 'secondary': { const secondaryLighter = computedStyle .getPropertyValue('--secondary-lighter') .trim(); const secondaryMain = computedStyle .getPropertyValue('--secondary-main') .trim(); const secondaryDarker = computedStyle .getPropertyValue('--secondary-darker') .trim(); newColors = { color1: convertToRgba({ color: secondaryLighter, opacity: 1 }) || 'rgb(177, 177, 177)', color2: convertToRgba({ color: secondaryMain, opacity: 1 }) || 'rgb(100, 100, 100)', color3: convertToRgba({ color: secondaryDarker, opacity: 1 }) || 'rgb(34, 34, 34)', }; break; } default: { const primaryLighter = computedStyle .getPropertyValue('--primary-lighter') .trim(); const secondaryMain = computedStyle .getPropertyValue('--secondary-main') .trim(); const secondaryDarker = computedStyle .getPropertyValue('--secondary-darker') .trim(); newColors = { color1: convertToRgba({ color: primaryLighter, opacity: 1 }) || 'rgb(177, 177, 177)', color2: convertToRgba({ color: secondaryMain, opacity: 1 }) || 'rgb(100, 100, 100)', color3: convertToRgba({ color: secondaryDarker, opacity: 1 }) || 'rgb(34, 34, 34)', }; break; } } setColors(newColors); // Update the ref with converted colors const toRGBA = (color: string) => { if (typeof window === 'undefined') { return `rgba(0, 0, 0,`; } const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; const ctx = canvas.getContext('2d'); if (!ctx) return 'rgba(255, 0, 0,'; ctx.fillStyle = color; ctx.fillRect(0, 0, 1, 1); const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data); return `rgba(${r}, ${g}, ${b},`; }; colorsRef.current = [ newColors.color1, newColors.color2, newColors.color3, ].map(toRGBA); }, [variant]); const setupCanvas = useCallback( (canvas: HTMLCanvasElement, width: number, height: number) => { const dpr = window.devicePixelRatio || 1; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; const cols = Math.floor(width / (squareSize + gridGap)); const rows = Math.floor(height / (squareSize + gridGap)); const squares = new Float32Array(cols * rows); const squareColors = new Uint8Array(cols * rows); for (let i = 0; i < squares.length; i++) { squares[i] = Math.random() * maxOpacity; squareColors[i] = Math.floor(Math.random() * 3); } return { cols, rows, squares, squareColors, dpr }; }, [squareSize, gridGap, maxOpacity], ); const updateSquares = useCallback( (squares: Float32Array, squareColors: Uint8Array, deltaTime: number) => { for (let i = 0; i < squares.length; i++) { if (Math.random() < flickerChance * deltaTime) { squares[i] = Math.random() * maxOpacity; squareColors[i] = Math.floor(Math.random() * 3); } } }, [flickerChance, maxOpacity], ); const drawGrid = useCallback( ( ctx: CanvasRenderingContext2D, width: number, height: number, cols: number, rows: number, squares: Float32Array, squareColors: Uint8Array, dpr: number, ) => { ctx.clearRect(0, 0, width, height); ctx.fillStyle = 'transparent'; ctx.fillRect(0, 0, width, height); for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const index = i * rows + j; const opacity = squares[index]; const colorIndex = squareColors[index]; ctx.fillStyle = `${colorsRef.current[colorIndex]}${opacity})`; ctx.fillRect( i * (squareSize + gridGap) * dpr, j * (squareSize + gridGap) * dpr, squareSize * dpr, squareSize * dpr, ); } } }, [squareSize, gridGap], ); const cycleDuration = 5 * 1000; useEffect(() => { generateNewColors(); }, [generateNewColors]); useEffect(() => { const interval = setInterval(() => { generateNewColors(); }, cycleDuration); return () => clearInterval(interval); }, [generateNewColors, cycleDuration]); useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let animationFrameId: number; let gridParams: ReturnType; let isInitialized = false; const updateCanvasSize = () => { const newWidth = container.clientWidth; const newHeight = container.clientHeight; setCanvasSize({ width: newWidth, height: newHeight }); // Only recreate the grid if it's the first time or canvas size changed significantly if ( !isInitialized || !gridParams || Math.abs( newWidth - (canvas.style.width ? parseInt(canvas.style.width) : 0), ) > 10 || Math.abs( newHeight - (canvas.style.height ? parseInt(canvas.style.height) : 0), ) > 10 ) { gridParams = setupCanvas(canvas, newWidth, newHeight); isInitialized = true; } else { // Just update the canvas size but keep the existing squares const dpr = window.devicePixelRatio || 1; canvas.width = newWidth * dpr; canvas.height = newHeight * dpr; canvas.style.width = `${newWidth}px`; canvas.style.height = `${newHeight}px`; gridParams.dpr = dpr; } }; updateCanvasSize(); let lastTime = 0; const animate = (time: number) => { if (!isInView || !gridParams) return; const deltaTime = Math.min((time - lastTime) / 1000, 0.1); // Cap deltaTime to prevent large jumps lastTime = time; updateSquares(gridParams.squares, gridParams.squareColors, deltaTime); drawGrid( ctx, canvas.width, canvas.height, gridParams.cols, gridParams.rows, gridParams.squares, gridParams.squareColors, gridParams.dpr, ); animationFrameId = requestAnimationFrame(animate); }; const resizeObserver = new ResizeObserver(() => { updateCanvasSize(); }); resizeObserver.observe(container); const intersectionObserver = new IntersectionObserver( ([entry]) => { setIsInView(entry.isIntersecting); }, { threshold: 0 }, ); intersectionObserver.observe(canvas); if (isInView) { animationFrameId = requestAnimationFrame(animate); } return () => { cancelAnimationFrame(animationFrameId); resizeObserver.disconnect(); intersectionObserver.disconnect(); }; }, [setupCanvas, updateSquares, drawGrid, isInView]); return (
); };