Try it live at 30days.abduarrahman.com — and the source code is on GitHub.
The Origin
Day 4 needed a splash screen. A proper, cinematic intro — not just a loading spinner. I'm a One Piece fan, and Zoro's Santoryu (Three-Sword Style) is iconic. What if the splash screen was Zoro running, then slashing the screen three times until it shatters?
The result: a multi-phase splash screen with pixel art Zoro running, a loading bar, a dramatic Santoryu reveal, three timed slashes with screen shake, and a shatter effect that breaks the screen into 9 fragments.
What I Built
A cinematic splash screen with 5 phases:
- Tap to Start — Pixel art Zoro running animation on a clean white screen
- Loading — Progress bar fills over 5 seconds while Zoro keeps running, ambient music plays
- Santoryu — Loading music fades out, dramatic Santoryu image zooms in with impact
- Slash — Three timed sword slashes with blade tips, scar lines, sparks, and screen shake
- Shatter — Screen breaks into 9 fragments that fly apart with debris particles
All powered by a single ZoroPixelLoader canvas component and Framer Motion for phase transitions.
How It Works
Canvas Sprite Animator
The ZoroPixelLoader renders pixel art sprite sheets onto a canvas with frame-by-frame animation:
const SPRITES: Record<string, SpriteConfig> = {
run: { src: "/zoro_run.png", frameW: 445, frameH: 363 },
slash: { src: "/zoro.png", frameW: 290, frameH: 349 },
};
export default function ZoroPixelLoader({ sprite = "run", fps = 14, scale = 0.5 }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const frameRef = useRef(0);
const rafRef = useRef<number>(0);
const lastTimeRef = useRef(0);
const config = SPRITES[sprite];
const fpsInterval = 1000 / fps;
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const img = new Image();
img.src = config.src;
const animate = (time: number) => {
const delta = time - lastTimeRef.current;
if (delta >= fpsInterval) {
lastTimeRef.current = time - (delta % fpsInterval);
const frame = frameRef.current;
const col = frame % COLS;
const row = Math.floor(frame / COLS);
ctx!.clearRect(0, 0, config.frameW, config.frameH);
ctx!.drawImage(
img,
col * config.frameW, row * config.frameH,
config.frameW, config.frameH,
0, 0, config.frameW, config.frameH
);
frameRef.current = (frame + 1) % TOTAL_FRAMES;
}
rafRef.current = requestAnimationFrame(animate);
};
img.onload = () => {
ctx!.imageSmoothingEnabled = false; // crisp pixel art
rafRef.current = requestAnimationFrame(animate);
};
return () => cancelAnimationFrame(rafRef.current);
}, [sprite, config, fpsInterval]);
return (
<canvas
ref={canvasRef}
width={config.frameW}
height={config.frameH}
style={{ imageRendering: "pixelated", width: config.frameW * scale, height: config.frameH * scale }}
/>
);
}
The key detail: imageSmoothingEnabled = false and imageRendering: "pixelated" keep the pixel art crisp at any scale.
Slash Sequence Timing
Three slashes are precisely timed using constants:
const SLASH_SOUND_DUR = 576; // ms — matches the slash sound effect length
const NUM_SLASHES = 3;
const SLASH_INTERVAL = SLASH_SOUND_DUR; // each slash starts after previous sound ends
const TOTAL_SLASH_TIME = NUM_SLASHES * SLASH_INTERVAL;
const SANTORYU_DUR = 2736; // ms — duration of the Santoryu vocal sample
const SLASHES = [
{ angle: -30, delay: 0 },
{ angle: 15, delay: SLASH_INTERVAL },
{ angle: -50, delay: SLASH_INTERVAL * 2 },
];
Each slash has a blade tip that sweeps across, a persistent scar line, sparks, and a white flash.
Screen Shatter Physics
After the three slashes, the screen shatters into 9 fragments using CSS clipPath polygons:
const FRAGMENTS = [
{ clipPath: "polygon(0 0, 33% 0, 33% 33%, 0 33%)", origin: "0% 0%", x: "-20%", y: "-30%", rot: -6 },
{ clipPath: "polygon(33% 0, 66% 0, 66% 33%, 33% 33%)", origin: "50% 16%", x: "0%", y: "-35%", rot: 3 },
{ clipPath: "polygon(66% 0, 100% 0, 100% 33%, 66% 33%)", origin: "100% 0%", x: "25%", y: "-25%", rot: 8 },
// ... 6 more fragments covering the full screen
];
// Each fragment animates away from its transform origin
Audio Transitions
Three audio tracks crossfade during the sequence:
- Loading music — loops during the progress bar phase, fades out over ~600ms when Santoryu triggers
- Santoryu vocal — plays once during the reveal image
- Slash sounds — three precisely timed hits, one per slash
const triggerSantoryu = useCallback(() => {
// Fade out loading music
const loadAudio = loadingAudioRef.current;
if (loadAudio) {
const fade = setInterval(() => {
if (loadAudio.volume > 0.05) {
loadAudio.volume = Math.max(0, loadAudio.volume - 0.05);
} else {
loadAudio.pause();
clearInterval(fade);
}
}, 30);
}
// Play Santoryu vocal
const santoryu = new Audio(SANTORYU_MUSIC);
santoryu.volume = 0.8;
santoryu.play().catch(() => {});
// Start slashes after vocal ends
santoryu.onended = () => startSlash();
}, [onComplete]);
Tech Stack
| Technology | Purpose |
|---|---|
| Next.js | React framework |
| TypeScript | Type-safe phase management |
| HTML Canvas | Pixel art sprite animation (ZoroPixelLoader) |
| Framer Motion | Phase transitions, screen shake, fragment animations |
| CSS clipPath | Screen shatter fragments |
| Web Audio | Music crossfade, timed slash sounds |
Links
- Live Demo: 30days.abduarrahman.com
- Source Code: github.com/ab2rahman/30days-web-challenge
-
Key Files:
SwordSplash.tsx,ZoroPixelLoader.tsx
Follow the challenge:
- Instagram: @abduarrahman
- YouTube: @abduarrahmanscode
- TikTok: @anduarrahmans
Support the challenge:
- Ko-fi: ko-fi.com/abduarrahman
Originally published at abduarrahman.com
Top comments (0)