How Animation Pacing Is Controlled in Google Timeline Visualizer
Google Timeline Visualizer regulates animation pacing through a deterministic timing module that converts elapsed wall-clock time into normalized progress values, separating the travel segment from the outro fade-out while applying cubic easing functions for natural motion curves.
The mahlernim/google-timeline-visualizer repository implements a precision timing system in TypeScript to orchestrate travel animations across map timelines. By decoupling the journey progress from the outro transition and exposing pure easing utilities, the codebase gives developers fine-grained control over how the camera moves, accelerates, and concludes.
Core Timing Architecture in animation.ts
All pacing logic lives inside web/src/animation.ts, which exports a minimal API for converting seconds into animation frames. The module treats time as a sequential progression through two distinct phases: the active travel segment and the concluding outro.
Journey Progress vs. Outro Progress
The system uses a TimelineFrame interface that splits animation state into two normalized properties (0 → 1):
journeyProgress– Represents the fraction of the travel portion completedoutroProgress– Represents the fraction of the fade-out phase completed after the journey ends
This separation allows the renderer to handle map movement and opacity transitions independently.
Key Functions in animation.ts
Four primary functions control the pacing calculations:
totalDurationSeconds(journeyDurationSeconds)– Computes the full animation length by adding the fixed 1.5 second outro (OUTRO_SECONDS) to the travel durationframeAtElapsedSeconds(elapsedSeconds, journeyDurationSeconds)– Converts raw elapsed time into aTimelineFrame; linearly interpolatesjourneyProgressduring travel and calculatesoutroProgressover the 1 second transition window (OUTRO_TRANSITION_SECONDS) once the journey completesframeAtOverallProgress(overallProgress, journeyDurationSeconds)– Scales a normalized 0‑1 progress value to the total duration before delegating toframeAtElapsedSeconds, used primarily by the preview UI scrubbereaseOutCubic(value)&easeInOutCubic(value)– Pure easing functions that clamp input to 0‑1 and apply cubic curves for smoother acceleration and deceleration
How the UI Implements Animation Pacing
The timing functions are consumed by three separate subsystems, each applying the same core logic in different contexts.
Preview Mode Timing
In web/src/main.ts, the preview loop drives real-time playback by measuring elapsed wall-clock time via performance.now(). The loop caps the elapsed time to the preview duration and calls frameAtElapsedSeconds to obtain current progress values:
import { frameAtElapsedSeconds } from './animation';
let start = performance.now();
const journeyDuration = 30; // seconds
function tick(now: number) {
const elapsedSec = (now - start) / 1000;
const frame = frameAtElapsedSeconds(elapsedSec, journeyDuration);
// frame.journeyProgress: 0 → 1 during travel
// frame.outroProgress: 0 → 1 during 1.5s outro
drawFrame(canvas, journey, frame);
requestAnimationFrame(tick);
}
Video Export Timing
When generating the final MP4 in web/src/video.ts, the createJourneyMp4 function calculates the total frame count based on FPS and duration. For each frame index, it derives the elapsed time and invokes frameAtElapsedSeconds, ensuring the exported video matches the preview timing exactly:
import { frameAtElapsedSeconds, totalDurationSeconds } from './animation';
const fps = 30;
const journeySeconds = 45;
const totalSeconds = totalDurationSeconds(journeySeconds); // 46.5
const totalFrames = Math.ceil(totalSeconds * fps);
for (let i = 0; i < totalFrames; i++) {
const elapsed = i / fps;
const frame = frameAtElapsedSeconds(elapsed, journeySeconds);
// Render frame with journeyProgress and outroProgress
}
Applying Easing Curves
The renderer in web/src/renderer.ts imports easeInOutCubic and easeOutCubic to transform linear progress into smooth motion. Rather than applying easing to the time calculation itself, the renderer passes the raw journeyProgress through these functions when interpolating camera positions, map tile coordinates, and overlay text opacity:
import { easeInOutCubic, easeOutCubic } from './animation';
function renderCameraPosition(rawProgress: number) {
// Apply ease-in-out for natural acceleration and deceleration
const smoothProgress = easeInOutCubic(rawProgress);
return interpolate(startCoords, endCoords, smoothProgress);
}
Summary
- Animation pacing in Google Timeline Visualizer is centralized in
web/src/animation.ts, which provides pure functions for time-to-progress conversion - The system separates journey progress (travel movement) from outro progress (fade-out phase) using the
TimelineFrameinterface frameAtElapsedSecondshandles the linear mapping of seconds to progress, whiletotalDurationSecondsaccounts for the fixed 1.5 second outro- Cubic easing functions (
easeOutCubic,easeInOutCubic) are applied during rendering to create natural acceleration curves without affecting the underlying timeline calculations - Both real-time preview (
main.ts) and MP4 export (video.ts) consume the same timing functions to ensure behavioral consistency
Frequently Asked Questions
How does Google Timeline Visualizer handle the transition between travel and outro phases?
The transition is handled automatically inside frameAtElapsedSeconds in web/src/animation.ts. When the elapsed time exceeds the journeyDurationSeconds, the function stops advancing journeyProgress at 1.0 and begins calculating outroProgress over a fixed 1‑second window (OUTRO_TRANSITION_SECONDS). This allows the renderer to trigger fade-out effects independently from the travel movement.
Can I change the speed or duration of the outro fade-out?
Yes, but it requires modifying the constants in web/src/animation.ts. The outro duration is controlled by OUTRO_SECONDS (default 1.5 s), while the fade-out transition length is controlled by OUTRO_TRANSITION_SECONDS (default 1 s). Changing these values will automatically update the totalDurationSeconds calculations used by both the preview and export systems.
What is the difference between frameAtElapsedSeconds and frameAtOverallProgress?
frameAtElapsedSeconds accepts raw seconds (e.g., 12.5) and is used by the real-time preview loop and video exporter. frameAtOverallProgress accepts a normalized 0‑1 value and is used primarily by UI components like the timeline scrubber. The latter scales the normalized progress to the total duration before delegating to frameAtElapsedSeconds, ensuring both functions return consistent TimelineFrame objects.
Why are easing functions kept separate from the timing calculations?
The easing functions (easeInOutCubic, easeOutCubic) are pure mathematical utilities that transform a 0‑1 input into a curved output. By keeping them separate from frameAtElapsedSeconds, the system maintains linear time progression for the underlying timeline while allowing the renderer to apply different easing curves to different elements (camera movement vs. text opacity) without altering the core pacing logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →