DEV Community

Cover image for Zoro Santoryu Splash Screen — 30 Days Web Challenge Day 4
Muhammad Abdu ar Rahman
Muhammad Abdu ar Rahman

Posted on • Originally published at abduarrahman.com

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:

  1. Tap to Start — Pixel art Zoro running animation on a clean white screen
  2. Loading — Progress bar fills over 5 seconds while Zoro keeps running, ambient music plays
  3. Santoryu — Loading music fades out, dramatic Santoryu image zooms in with impact
  4. Slash — Three timed sword slashes with blade tips, scar lines, sparks, and screen shake
  5. 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

Follow the challenge:

Support the challenge:


Originally published at abduarrahman.com

Top comments (0)