3D shard explosion triggered on click — add explosive interactions to your site
Click the image to trigger the explosion
Copy URL or Code
Paste to your AI coding assistant and say:
“Add this explode effect to my logo”
Works with images, text, logos, cards, buttons — any element
Done. Your AI handles the rest.
Make it yours. Tweak explosion intensity, shard shapes, colors, gravity, and reform timing. Add satisfying interactions to any clickable element.
/*
================================================================================
AI COMPONENT: ExplodeEffect
================================================================================
SETUP:
1. Create file: components/ExplodeEffect.tsx
2. Copy this entire code block into that file
3. Import: import ExplodeEffect from "@/components/ExplodeEffect"
4. No external dependencies required - pure React + Canvas
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default | Description |
|-------------------|-------------------------------|----------------------|--------------------------------|
| text | string | "CLICK ME" | Text to display and explode |
| fontSize | number | 80 | Font size in pixels |
| shardBaseSize | number | 8 | Base size of shards |
| shardSizeVariance | number | 6 | Variance in shard sizes |
| shardSpacing | number | 10 | Spacing between shards |
| explodeForceX | number | 18 | Horizontal explosion force |
| explodeForceY | number | 25 | Vertical explosion force |
| explodeForceZ | number | 15 | Depth explosion force |
| reformDelay | number | 1000 | Delay before reforming (ms) |
| gravity | number | 0.4 | Gravity strength |
| baseColor | {r: number, g: number, b: number} | {r:255,g:255,b:255} | Base color RGB |
| backgroundColor | string | "#0a0a0f" | Background color |
| className | string | "" | Additional CSS classes |
| imageSrc | string | - | Image URL (use instead of text)|
| imageWidth | number | 200 | Image width |
| imageHeight | number | 200 | Image height |
================================================================================
COMMON MODIFICATIONS
================================================================================
1. CHANGE TEXT COLOR TO CYAN:
<ExplodeEffect
text="BOOM"
baseColor={{ r: 0, g: 255, b: 255 }}
/>
2. MORE POWERFUL EXPLOSION:
<ExplodeEffect
text="KABOOM"
explodeForceX={30}
explodeForceY={40}
explodeForceZ={25}
/>
3. SLOWER REFORM:
<ExplodeEffect
text="SLOW MO"
reformDelay={3000}
/>
4. WITH IMAGE INSTEAD OF TEXT:
<ExplodeEffect
imageSrc="/logo.png"
imageWidth={150}
imageHeight={150}
/>
5. SMALLER SHARDS:
<ExplodeEffect
text="FINE"
shardBaseSize={4}
shardSpacing={6}
/>
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import { useEffect, useRef, useCallback } from "react";
export interface ExplodeEffectProps {
text?: string;
fontSize?: number;
shardBaseSize?: number;
shardSizeVariance?: number;
shardSpacing?: number;
explodeForceX?: number;
explodeForceY?: number;
explodeForceZ?: number;
reformDelay?: number;
gravity?: number;
baseColor?: { r: number; g: number; b: number };
backgroundColor?: string;
className?: string;
imageSrc?: string;
imageWidth?: number;
imageHeight?: number;
}
interface ShardConfig {
shardBaseSize: number;
shardSizeVariance: number;
shardSpacing: number;
shardTypes: string[];
explodeForceX: number;
explodeForceY: number;
explodeForceZ: number;
reformDelay: number;
gravity: number;
rotationSpeed: number;
airResistance: number;
returnSpeed: number;
friction: number;
perspective: number;
maxZ: number;
baseColor: { r: number; g: number; b: number };
backgroundColor: string;
}
class Shard {
config: ShardConfig;
originX: number;
originY: number;
originZ: number;
x: number;
y: number;
z: number;
vx: number;
vy: number;
vz: number;
rotation: number;
rotationVelocity: number;
tilt: number;
size: number;
sides: number;
vertices: { x: number; y: number }[];
color: { r: number; g: number; b: number };
constructor(x: number, y: number, config: ShardConfig, color?: { r: number; g: number; b: number }) {
this.config = config;
this.originX = x;
this.originY = y;
this.originZ = 0;
this.x = x;
this.y = y;
this.z = 0;
this.vx = 0;
this.vy = 0;
this.vz = 0;
this.rotation = Math.random() * Math.PI * 2;
this.rotationVelocity = 0;
this.tilt = Math.random() * 0.5;
this.size = config.shardBaseSize + Math.random() * config.shardSizeVariance;
this.sides = this.getRandomSides();
this.vertices = this.generateVertices();
this.color = color || config.baseColor;
}
getRandomSides(): number {
const type = this.config.shardTypes[Math.floor(Math.random() * this.config.shardTypes.length)];
switch (type) {
case "triangle": return 3;
case "quad": return 4;
case "pentagon": return 5;
default: return 3;
}
}
generateVertices(): { x: number; y: number }[] {
const vertices: { x: number; y: number }[] = [];
const angleStep = (Math.PI * 2) / this.sides;
for (let i = 0; i < this.sides; i++) {
const angle = angleStep * i;
const radius = this.size * (0.7 + Math.random() * 0.6);
vertices.push({ x: Math.cos(angle) * radius, y: Math.sin(angle) * radius });
}
return vertices;
}
explode(): void {
const angle = Math.random() * Math.PI * 2;
const upwardBias = -Math.abs(Math.random());
this.vx = Math.cos(angle) * (Math.random() * this.config.explodeForceX);
this.vy = upwardBias * this.config.explodeForceY - Math.random() * 5;
this.vz = (Math.random() - 0.5) * 2 * this.config.explodeForceZ;
this.rotationVelocity = (Math.random() - 0.5) * this.config.rotationSpeed * 2;
}
update(isExploded: boolean): void {
if (isExploded) {
this.vy += this.config.gravity;
this.vx *= this.config.airResistance;
this.vy *= this.config.airResistance;
this.vz *= this.config.airResistance;
} else {
const dx = this.originX - this.x;
const dy = this.originY - this.y;
const dz = this.originZ - this.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < 2) {
this.x = this.originX;
this.y = this.originY;
this.z = this.originZ;
this.vx = 0;
this.vy = 0;
this.vz = 0;
this.rotationVelocity = 0;
this.rotation = 0;
return;
}
this.vx += dx * this.config.returnSpeed;
this.vy += dy * this.config.returnSpeed;
this.vz += dz * this.config.returnSpeed;
this.rotationVelocity *= 0.85;
}
this.vx *= this.config.friction;
this.vy *= this.config.friction;
this.vz *= this.config.friction;
this.x += this.vx;
this.y += this.vy;
this.z += this.vz;
this.z = Math.max(-this.config.maxZ, Math.min(this.config.maxZ, this.z));
this.rotation += this.rotationVelocity;
}
draw(ctx: CanvasRenderingContext2D, centerX: number, centerY: number): void {
const perspectiveScale = this.config.perspective / (this.config.perspective + this.z);
const screenX = centerX + (this.x - centerX) * perspectiveScale;
const screenY = centerY + (this.y - centerY) * perspectiveScale;
const depthFactor = Math.max(0.2, Math.min(1, perspectiveScale));
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate(this.rotation);
ctx.scale(perspectiveScale, perspectiveScale * (1 - this.tilt * Math.abs(Math.sin(this.rotation))));
ctx.beginPath();
ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
for (let i = 1; i < this.vertices.length; i++) {
ctx.lineTo(this.vertices[i].x, this.vertices[i].y);
}
ctx.closePath();
const { r, g, b } = this.color;
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${depthFactor})`;
ctx.fill();
if (depthFactor > 0.3) {
ctx.strokeStyle = `rgba(${Math.min(255, r + 50)}, ${Math.min(255, g + 50)}, ${Math.min(255, b + 50)}, ${depthFactor * 0.5})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
ctx.restore();
}
}
export function ExplodeEffect({
text = "CLICK ME",
fontSize = 80,
shardBaseSize = 8,
shardSizeVariance = 6,
shardSpacing = 10,
explodeForceX = 18,
explodeForceY = 25,
explodeForceZ = 15,
reformDelay = 1000,
gravity = 0.4,
baseColor = { r: 255, g: 255, b: 255 },
backgroundColor = "#0a0a0f",
className = "",
imageSrc,
imageWidth = 200,
imageHeight = 200,
}: ExplodeEffectProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const shardsRef = useRef<Shard[]>([]);
const stateRef = useRef({
isExploded: false,
isAnimating: false,
isReforming: false,
showShards: false,
textOpacity: 1,
centerX: 0,
centerY: 0,
canvasWidth: 0,
canvasHeight: 0,
imageLoaded: false,
image: null as HTMLImageElement | null,
});
const reformTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const animationRef = useRef<number | null>(null);
const config: ShardConfig = {
shardBaseSize,
shardSizeVariance,
shardSpacing,
shardTypes: ["triangle", "quad", "pentagon"],
explodeForceX,
explodeForceY,
explodeForceZ,
reformDelay,
gravity,
rotationSpeed: 0.15,
airResistance: 0.98,
returnSpeed: 0.12,
friction: 0.88,
perspective: 400,
maxZ: 200,
baseColor,
backgroundColor,
};
const drawText = useCallback((ctx: CanvasRenderingContext2D, opacity: number = 1) => {
const state = stateRef.current;
const { r, g, b } = baseColor;
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, state.centerX, state.centerY);
}, [text, fontSize, baseColor]);
const drawImage = useCallback((ctx: CanvasRenderingContext2D, opacity: number = 1) => {
const state = stateRef.current;
if (state.image && state.imageLoaded) {
ctx.globalAlpha = opacity;
const x = state.centerX - imageWidth / 2;
const y = state.centerY - imageHeight / 2;
ctx.drawImage(state.image, x, y, imageWidth, imageHeight);
ctx.globalAlpha = 1;
}
}, [imageWidth, imageHeight]);
const explode = useCallback(() => {
const state = stateRef.current;
if (state.isAnimating) return;
state.isAnimating = true;
state.isExploded = true;
state.isReforming = false;
state.showShards = true;
state.textOpacity = 0;
shardsRef.current.forEach((s) => {
s.x = s.originX;
s.y = s.originY;
s.z = 0;
s.rotation = Math.random() * Math.PI * 2;
s.explode();
});
if (reformTimeoutRef.current) clearTimeout(reformTimeoutRef.current);
reformTimeoutRef.current = setTimeout(() => {
state.isExploded = false;
state.isReforming = true;
}, config.reformDelay);
}, [config.reformDelay]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const state = stateRef.current;
const initWithText = () => {
const temp = document.createElement("canvas");
const tempCtx = temp.getContext("2d");
if (!tempCtx) return;
tempCtx.font = `bold ${fontSize}px system-ui, sans-serif`;
const metrics = tempCtx.measureText(text);
temp.width = Math.ceil(metrics.width) + 40;
temp.height = fontSize + 60;
tempCtx.fillStyle = backgroundColor;
tempCtx.fillRect(0, 0, temp.width, temp.height);
tempCtx.font = `bold ${fontSize}px system-ui, sans-serif`;
tempCtx.fillStyle = "#ffffff";
tempCtx.textAlign = "center";
tempCtx.textBaseline = "middle";
tempCtx.fillText(text, temp.width / 2, temp.height / 2);
canvas.width = temp.width;
canvas.height = temp.height;
state.canvasWidth = temp.width;
state.canvasHeight = temp.height;
state.centerX = temp.width / 2;
state.centerY = temp.height / 2;
const imageData = tempCtx.getImageData(0, 0, temp.width, temp.height);
const data = imageData.data;
shardsRef.current = [];
const spacing = config.shardSpacing;
for (let y = 0; y < temp.height; y += spacing) {
for (let x = 0; x < temp.width; x += spacing) {
const i = (y * temp.width + x) * 4;
if (data[i] > 128) shardsRef.current.push(new Shard(x, y, config));
}
}
};
const initWithImage = (img: HTMLImageElement) => {
const temp = document.createElement("canvas");
const tempCtx = temp.getContext("2d");
if (!tempCtx) return;
const padding = 40;
temp.width = imageWidth + padding * 2;
temp.height = imageHeight + padding * 2;
tempCtx.fillStyle = backgroundColor;
tempCtx.fillRect(0, 0, temp.width, temp.height);
tempCtx.drawImage(img, padding, padding, imageWidth, imageHeight);
canvas.width = temp.width;
canvas.height = temp.height;
state.canvasWidth = temp.width;
state.canvasHeight = temp.height;
state.centerX = temp.width / 2;
state.centerY = temp.height / 2;
state.image = img;
state.imageLoaded = true;
const imageData = tempCtx.getImageData(0, 0, temp.width, temp.height);
const data = imageData.data;
shardsRef.current = [];
const spacing = config.shardSpacing;
for (let y = 0; y < temp.height; y += spacing) {
for (let x = 0; x < temp.width; x += spacing) {
const i = (y * temp.width + x) * 4;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
if (a > 50 && (r > 20 || g > 20 || b > 20)) {
shardsRef.current.push(new Shard(x, y, config, { r, g, b }));
}
}
}
};
if (imageSrc) {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => initWithImage(img);
img.src = imageSrc;
} else {
initWithText();
}
const animate = () => {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (state.showShards) {
shardsRef.current.forEach((s) => s.update(state.isExploded));
if (state.isReforming) {
let maxDist = 0;
shardsRef.current.forEach((s) => {
const dx = s.originX - s.x;
const dy = s.originY - s.y;
const dz = s.originZ - s.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
maxDist = Math.max(maxDist, dist);
});
if (maxDist < 15) {
state.showShards = false;
state.textOpacity = 1;
state.isReforming = false;
state.isAnimating = false;
}
}
const sorted = [...shardsRef.current].sort((a, b) => b.z - a.z);
sorted.forEach((s) => s.draw(ctx, state.centerX, state.centerY));
} else {
if (imageSrc && state.imageLoaded) {
drawImage(ctx, state.textOpacity);
} else {
drawText(ctx, state.textOpacity);
}
}
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) cancelAnimationFrame(animationRef.current);
if (reformTimeoutRef.current) clearTimeout(reformTimeoutRef.current);
};
}, [text, fontSize, backgroundColor, config, drawText, drawImage, imageSrc, imageWidth, imageHeight]);
return (
<canvas
ref={canvasRef}
onClick={explode}
className={`cursor-pointer ${className}`}
style={{ display: "block" }}
/>
);
}
export default ExplodeEffect;import ExplodeEffect from "@/components/ExplodeEffect";
// Basic usage - text that explodes on click
export default function Hero() {
return (
<div className="h-screen bg-black flex items-center justify-center">
<ExplodeEffect text="WELCOME" fontSize={100} />
</div>
);
}
// Colorful explosion
export function ColorfulExplosion() {
return (
<ExplodeEffect
text="BOOM"
fontSize={80}
baseColor={{ r: 255, g: 100, b: 100 }}
backgroundColor="#1a1a2e"
/>
);
}
// With image instead of text
export function ImageExplosion() {
return (
<ExplodeEffect
imageSrc="/logo.png"
imageWidth={200}
imageHeight={200}
backgroundColor="#000000"
/>
);
}
// More explosive settings
export function PowerfulExplosion() {
return (
<ExplodeEffect
text="KABOOM"
explodeForceX={30}
explodeForceY={45}
explodeForceZ={25}
reformDelay={2000}
/>
);
}