# How Animation Pacing Is Controlled in Google Timeline Visualizer

> Discover how Google Timeline Visualizer controls animation pacing using a deterministic timing module and cubic easing for smooth, natural motion. Learn more about elapsed time to progress value conversion.

- Repository: [mahlernim/google-timeline-visualizer](https://github.com/mahlernim/google-timeline-visualizer)
- Tags: internals
- Published: 2026-08-22

---

**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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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 completed
- **`outroProgress`** – 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 duration
- **`frameAtElapsedSeconds(elapsedSeconds, journeyDurationSeconds)`** – Converts raw elapsed time into a `TimelineFrame`; linearly interpolates `journeyProgress` during travel and calculates `outroProgress` over the **1 second** transition window (`OUTRO_TRANSITION_SECONDS`) once the journey completes
- **`frameAtOverallProgress(overallProgress, journeyDurationSeconds)`** – Scales a normalized 0‑1 progress value to the total duration before delegating to `frameAtElapsedSeconds`, used primarily by the preview UI scrubber
- **`easeOutCubic(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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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:

```typescript
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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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:

```typescript
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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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:

```typescript
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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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 `TimelineFrame` interface
- **`frameAtElapsedSeconds`** handles the linear mapping of seconds to progress, while **`totalDurationSeconds`** accounts 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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/main.ts)**) and MP4 export (**[`video.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/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.