11Pixelate

Pixelate

GSAPCanvasRetroGaming

Retro pixel dissolution effect using Canvas for smooth 60fps performance. Pixels appear and dissolve randomly, creating an authentic 8-bit style transition.

Quick Start

  • 📦npm install next-transition-router gsap
  • 🎨Canvas-based rendering for 60fps performance
  • 🎲Seeded random ensures consistent pixel positions
  • ✨GSAP animates progress 0→1, Canvas draws each frame

Installation

Terminal
1npm install next-transition-router gsap

Provider Setup

app/providers.tsx
1'use client'
2
3import { useRef, useEffect, useCallback } from 'react'
4import { TransitionRouter } from 'next-transition-router'
5import gsap from 'gsap'
6
7export 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 })
11
12 const drawPixels = useCallback((progress: number, isEntering: boolean) => {
13 const canvas = canvasRef.current
14 if (!canvas) return
15
16 const ctx = canvas.getContext('2d')
17 if (!ctx) return
18
19 const pixelSize = 12
20 const cols = Math.ceil(canvas.width / pixelSize)
21 const rows = Math.ceil(canvas.height / pixelSize)
22
23 ctx.clearRect(0, 0, canvas.width, canvas.height)
24
25 // Seeded random for consistent animation
26 const seed = 12345
27 const random = (i: number) => {
28 const x = Math.sin(seed + i) * 10000
29 return x - Math.floor(x)
30 }
31
32 for (let row = 0; row < rows; row++) {
33 for (let col = 0; col < cols; col++) {
34 const i = row * cols + col
35 const threshold = random(i)
36
37 const shouldShow = isEntering
38 ? progress < threshold
39 : progress > threshold
40
41 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 }, [])
49
50 useEffect(() => {
51 const canvas = canvasRef.current
52 if (!canvas) return
53
54 const resize = () => {
55 canvas.width = window.innerWidth
56 canvas.height = window.innerHeight
57 }
58
59 resize()
60 window.addEventListener('resize', resize)
61 return () => window.removeEventListener('resize', resize)
62 }, [])
63
64 return (
65 <>
66 <TransitionRouter
67 auto
68 leave={(next) => {
69 const tl = gsap.timeline({ onComplete: next })
70 animationRef.current.progress = 0
71
72 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 = 1
82
83 tl.to(animationRef.current, {
84 progress: 0,
85 duration: 0.4,
86 ease: 'power2.inOut',
87 onUpdate: () => drawPixels(animationRef.current.progress, true),
88 })
89
90 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>
100
101 <canvas ref={canvasRef} style={{
102 position: 'fixed', inset: 0,
103 zIndex: 9999, pointerEvents: 'none',
104 }} />
105 </>
106 )
107}