Zoro Santoryu Splash Screen — 30 Days Web Challenge Day 4
A cinematic splash screen featuring pixel art Zoro running across the screen, then unleashing Santoryu — three sword slashes that shatter the page with screen shake, debris particles, and music transitions.
// table of contents (10 sections)
Try it live at 30days.abduarrahman.com and the source code is on GitHub.
Live Demo
Tap to start, watch Zoro run, then unleash Santoryu — three slashes that shatter the white screen and reveal the challenge beneath:
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:
{/* Blade tip — sweeps across */}
<motion.div
style={{
width: "30%", height: "6px",
background: "linear-gradient(90deg, transparent, #444 20%, #000 50%, #444 80%, transparent)",
transform: `rotate(${s.angle}deg)`,
}}
initial={{ x: "-150%", opacity: 0 }}
animate={{ x: "500%", opacity: [0, 1, 1, 0] }}
transition={{ duration: 0.35, delay: s.delay / 1000 }}
/>
{/* Scar line — stays visible */}
<motion.div
style={{
width: "120%", height: "6px", background: "#000",
transform: `rotate(${s.angle}deg)`,
}}
initial={{ scaleX: 0, opacity: 0 }}
animate={{ scaleX: 1, opacity: 1 }}
transition={{ duration: 0.25, delay: s.delay / 1000 }}
/>
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
<motion.div
style={{ background: "#FFF", clipPath: f.clipPath, transformOrigin: f.origin }}
initial={{ x: 0, y: 0, rotate: 0, opacity: 1 }}
animate={{ x: f.x, y: f.y, rotate: f.rot, opacity: 0 }}
transition={{ duration: 0.7, delay: f.delay, ease: [0.22, 1, 0.36, 1] }}
/>
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
You might also like
Granloop Arena TV — Pixel Art Fighting Game — 30 Days Web Challenge Day 3
A pixel art 2v2 fighting game running inside a draggable TV overlay. Four characters — two Granmaja vs two Bloop — bounce around an arena, deal random damage on collision, and fight until one team is KO'd.
Magikarp Countdown Challenge — 30 Days Web Challenge Day 1
Roll 4 dice to get a random target number, then try to stop a timer at exactly that many milliseconds. Hit it and watch the countdown drop. Miss it and time goes up. Catch the hidden Magikarp to begin!
ASCII Donut Math Animation — 30 Days Web Challenge Day 2
The classic donut.c torus rendered in real-time ASCII art with sin/cos math, z-buffer depth, and luminance mapping — with a glitch mode that scrambles into chaos when you click it.
More Posts
API Gateway Patterns: The Front Door to Your Microservices
Web Components 2026: Building Framework-Agnostic UI Libraries
Building Autonomous AI Workflows with LangGraph: A Practical Guide
Building Type-Safe APIs with tRPC in 2026: Full-Stack TypeScript Without Schemas
Database Connection Pooling: Patterns for High-Performance Applications
Prompt Caching: Reduce LLM Costs by 90% with Smart Context Management
Enjoyed This Post?
Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.
