"use client"; import { useEffect, useRef, useState } from "react"; import { useReducedMotion } from "motion/react"; type PixelRevealProps = { src: string; alt: string; /** Size of the first blocks in pixels. */ block?: number; /** Seconds from the first blocks to the sharp picture. */ duration?: number; /** false plays it again each time it comes back on screen. */ once?: boolean; /** Set the size here. */ className?: string; }; /* PixelReveal: the picture starts as big square blocks and sharpens in steps when it comes on screen. */ export function PixelReveal({ src, alt, block = 64, duration = 0.9, once = true, className = "aspect-video" }: PixelRevealProps) { const reduce = useReducedMotion(); const boxRef = useRef(null); const canvasRef = useRef(null); const [done, setDone] = useState(false); const sharp = done || reduce === true; useEffect(() => { const canvas = canvasRef.current; const ctx = canvas?.getContext("2d"); if (!canvas || !ctx || sharp) return; const image = new Image(); const small = document.createElement("canvas"); const smallCtx = small.getContext("2d"); let timer = 0; let inView = false; let loaded = false; const paint = (size: number) => { const { width, height } = canvas.getBoundingClientRect(); const dpr = Math.min(window.devicePixelRatio, 2); canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr); const scale = Math.max(width / image.naturalWidth, height / image.naturalHeight); const sw = width / scale; const sh = height / scale; small.width = Math.max(1, Math.ceil(width / size)); small.height = Math.max(1, Math.ceil(height / size)); smallCtx?.drawImage(image, (image.naturalWidth - sw) / 2, (image.naturalHeight - sh) / 2, sw, sh, 0, 0, small.width, small.height); ctx.imageSmoothingEnabled = false; ctx.drawImage(small, 0, 0, canvas.width, canvas.height); }; const sizes: number[] = []; for (let size = block; size >= 2; size /= 2) sizes.push(size); const start = () => { if (!loaded) return; window.clearTimeout(timer); paint(block); if (!inView) return; let step = 0; const next = () => { if (step === sizes.length) { setDone(true); return; } paint(sizes[step]); step += 1; timer = window.setTimeout(next, (duration * 1000) / sizes.length); }; next(); }; image.onload = () => { loaded = true; start(); }; image.src = src; const watch = new IntersectionObserver( ([entry]) => { inView = entry.isIntersecting; if (inView) start(); }, { threshold: 0.35 }, ); watch.observe(canvas); return () => { image.onload = null; window.clearTimeout(timer); watch.disconnect(); }; }, [src, block, duration, sharp]); useEffect(() => { const box = boxRef.current; if (!box || once || !done) return; const watch = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) setDone(false); }); watch.observe(box); return () => watch.disconnect(); }, [once, done]); return (
{/* eslint-disable-next-line @next/next/no-img-element */} {alt} {!sharp && }
); }