Creating a Stunning Pixelated Image Animation with Framer Motion
Ever wanted to create that mesmerizing effect where an image appears to assemble itself pixel by pixel as you scroll? Today, I'll walk you through building a captivating pixelated image animation using React and Framer Motion that will make your portfolio stand out from the crowd.
π― What We're Building
We're creating an animated component that:
- Breaks an image into a grid of individual pixels
- Scatters these pixels randomly across the screen initially
- Assembles them into the complete image as the user scrolls
- Provides smooth, performant animations using Framer Motion
π οΈ The Tech Stack
- React - For component structure
- Framer Motion - For smooth animations and scroll-based triggers
- TypeScript - For type safety and better developer experience
- Tailwind CSS - For styling (optional but recommended)
π¨ The Animation Breakdown
The magic happens in two main components:
1. The PixelatedImage Component
This is our main orchestrator that sets up the grid and manages the overall animation.
2. The Pixel Component
Each individual pixel that knows how to animate itself from a random position to its final location.
π» Implementation
Let's dive into the code:
// PixelatedImage.tsx
"use client";
import { motion, useTransform, MotionValue } from "framer-motion";
import { useMemo } from "react";
interface PixelProps {
i: number;
src: string;
gridSize: number;
width: number;
height: number;
scrollYProgress: MotionValue<number>;
}
const Pixel = ({
i,
src,
gridSize,
width,
height,
scrollYProgress,
}: PixelProps) => {
const row = Math.floor(i / gridSize);
const col = i % gridSize;
// Calculate which part of the image this pixel should show
const backgroundPosition = `${(col / (gridSize - 1)) * 100}% ${(row / (gridSize - 1)) * 100}%`;
const backgroundSize = `${gridSize * 100}% ${gridSize * 100}%`;
// Generate random initial positions (cached with useMemo)
const [initialX, initialY] = useMemo(
() => [
(Math.random() - 0.5) * width * 1.5,
(Math.random() - 0.5) * height * 1.5,
],
[width, height]
);
// Calculate animation timing for staggered effect
const numPixels = gridSize * gridSize;
const pixelProgress = i / numPixels;
const animationStart = pixelProgress * 0.8;
const animationDuration = 0.2;
// Create smooth transforms based on scroll progress
const x = useTransform(
scrollYProgress,
[animationStart, animationStart + animationDuration],
[initialX, 0]
);
const y = useTransform(
scrollYProgress,
[animationStart, animationStart + animationDuration],
[initialY, 0]
);
const opacity = useTransform(
scrollYProgress,
[animationStart, animationStart + animationDuration],
[0, 1]
);
const scale = useTransform(
scrollYProgress,
[animationStart, animationStart + animationDuration],
[0.5, 1]
);
return (
<motion.div
className="w-full h-full"
style={{
backgroundImage: `url(${src})`,
backgroundPosition,
backgroundSize,
x,
y,
opacity,
scale,
}}
/>
);
};
interface PixelatedImageProps {
src: string;
width: number;
height: number;
scrollYProgress: MotionValue<number>;
}
const PixelatedImage = ({
src,
width,
height,
scrollYProgress,
}: PixelatedImageProps) => {
const gridSize = 20; // 20x20 grid = 400 pixels
const numPixels = gridSize * gridSize;
return (
<div className="relative w-[250px] h-[250px] md:w-[400px] md:h-[400px]">
{/* Background circle for aesthetic */}
<div className="absolute inset-0 rounded-full bg-gray-200 dark:bg-gray-800" />
{/* Pixel grid */}
<div
className="relative z-10 w-full h-full grid"
style={{
gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
gridTemplateRows: `repeat(${gridSize}, 1fr)`,
}}
>
{Array.from({ length: numPixels }).map((_, i) => (
<Pixel
key={i}
i={i}
src={src}
gridSize={gridSize}
width={width}
height={height}
scrollYProgress={scrollYProgress}
/>
))}
</div>
</div>
);
};
export default PixelatedImage;
π How It Works
1. Grid Setup
We create a 20x20 CSS Grid, giving us 400 individual pixels. Each pixel is a div
that shows a specific portion of the source image using CSS background-position
and background-size
.
2. Background Positioning Magic
const backgroundPosition = `${(col / (gridSize - 1)) * 100}% ${(row / (gridSize - 1)) * 100}%`;
const backgroundSize = `${gridSize * 100}% ${gridSize * 100}%`;
This calculates exactly which part of the image each pixel should display. Think of it like cutting a photo into a jigsaw puzzle - each piece knows exactly where it belongs.
3. Staggered Animation
const pixelProgress = i / numPixels;
const animationStart = pixelProgress * 0.8;
Instead of all pixels animating at once, we stagger them based on their index. This creates a beautiful cascading effect where pixels assemble in sequence.
4. Smooth Transforms
Using Framer Motion's useTransform
, we create smooth transitions for:
- Position (x, y) - From random scatter to final position
- Opacity - Fade in effect
- Scale - Subtle zoom-in for extra polish
πͺ Usage in a Scroll-Based Layout
Here's how to integrate it into a scroll-triggered section:
const About = () => {
const scrollRef = useRef(null);
const { scrollYProgress } = useScroll({
target: scrollRef,
offset: ["start start", "end end"],
});
const imageScrollYProgress = useTransform(scrollYProgress, [0, 0.8], [0, 1]);
return (
<section ref={scrollRef} className="py-16 h-[500vh]">
<div className="sticky top-0 container mx-auto flex items-center h-screen">
<div className="w-1/2 flex items-center justify-center">
<PixelatedImage
src="/your-image.png"
width={250}
height={250}
scrollYProgress={imageScrollYProgress}
/>
</div>
{/* Your content here */}
</div>
</section>
);
};
β‘ Performance Considerations
useMemo for Random Values: We cache the random initial positions to prevent recalculation on every render.
CSS Grid Over Absolute Positioning: CSS Grid is more performant than calculating absolute positions for hundreds of elements.
Transform Instead of Layout Properties: Using
transform
for animations is hardware-accelerated and doesn't trigger layout recalculations.Reasonable Grid Size: 20x20 (400 pixels) provides good visual quality without overwhelming the browser.
π¨ Customization Ideas
Want to make it your own? Try these variations:
- Different Grid Sizes: Smaller grids (10x10) for a more chunky pixel effect, larger grids (30x30) for smoother transitions
- Animation Patterns: Instead of sequential, try animating from center outward or in a spiral pattern
- Color Effects: Add color transitions or filters during the animation
- Multiple Images: Create a slideshow effect with multiple pixelated images
π Browser Support
This animation works great in all modern browsers that support:
- CSS Grid (IE11+)
- CSS Transforms (IE9+)
- ES6 features (or with appropriate polyfills)
π Conclusion
This pixelated image animation is a perfect example of how modern web technologies can create engaging, performant visual effects. The combination of CSS Grid, Framer Motion, and clever mathematical calculations results in an animation that's both visually stunning and technically solid.
The key takeaways:
- Break complex animations into smaller, manageable pieces
- Use CSS features like Grid and transforms for performance
- Leverage Framer Motion's scroll-based animations for smooth interactions
- Cache expensive calculations to maintain 60fps
Ready to add some pixel magic to your next project? Give it a try and let me know what creative variations you come up with!
Found this helpful? Follow me for more web animation tutorials and React tips! π
Top comments (0)