Building a Japanese-inspired Snow vs. Sakura toggle with Three.js

After years of seeing the same light/dark mode toggles everywhere, I wanted something different for my personal site. Here's how I built a Japanese-inspired seasonal theme toggle using Three.js that switches between animated snow and sakura (cherry blossom) effects.
The core concept
Rather than switching between light and dark, site toggles between:
- A winter scene with falling snowflakes and cooler blue tones
- A spring scene with floating cherry blossom petals and soft pink accents
Each comes with matching color schemes and ambient audio that transitions smoothly when switching.
Deep Dive - Physics & Geometry
Particle physics for snow
Snow particles in nature follow fairly predictable physics. This implementation models several key behaviors:
// Snow particle motion in the animation loop
flake.position.y -= flake.userData.speed; // Gravity effect
// Sine wave wobble using time-based oscillation
flake.position.x += Math.sin(Date.now() * flake.userData.wobbleSpeed +
flake.userData.wobbleOffset) * flake.userData.wobbleAmount;
// Each flake has unique parameters
flake.userData = {
speed: THREE.MathUtils.randFloat(0.008, 0.02), // Terminal velocity
wobbleSpeed: THREE.MathUtils.randFloat(0.001, 0.005), // Oscillation frequency
wobbleAmount: THREE.MathUtils.randFloat(0.002, 0.01), // Oscillation amplitude
wobbleOffset: Math.random() * Math.PI * 2 // Phase shift
};
Particle physics for snow
The key physics principles at work here:
- Terminal velocity: Heavier snowflakes fall faster
- Air resistance: Creates lateral movement
- Random oscillation: Every flake follows a unique path
- Phase offsets: Prevents unnatural synchronization
Sakura petal geometry
Creating realistic petal shapes requires translating different botanical forms to geometry:
// Creating a petal shape using bezier curves
const petalShape = new THREE.Shape();
// Start at the base
petalShape.moveTo(0, 0);
// Curve outward and upward to the tip
petalShape.bezierCurveTo(0.15, 0.1, 0.2, 0.15, 0.15, 0.3);
// Curve back down on the other side
petalShape.bezierCurveTo(0.1, 0.4, -0.1, 0.4, -0.15, 0.3);
// Complete the shape back to the base
petalShape.bezierCurveTo(-0.2, 0.15, -0.15, 0.1, 0, 0);
Sakura petal geometry
The bezier curve parameters aren't random. They're designed to mimic the slight asymmetry of natural petals:
- The control points create the characteristic gentle curvature
- The slight asymmetry makes the petals look organic
- Using multiple petal shapes adds visual diversity
Wind dynamics
For the sakura petals, to simulate the interaction between petals and air:
// Sway motion combining multiple oscillations
petal.position.x += Math.sin(Date.now() * petal.userData.swaySpeed +
petal.userData.swayOffset) * petal.userData.swayAmount;
// Slower oscillation on z-axis for depth
petal.position.z += Math.cos(Date.now() * petal.userData.swaySpeed * 0.7 +
petal.userData.swayOffset) * petal.userData.swayAmount * 0.5;
// Multi-axis rotation simulates tumbling in the air
petal.rotation.x += petal.userData.rotationSpeedX;
petal.rotation.y += petal.userData.rotationSpeedY;
petal.rotation.z += petal.userData.rotationSpeedZ;
Wind dynamics: interaction between petals and air
This creates several realistic effects:
- Larger movements occur at slower frequencies
- Creates more unpredictable paths
- Rotation affects how the petal catches the wind
Visual realism : material properties
For visual believability, materials are crucial (these could be enhanced by using right images):
// Snow with slight transparency
const snowMaterial = new THREE.MeshBasicMaterial({
color: 0xe6f0ff,
transparent: true,
opacity: 0.98
});
// Sakura petals with double-sided rendering
const petalMaterial = new THREE.MeshBasicMaterial({
color: 0xff8fab,
transparent: true,
opacity: 0.9,
side: THREE.DoubleSide
});
Visual realism: Transparent snow vs double-sided sakura petals
Performance optimizations
Three.js performance requires understanding the rendering pipeline:
// Instead of creating new particles, recycle existing ones
if (flake.position.y < -8) {
flake.position.y = 18; // Move back to the top
flake.position.x = THREE.MathUtils.randFloatSpread(18);
flake.position.z = THREE.MathUtils.randFloatSpread(18);
}
Three.js performance optimization
This recycling approach avoids:
- Expensive garbage collection from destroying/creating objects
- Memory fragmentation from constant allocation/deallocation
- CPU spikes that would cause animation stutters
Beyond light / dark mode
This project reminded me why I fell in love with coding 2d / 3d graphics, game programming and visual systems years ago.
There's something deeply satisfying about building visual systems where code translates directly into motion. If you're looking for a break from your typical web development routine, I'd recommend playing with Three.js. It's accessible enough for quick experiments but deep enough for serious projects.
If you want to see it in action, check out exitcode42.com