TL;DR
What I did: Added a double pendulum animation to the blog header, replacing the original emoji.
Technical highlights:
- Used Lagrangian mechanics to derive equations of motion, fourth-order Runge-Kutta integration ensures numerical stability
- Global singleton pattern preserves state, pendulum continues motion from current position during page navigation
- Nonlinear deceleration stop (easeOutCubic) when entering article pages to avoid distracting reading attention
Design philosophy: Animation should be the icing on the cake, not stealing the show.
Why a Double Pendulum?
The double pendulum is a classic demonstration system in chaos theory. Two simple pendulums connected in series can produce extremely complex, never-repeating motion trajectories. It perfectly illustrates the philosophy of “simple rules producing complex behavior”—which is also my understanding of programming and writing.
And besides, double pendulums look cool.
Phase One: Making It Move
Physical Modeling
The equations of motion for a double pendulum can be derived using Lagrangian mechanics. Let the angles of the two pendulums be θ₁ and θ₂, angular velocities be ω₁ and ω₂, then the angular accelerations are:
function derivatives(θ1, θ2, ω1, ω2) {
const Δ = θ2 - θ1;
const sinΔ = Math.sin(Δ), cosΔ = Math.cos(Δ);
const sin1 = Math.sin(θ1), sin2 = Math.sin(θ2);
const M = m1 + m2;
const d = m1 + m2 * sinΔ * sinΔ;
const α1 = (m2 * g * Math.sin(θ2) * cosΔ
- m2 * sinΔ * (l1 * ω1 * ω1 * cosΔ + l2 * ω2 * ω2)
- M * g * sin1) / (l1 * d);
const α2 = (M * (l1 * ω1 * ω1 * sinΔ - g * sin2 + g * sin1 * cosΔ)
+ m2 * l2 * ω2 * ω2 * sinΔ * cosΔ) / (l2 * d);
return { α1, α2 };
}
These equations look complex, but they’re essentially Newton’s second law applied to rotational systems.
Numerical Integration: Runge-Kutta Method
Simple Euler method (θ += ω * dt) rapidly accumulates errors in chaotic systems like double pendulums, leading to energy non-conservation. I used fourth-order Runge-Kutta (RK4) integration:
const k1 = derivatives(θ1, θ2, ω1, ω2);
const k2 = derivatives(θ1 + 0.5*dt*ω1, θ2 + 0.5*dt*ω2, ...);
const k3 = derivatives(...);
const k4 = derivatives(...);
ω1 += (dt/6) * (k1.α1 + 2*k2.α1 + 2*k3.α1 + k4.α1);
ω2 += (dt/6) * (k1.α2 + 2*k2.α2 + 2*k3.α2 + k4.α2);
RK4 significantly improves precision by sampling slopes multiple times within each time step.
First Problem: Choppy Motion
Initially I limited it to 30fps to “save performance,” but the animation looked choppy. Double pendulums have highly variable motion speeds—low frame rates make the fast-moving parts appear jumpy.
Solution: Use fixed-timestep physics simulation + 60fps rendering.
const physicsStep = 0.008; // Fixed physics timestep
let accumulator = 0;
function animate(currentTime) {
const deltaTime = (currentTime - lastTime) / 1000;
accumulator += deltaTime;
// Physics simulation uses fixed timestep
while (accumulator >= physicsStep) {
step(physicsStep);
accumulator -= physicsStep;
}
draw(); // Rendering uses actual frame rate
requestAnimationFrame(animate);
}
This way physics simulation precision isn’t affected by frame rate, while rendering stays consistently smooth.
Phase Two: Making It Stop at the Right Time
Problem: Animation Distracts Attention
The double pendulum animation is cool on the homepage, but when users click into an article to read, a constantly swaying object becomes a distraction.
I wanted:
- On homepage/list pages: pendulum swings normally
- Entering article pages: pendulum gradually stops
- Returning to homepage: pendulum resumes swinging
Nonlinear Stopping
Directly “freezing” the pendulum would be jarring. I chose the easeOutCubic easing function for nonlinear deceleration:
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// Apply damping in physics simulation
if (isStopping) {
stopElapsed += dt;
const progress = Math.min(stopElapsed / 1.5, 1.0); // 1.5 second stop
damping = 1.0 - easeOutCubic(progress);
ω1 = ω1 * damping;
ω2 = ω2 * damping;
}
The characteristic of easeOutCubic is “fast start, slow finish”—the pendulum decelerates rapidly, then slowly settles into a position. This feels more physically intuitive than linear deceleration (imagine how a pendulum stops under air resistance).
The Challenge of State Persistence
Astro’s page navigation re-executes scripts, causing the pendulum’s state (angles, angular velocities) to be lost. Initially I tried using sessionStorage to save state:
// Save before navigation
sessionStorage.setItem('pendulum-state', JSON.stringify({θ1, θ2, ω1, ω2}));
// Restore after new page loads
const saved = JSON.parse(sessionStorage.getItem('pendulum-state'));
Problem: sessionStorage is synchronous—writing in the astro:before-preparation event blocks navigation, making page transitions feel slower.
Solution: Use global singleton pattern, saving state on the window object:
const G = window as any;
if (!G.__pendulum) {
G.__pendulum = {
θ1: Math.PI * 0.75,
θ2: Math.PI * 0.5,
ω1: 0,
ω2: 0,
// ...
};
}
const P = G.__pendulum;
Since Astro’s ClientRouter is SPA navigation, the window object is shared between pages. This naturally persists state without any I/O overhead.
Phase Three: Performance Optimization
Problem: Event Listener Leakage
After each page navigation, the astro:page-load event triggers script re-execution, causing event listeners to be repeatedly added. After dozens of navigations, a single click event might trigger dozens of handlers.
Solution: Use a flag to ensure listeners are only registered once:
if (!P.initialized) {
P.initialized = true;
document.addEventListener('astro:before-preparation', ...);
document.addEventListener('astro:page-load', ...);
}
Optimization: Pause When Page Hidden
When users switch to other tabs, there’s no need to continue calculating and rendering:
function animate(currentTime) {
if (document.hidden) {
requestAnimationFrame(animate);
return; // Skip calculation and rendering
}
// ...
}
Optimization: Stop Animation Loop After Complete Stop
When the pendulum has completely stopped and the trail has disappeared, continuing to run requestAnimationFrame is wasteful:
if (P.isStopping && P.ω1 === 0 && P.ω2 === 0 && P.trail.length === 0) {
draw(); // Draw final frame
P.animId = 0;
return; // Stop animation loop
}
Design Thoughts: Animation and Attention
Animation is Supporting Cast, Not the Lead
I’ve seen too many websites turn animations into “skill demonstrations”—full-screen particle effects, ubiquitous parallax scrolling, 3D rotating logos on loading. These are technically impressive but often make users feel fatigued.
The design principle for the double pendulum animation is: You can notice it, but you don’t have to notice it.
- Small size (28px), won’t steal visual focus
- Colors follow theme, won’t be jarring
- 75% opacity, only becomes 100% on hover
- Automatically stops when entering reading mode
UX Considerations of Nonlinear Stopping
Why do nonlinear stopping instead of direct disappearance?
- Continuity: Sudden disappearance would make users wonder “where did that thing go”
- Physical intuition: Nothing in reality stops instantly—nonlinear deceleration matches expectations
- Graceful degradation: Even if the stopping animation code bugs out, the pendulum just keeps swinging without crashing
Some Insights
1. Physics Simulation is Harder Than It Looks
Deriving double pendulum equations isn’t hard, but making it run stably in browsers requires considering: numerical precision, frame rate fluctuations, state persistence, performance optimization. Each is a pitfall.
2. Good Animation is Restrained
Technically I could make the pendulum spin continuously, have longer trails, brighter colors. But all of these would turn it from an “interesting decoration” into an “annoying distraction.” Good animation is restrained.
3. Small Details Are Worth Polishing
This double pendulum animation will probably only be noticed by 1% of visitors. But it’s precisely these 1% details that constitute a website’s “texture.”
Final Result
If you look at the upper left corner of the page now, you should see a small double pendulum swinging. Click on any article, and it will slowly stop. Return to the homepage, and it will resume swinging.
That’s it. A small, non-intrusive, physically correct double pendulum.
Links
- Double Pendulum - Wikipedia
- Runge-Kutta methods - Wikipedia
- Astro View Transitions
- The Art of Smooth Motion - Deep dive into animation easing functions