Zoro Santoryu Splash Screen β 30 Days Web Challenge Day 4
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 }}
/>
);
}
Enter fullscreen mode Exit fullscreen mode
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 },
];
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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]);
Enter fullscreen mode Exit fullscreen mode
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
