UI Components Medium
Pixel Image
Image that assembles from scattered pixels or reveals pixel-by-pixel on click, using canvas for dynamic pixel manipulation.
Open in Lab
MCP
css javascript canvas vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
background: #0a0a0a;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.pixel-wrapper {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
#pixel-canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
.pixel-overlay {
position: absolute;
bottom: 3rem;
left: 50%;
transform: translateX(-50%);
text-align: center;
pointer-events: none;
z-index: 10;
}
.pixel-title {
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #e0e7ff 0%, #818cf8 50%, #6366f1 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.25rem;
}
.pixel-subtitle {
font-size: clamp(0.8rem, 1.8vw, 1rem);
color: rgba(148, 163, 184, 0.7);
}// Pixel Image — scatter and assemble pixels from a generated gradient image
(function () {
"use strict";
const canvas = document.getElementById("pixel-canvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
const PIXEL_SIZE = 4;
const ANIM_DURATION = 2000;
let pixels = [];
let animating = false;
let scattered = true;
let startTime = 0;
let imgWidth = 0;
let imgHeight = 0;
// Generate a demo gradient image since we can't load external images in sandboxed iframe
function generateDemoImage() {
const w = Math.min(400, window.innerWidth - 40);
const h = Math.min(300, window.innerHeight - 160);
imgWidth = w;
imgHeight = h;
canvas.width = w;
canvas.height = h;
// Draw a beautiful gradient with shapes
const tempCanvas = document.createElement("canvas");
tempCanvas.width = w;
tempCanvas.height = h;
const tctx = tempCanvas.getContext("2d");
// Background gradient
const bg = tctx.createLinearGradient(0, 0, w, h);
bg.addColorStop(0, "#1e1b4b");
bg.addColorStop(0.3, "#312e81");
bg.addColorStop(0.6, "#4338ca");
bg.addColorStop(1, "#6366f1");
tctx.fillStyle = bg;
tctx.fillRect(0, 0, w, h);
// Draw circles
for (let i = 0; i < 6; i++) {
const cx = w * (0.15 + Math.random() * 0.7);
const cy = h * (0.15 + Math.random() * 0.7);
const r = 20 + Math.random() * 60;
const grad = tctx.createRadialGradient(cx, cy, 0, cx, cy, r);
grad.addColorStop(
0,
`rgba(${167 + Math.random() * 60}, ${139 + Math.random() * 60}, 250, 0.6)`
);
grad.addColorStop(1, "transparent");
tctx.beginPath();
tctx.arc(cx, cy, r, 0, Math.PI * 2);
tctx.fillStyle = grad;
tctx.fill();
}
// Draw a star shape in center
tctx.save();
tctx.translate(w / 2, h / 2);
tctx.fillStyle = "rgba(255, 255, 255, 0.15)";
tctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const x = Math.cos(angle) * 50;
const y = Math.sin(angle) * 50;
if (i === 0) tctx.moveTo(x, y);
else tctx.lineTo(x, y);
}
tctx.closePath();
tctx.fill();
tctx.restore();
return tctx.getImageData(0, 0, w, h);
}
function extractPixels(imageData) {
pixels = [];
const data = imageData.data;
const w = imageData.width;
const h = imageData.height;
for (let y = 0; y < h; y += PIXEL_SIZE) {
for (let x = 0; x < w; x += PIXEL_SIZE) {
const i = (y * w + x) * 4;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
if (a < 10) continue;
pixels.push({
targetX: x,
targetY: y,
currentX: Math.random() * w * 2 - w * 0.5,
currentY: Math.random() * h * 2 - h * 0.5,
startX: 0,
startY: 0,
color: `rgba(${r},${g},${b},${a / 255})`,
delay: Math.random() * 0.3,
});
}
}
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function easeInCubic(t) {
return t * t * t;
}
function startAnimation() {
if (animating) return;
animating = true;
startTime = performance.now();
// Save start positions
for (const p of pixels) {
p.startX = p.currentX;
p.startY = p.currentY;
}
requestAnimationFrame(animate);
}
function animate(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / ANIM_DURATION, 1);
ctx.clearRect(0, 0, imgWidth, imgHeight);
for (const p of pixels) {
const adjustedProgress = Math.max(0, Math.min(1, (progress - p.delay) / (1 - p.delay)));
if (scattered) {
// Assembling: scattered -> target
const ease = easeOutCubic(adjustedProgress);
p.currentX = p.startX + (p.targetX - p.startX) * ease;
p.currentY = p.startY + (p.targetY - p.startY) * ease;
} else {
// Scattering: target -> random
const ease = easeInCubic(adjustedProgress);
const randX = Math.random() * imgWidth * 2 - imgWidth * 0.5;
const randY = Math.random() * imgHeight * 2 - imgHeight * 0.5;
p.currentX = p.startX + (randX - p.startX) * ease;
p.currentY = p.startY + (randY - p.startY) * ease;
}
ctx.fillStyle = p.color;
ctx.globalAlpha = scattered ? adjustedProgress : 1 - adjustedProgress * 0.5;
ctx.fillRect(p.currentX, p.currentY, PIXEL_SIZE, PIXEL_SIZE);
}
ctx.globalAlpha = 1;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
animating = false;
scattered = !scattered;
}
}
function drawCurrent() {
ctx.clearRect(0, 0, imgWidth, imgHeight);
for (const p of pixels) {
ctx.fillStyle = p.color;
ctx.fillRect(p.currentX, p.currentY, PIXEL_SIZE, PIXEL_SIZE);
}
}
// Init
const imageData = generateDemoImage();
extractPixels(imageData);
drawCurrent();
// Auto-assemble on load
setTimeout(function () {
startAnimation();
}, 500);
// Click to toggle
document.querySelector(".pixel-wrapper").addEventListener("click", function () {
startAnimation();
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pixel Image</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="pixel-wrapper">
<canvas id="pixel-canvas"></canvas>
<div class="pixel-overlay">
<h1 class="pixel-title">Pixel Image</h1>
<p class="pixel-subtitle">Click anywhere to scatter & reassemble</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useEffect, useRef, useCallback, useState } from "react";
interface PixelImageProps {
width?: number;
height?: number;
pixelSize?: number;
animationDuration?: number;
className?: string;
}
interface Pixel {
targetX: number;
targetY: number;
currentX: number;
currentY: number;
startX: number;
startY: number;
color: string;
delay: number;
}
function easeOutCubic(t: number) {
return 1 - Math.pow(1 - t, 3);
}
function easeInCubic(t: number) {
return t * t * t;
}
export function PixelImage({
width = 400,
height = 300,
pixelSize = 4,
animationDuration = 2000,
className = "",
}: PixelImageProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const pixelsRef = useRef<Pixel[]>([]);
const animRef = useRef<number>(0);
const scatteredRef = useRef(true);
const animatingRef = useRef(false);
const generateAndExtract = useCallback(() => {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tctx = tempCanvas.getContext("2d")!;
const bg = tctx.createLinearGradient(0, 0, width, height);
bg.addColorStop(0, "#1e1b4b");
bg.addColorStop(0.3, "#312e81");
bg.addColorStop(0.6, "#4338ca");
bg.addColorStop(1, "#6366f1");
tctx.fillStyle = bg;
tctx.fillRect(0, 0, width, height);
for (let i = 0; i < 6; i++) {
const cx = width * (0.15 + Math.random() * 0.7);
const cy = height * (0.15 + Math.random() * 0.7);
const r = 20 + Math.random() * 60;
const grad = tctx.createRadialGradient(cx, cy, 0, cx, cy, r);
grad.addColorStop(0, `rgba(${167 + Math.random() * 60},${139 + Math.random() * 60},250,0.6)`);
grad.addColorStop(1, "transparent");
tctx.beginPath();
tctx.arc(cx, cy, r, 0, Math.PI * 2);
tctx.fillStyle = grad;
tctx.fill();
}
tctx.save();
tctx.translate(width / 2, height / 2);
tctx.fillStyle = "rgba(255,255,255,0.15)";
tctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const x = Math.cos(angle) * 50;
const y = Math.sin(angle) * 50;
if (i === 0) tctx.moveTo(x, y);
else tctx.lineTo(x, y);
}
tctx.closePath();
tctx.fill();
tctx.restore();
const imageData = tctx.getImageData(0, 0, width, height);
const data = imageData.data;
const pixels: Pixel[] = [];
for (let y = 0; y < height; y += pixelSize) {
for (let x = 0; x < width; x += pixelSize) {
const i = (y * width + x) * 4;
const r = data[i],
g = data[i + 1],
b = data[i + 2],
a = data[i + 3];
if (a < 10) continue;
pixels.push({
targetX: x,
targetY: y,
currentX: Math.random() * width * 2 - width * 0.5,
currentY: Math.random() * height * 2 - height * 0.5,
startX: 0,
startY: 0,
color: `rgba(${r},${g},${b},${a / 255})`,
delay: Math.random() * 0.3,
});
}
}
pixelsRef.current = pixels;
}, [width, height, pixelSize]);
const drawCurrent = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
for (const p of pixelsRef.current) {
ctx.fillStyle = p.color;
ctx.fillRect(p.currentX, p.currentY, pixelSize, pixelSize);
}
}, [width, height, pixelSize]);
const startAnimation = useCallback(() => {
if (animatingRef.current) return;
animatingRef.current = true;
const start = performance.now();
const scattered = scatteredRef.current;
for (const p of pixelsRef.current) {
p.startX = p.currentX;
p.startY = p.currentY;
}
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
function animate(now: number) {
const elapsed = now - start;
const progress = Math.min(elapsed / animationDuration, 1);
ctx!.clearRect(0, 0, width, height);
for (const p of pixelsRef.current) {
const adj = Math.max(0, Math.min(1, (progress - p.delay) / (1 - p.delay)));
if (scattered) {
const ease = easeOutCubic(adj);
p.currentX = p.startX + (p.targetX - p.startX) * ease;
p.currentY = p.startY + (p.targetY - p.startY) * ease;
} else {
const ease = easeInCubic(adj);
const rx = Math.random() * width * 2 - width * 0.5;
const ry = Math.random() * height * 2 - height * 0.5;
p.currentX = p.startX + (rx - p.startX) * ease;
p.currentY = p.startY + (ry - p.startY) * ease;
}
ctx!.fillStyle = p.color;
ctx!.globalAlpha = scattered ? adj : 1 - adj * 0.5;
ctx!.fillRect(p.currentX, p.currentY, pixelSize, pixelSize);
}
ctx!.globalAlpha = 1;
if (progress < 1) {
animRef.current = requestAnimationFrame(animate);
} else {
animatingRef.current = false;
scatteredRef.current = !scattered;
}
}
animRef.current = requestAnimationFrame(animate);
}, [animationDuration, width, height, pixelSize]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = width;
canvas.height = height;
generateAndExtract();
drawCurrent();
const timer = setTimeout(() => startAnimation(), 500);
return () => {
clearTimeout(timer);
cancelAnimationFrame(animRef.current);
};
}, [width, height, generateAndExtract, drawCurrent, startAnimation]);
return (
<canvas
ref={canvasRef}
className={className}
onClick={startAnimation}
style={{ cursor: "pointer", display: "block" }}
/>
);
}
// Demo usage
export default function PixelImageDemo() {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "#0a0a0a",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "1.5rem",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<PixelImage width={400} height={300} pixelSize={4} />
<div style={{ textAlign: "center" }}>
<h1
style={{
fontSize: "clamp(1.5rem, 4vw, 2.5rem)",
fontWeight: 800,
letterSpacing: "-0.03em",
background: "linear-gradient(135deg, #e0e7ff 0%, #818cf8 50%, #6366f1 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
marginBottom: "0.25rem",
}}
>
Pixel Image
</h1>
<p style={{ fontSize: "1rem", color: "rgba(148,163,184,0.7)" }}>
Click to scatter & reassemble
</p>
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
width: { type: Number, default: 400 },
height: { type: Number, default: 300 },
pixelSize: { type: Number, default: 4 },
animationDuration: { type: Number, default: 2000 },
});
const canvasRef = ref(null);
let pixels = [];
let animId = 0;
let scattered = true;
let animating = false;
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function easeInCubic(t) {
return t * t * t;
}
function generateAndExtract() {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = props.width;
tempCanvas.height = props.height;
const tctx = tempCanvas.getContext("2d");
const bg = tctx.createLinearGradient(0, 0, props.width, props.height);
bg.addColorStop(0, "#1e1b4b");
bg.addColorStop(0.3, "#312e81");
bg.addColorStop(0.6, "#4338ca");
bg.addColorStop(1, "#6366f1");
tctx.fillStyle = bg;
tctx.fillRect(0, 0, props.width, props.height);
for (let i = 0; i < 6; i++) {
const cx = props.width * (0.15 + Math.random() * 0.7);
const cy = props.height * (0.15 + Math.random() * 0.7);
const r = 20 + Math.random() * 60;
const grad = tctx.createRadialGradient(cx, cy, 0, cx, cy, r);
grad.addColorStop(0, `rgba(${167 + Math.random() * 60},${139 + Math.random() * 60},250,0.6)`);
grad.addColorStop(1, "transparent");
tctx.beginPath();
tctx.arc(cx, cy, r, 0, Math.PI * 2);
tctx.fillStyle = grad;
tctx.fill();
}
tctx.save();
tctx.translate(props.width / 2, props.height / 2);
tctx.fillStyle = "rgba(255,255,255,0.15)";
tctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const x = Math.cos(angle) * 50;
const y = Math.sin(angle) * 50;
if (i === 0) tctx.moveTo(x, y);
else tctx.lineTo(x, y);
}
tctx.closePath();
tctx.fill();
tctx.restore();
const imageData = tctx.getImageData(0, 0, props.width, props.height);
const data = imageData.data;
pixels = [];
for (let y = 0; y < props.height; y += props.pixelSize) {
for (let x = 0; x < props.width; x += props.pixelSize) {
const idx = (y * props.width + x) * 4;
const r = data[idx],
g = data[idx + 1],
b = data[idx + 2],
a = data[idx + 3];
if (a < 10) continue;
pixels.push({
targetX: x,
targetY: y,
currentX: Math.random() * props.width * 2 - props.width * 0.5,
currentY: Math.random() * props.height * 2 - props.height * 0.5,
startX: 0,
startY: 0,
color: `rgba(${r},${g},${b},${a / 255})`,
delay: Math.random() * 0.3,
});
}
}
}
function drawCurrent() {
const canvas = canvasRef.value;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, props.width, props.height);
for (const p of pixels) {
ctx.fillStyle = p.color;
ctx.fillRect(p.currentX, p.currentY, props.pixelSize, props.pixelSize);
}
}
function startAnimation() {
if (animating) return;
animating = true;
const start = performance.now();
const wasScattered = scattered;
for (const p of pixels) {
p.startX = p.currentX;
p.startY = p.currentY;
}
const canvas = canvasRef.value;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
function animate(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / props.animationDuration, 1);
ctx.clearRect(0, 0, props.width, props.height);
for (const p of pixels) {
const adj = Math.max(0, Math.min(1, (progress - p.delay) / (1 - p.delay)));
if (wasScattered) {
const ease = easeOutCubic(adj);
p.currentX = p.startX + (p.targetX - p.startX) * ease;
p.currentY = p.startY + (p.targetY - p.startY) * ease;
} else {
const ease = easeInCubic(adj);
const rx = Math.random() * props.width * 2 - props.width * 0.5;
const ry = Math.random() * props.height * 2 - props.height * 0.5;
p.currentX = p.startX + (rx - p.startX) * ease;
p.currentY = p.startY + (ry - p.startY) * ease;
}
ctx.fillStyle = p.color;
ctx.globalAlpha = wasScattered ? adj : 1 - adj * 0.5;
ctx.fillRect(p.currentX, p.currentY, props.pixelSize, props.pixelSize);
}
ctx.globalAlpha = 1;
if (progress < 1) {
animId = requestAnimationFrame(animate);
} else {
animating = false;
scattered = !wasScattered;
}
}
animId = requestAnimationFrame(animate);
}
let timer;
onMounted(() => {
const canvas = canvasRef.value;
if (!canvas) return;
canvas.width = props.width;
canvas.height = props.height;
generateAndExtract();
drawCurrent();
timer = setTimeout(() => startAnimation(), 500);
});
onUnmounted(() => {
clearTimeout(timer);
cancelAnimationFrame(animId);
});
</script>
<template>
<canvas
ref="canvasRef"
@click="startAnimation"
style="cursor: pointer; display: block"
/>
</template>
<style scoped>
</style><script>
import { onMount, onDestroy } from "svelte";
export let width = 400;
export let height = 300;
export let pixelSize = 4;
export let animationDuration = 2000;
export let className = "";
let canvasEl;
let pixels = [];
let animId = 0;
let scattered = true;
let animating = false;
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function easeInCubic(t) {
return t * t * t;
}
function generateAndExtract() {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tctx = tempCanvas.getContext("2d");
const bg = tctx.createLinearGradient(0, 0, width, height);
bg.addColorStop(0, "#1e1b4b");
bg.addColorStop(0.3, "#312e81");
bg.addColorStop(0.6, "#4338ca");
bg.addColorStop(1, "#6366f1");
tctx.fillStyle = bg;
tctx.fillRect(0, 0, width, height);
for (let i = 0; i < 6; i++) {
const cx = width * (0.15 + Math.random() * 0.7);
const cy = height * (0.15 + Math.random() * 0.7);
const r = 20 + Math.random() * 60;
const grad = tctx.createRadialGradient(cx, cy, 0, cx, cy, r);
grad.addColorStop(0, `rgba(${167 + Math.random() * 60},${139 + Math.random() * 60},250,0.6)`);
grad.addColorStop(1, "transparent");
tctx.beginPath();
tctx.arc(cx, cy, r, 0, Math.PI * 2);
tctx.fillStyle = grad;
tctx.fill();
}
tctx.save();
tctx.translate(width / 2, height / 2);
tctx.fillStyle = "rgba(255,255,255,0.15)";
tctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const x = Math.cos(angle) * 50;
const y = Math.sin(angle) * 50;
if (i === 0) tctx.moveTo(x, y);
else tctx.lineTo(x, y);
}
tctx.closePath();
tctx.fill();
tctx.restore();
const imageData = tctx.getImageData(0, 0, width, height);
const data = imageData.data;
pixels = [];
for (let y2 = 0; y2 < height; y2 += pixelSize) {
for (let x2 = 0; x2 < width; x2 += pixelSize) {
const idx = (y2 * width + x2) * 4;
const r = data[idx],
g = data[idx + 1],
b = data[idx + 2],
a = data[idx + 3];
if (a < 10) continue;
pixels.push({
targetX: x2,
targetY: y2,
currentX: Math.random() * width * 2 - width * 0.5,
currentY: Math.random() * height * 2 - height * 0.5,
startX: 0,
startY: 0,
color: `rgba(${r},${g},${b},${a / 255})`,
delay: Math.random() * 0.3,
});
}
}
}
function drawCurrent() {
if (!canvasEl) return;
const ctx = canvasEl.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
for (const p of pixels) {
ctx.fillStyle = p.color;
ctx.fillRect(p.currentX, p.currentY, pixelSize, pixelSize);
}
}
function startAnimation() {
if (animating) return;
animating = true;
const start = performance.now();
const wasScattered = scattered;
for (const p of pixels) {
p.startX = p.currentX;
p.startY = p.currentY;
}
if (!canvasEl) return;
const ctx = canvasEl.getContext("2d");
if (!ctx) return;
function animate(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / animationDuration, 1);
ctx.clearRect(0, 0, width, height);
for (const p of pixels) {
const adj = Math.max(0, Math.min(1, (progress - p.delay) / (1 - p.delay)));
if (wasScattered) {
const ease = easeOutCubic(adj);
p.currentX = p.startX + (p.targetX - p.startX) * ease;
p.currentY = p.startY + (p.targetY - p.startY) * ease;
} else {
const ease = easeInCubic(adj);
const rx = Math.random() * width * 2 - width * 0.5;
const ry = Math.random() * height * 2 - height * 0.5;
p.currentX = p.startX + (rx - p.startX) * ease;
p.currentY = p.startY + (ry - p.startY) * ease;
}
ctx.fillStyle = p.color;
ctx.globalAlpha = wasScattered ? adj : 1 - adj * 0.5;
ctx.fillRect(p.currentX, p.currentY, pixelSize, pixelSize);
}
ctx.globalAlpha = 1;
if (progress < 1) {
animId = requestAnimationFrame(animate);
} else {
animating = false;
scattered = !wasScattered;
}
}
animId = requestAnimationFrame(animate);
}
onMount(() => {
canvasEl.width = width;
canvasEl.height = height;
generateAndExtract();
drawCurrent();
const timer = setTimeout(() => startAnimation(), 500);
return () => {
clearTimeout(timer);
cancelAnimationFrame(animId);
};
});
onDestroy(() => {
cancelAnimationFrame(animId);
});
</script>
<canvas
bind:this={canvasEl}
class={className}
on:click={startAnimation}
style="cursor: pointer; display: block;"
/>Pixel Image
A stunning pixel assembly animation where an image materializes from randomly scattered pixels that fly into their correct positions. Click to scatter and reassemble.
How it works
- An image is loaded and drawn onto a hidden canvas to extract pixel data
- Each pixel (sampled at intervals for performance) is assigned a random scattered position
- On trigger, pixels animate from scattered positions to their original coordinates using easing
- The reverse scatters them again with physics-like motion
Customization
src— any image URL (loaded via canvas)pixelSize— sampling resolution (larger = fewer pixels, faster)animationDuration— time for full assembly- Trigger mode: on mount, on click, or on scroll
When to use it
- Portfolio hero images
- Product reveals
- Interactive art installations
- Loading state transitions