Terminal
1npm install next-transition-router gsap
Retro pixel dissolution effect using Canvas for smooth 60fps performance. Pixels appear and dissolve randomly, creating an authentic 8-bit style transition.
npm install next-transition-router gsap1npm install next-transition-router gsap
1'use client'23import { useRef, useEffect, useCallback } from 'react'4import { TransitionRouter } from 'next-transition-router'5import gsap from 'gsap'67export default function PixelateProvider({ children }: { children: React.ReactNode }) {8 const wrapperRef = useRef<HTMLDivElement>(null)9 const canvasRef = useRef<HTMLCanvasElement>(null)10 const animationRef = useRef<{ progress: number }>({ progress: 0 })1112 const drawPixels = useCallback((progress: number, isEntering: boolean) => {13 const canvas = canvasRef.current14 if (!canvas) return1516 const ctx = canvas.getContext('2d')17 if (!ctx) return1819 const pixelSize = 1220 const cols = Math.ceil(canvas.width / pixelSize)21 const rows = Math.ceil(canvas.height / pixelSize)2223 ctx.clearRect(0, 0, canvas.width, canvas.height)2425 // Seeded random for consistent animation26 const seed = 1234527 const random = (i: number) => {28 const x = Math.sin(seed + i) * 1000029 return x - Math.floor(x)30 }3132 for (let row = 0; row < rows; row++) {33 for (let col = 0; col < cols; col++) {34 const i = row * cols + col35 const threshold = random(i)3637 const shouldShow = isEntering38 ? progress < threshold39 : progress > threshold4041 if (shouldShow) {42 const shade = Math.floor(10 + random(i + 1000) * 15)43 ctx.fillStyle = `rgb(${shade}, ${shade}, ${shade})`44 ctx.fillRect(col * pixelSize, row * pixelSize, pixelSize, pixelSize)45 }46 }47 }48 }, [])4950 useEffect(() => {51 const canvas = canvasRef.current52 if (!canvas) return5354 const resize = () => {55 canvas.width = window.innerWidth56 canvas.height = window.innerHeight57 }5859 resize()60 window.addEventListener('resize', resize)61 return () => window.removeEventListener('resize', resize)62 }, [])6364 return (65 <>66 <TransitionRouter67 auto68 leave={(next) => {69 const tl = gsap.timeline({ onComplete: next })70 animationRef.current.progress = 07172 tl.to(animationRef.current, {73 progress: 1,74 duration: 0.4,75 ease: 'power2.inOut',76 onUpdate: () => drawPixels(animationRef.current.progress, false),77 })78 }}79 enter={(next) => {80 const tl = gsap.timeline({ onComplete: next })81 animationRef.current.progress = 18283 tl.to(animationRef.current, {84 progress: 0,85 duration: 0.4,86 ease: 'power2.inOut',87 onUpdate: () => drawPixels(animationRef.current.progress, true),88 })8990 tl.add(() => {91 const ctx = canvasRef.current?.getContext('2d')92 if (ctx && canvasRef.current) {93 ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)94 }95 })96 }}97 >98 <div ref={wrapperRef}>{children}</div>99 </TransitionRouter>100101 <canvas ref={canvasRef} style={{102 position: 'fixed', inset: 0,103 zIndex: 9999, pointerEvents: 'none',104 }} />105 </>106 )107}