# Leg-Aware Camera Movement in Google Timeline Visualizer: How It Works Under the Hood

> Discover how leg-aware camera movement in Google Timeline Visualizer creates smooth journey animations by dynamically adjusting zoom and padding for seamless transport mode changes.

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

---

**Leg-aware camera movement detects transport transfers (e.g., walking to train) and dynamically adjusts zoom and padding to preserve context during mode changes, creating smoother journey animations.**

The **Google Timeline Visualizer** animates your Google Maps Timeline data with intelligent camera behavior that adapts to how you actually traveled. Unlike simple map following, its **leg-aware dynamic following** mode recognizes when you switch between transport methods and adjusts the viewport accordingly. This article explains exactly how this system works, directly from the `mahlernim/google-timeline-visualizer` source code.

## What Is Leg-Aware Camera Movement?

A **leg** in this context is a continuous travel segment between two transfers. A **transfer** occurs when there's a significant distance gap—calculated using a dynamic `transferThreshold` based on your journey's scale ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) [lines 19-28](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts#L19-L28)).

The visualizer offers three camera modes defined in `CameraMovementProfile`:

- **Fixed zoom** – Static camera, no following
- **Steady following** – Smooth tracking without leg awareness
- **Dynamic following** – **Leg-aware** mode with adaptive context and padding ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) [lines 59-68](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts#L59-L68))

Only **dynamic following** sets `legAware: true`, enabling the transfer-detection logic that produces more natural animations.

## How Leg Detection Works

The system builds a leg structure before generating any camera frames.

### Step 1: Calculate Transfer Threshold

```typescript
// Simplified from camera.ts lines 19-28
const totalKm = journey.worldPoints[journey.worldPoints.length - 1].distance;
const transferThreshold = totalKm * TRANSFER_THRESHOLD_FACTOR + MIN_TRANSFER_KM;

```

This ensures transfer detection scales appropriately whether your trip spans 5 kilometers or 500.

### Step 2: Build Leg Segments

The `buildLegs` function ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) [lines 31-48](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts#L31-L48)) scans your journey and splits it into legs whenever consecutive points exceed `transferThreshold`. Each leg stores:

- `startKm` and `endKm` — distance bounds
- `isTransfer` — true if this leg represents a gap (wait time, mode change)

### Step 3: Find Active Leg at Any Position

```typescript
// camera.ts lines 50-58 — the legAt function
function legAt(legs: Leg[], distanceKm: number): Leg {
  return legs.find(l => l.startKm <= distanceKm && l.endKm > distanceKm)!;
}

```

This lookup runs for every sampled frame during track generation.

## Adjusting Viewport Context for Transfers

When `legAware` is enabled, `rawViewport` ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) [lines 21-27](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts#L21-L27)) modifies its behavior:

| Condition | Context Used | Padding Used |
|-----------|------------|--------------|
| `leg.isTransfer === true` | Exact leg length (`endKm - startKm`) | `TRANSFER_PADDING` (larger) |
| Normal movement | `proportionalContextKm` from profile | Standard padding |

This difference is subtle but critical. During a transfer—say, arriving at a train station and boarding—the camera **zooms out** to show more surrounding geography, **holds** that broader view for the duration of the wait/travel gap, then **resumes** tighter following once continuous movement resumes.

## Generating the Camera Track

The `buildCameraTrack` function ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) [lines 78-85](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts#L78-L85)) samples your journey 480 times by default. For each sample:

1. Calculate progress (0.0 to 1.0)
2. Map to distance along journey
3. Call `legAt` to find current leg
4. Call `rawViewport` with leg awareness
5. Store resulting viewport in track array

The resulting `CameraTrack` is a pre-computed animation path that can be queried efficiently during rendering.

## Rendering and UI Integration

The movement mode is selected via a standard HTML `<select>` ([`index.html`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/index.html) [lines 129-133](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/index.html#L129-L133)):

```html
<select id="camera-movement">
  <option value="fixed">Fixed zoom</option>
  <option value="steady">Steady following</option>
  <option value="dynamic" selected>Dynamic following</option>
</select>

```

Each frame, the renderer ([`renderer.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/renderer.ts) [lines 6-9](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/renderer.ts#L6-L9)) queries the computed track:

```typescript
import { buildCameraTrack, cameraViewportAt } from './camera';
import type { Journey, RenderSize } from './types';

const size: RenderSize = { width: 1080, height: 1920 };
const cameraTrack = buildCameraTrack(journey, size, 'dynamic'); // leg-aware

const progress = 0.42; // 42% through trip
const viewport = cameraViewportAt(cameraTrack, progress);
// viewport: { minX, maxX, minY, maxY, zoom }

```

## Why Leg-Aware Movement Matters

Without leg awareness, the camera treats a 2-hour flight the same as 2 hours of walking—either both cramped or both zoomed too far out. The leg-aware approach delivers:

- **Context preservation** — Airports, stations, and transfer points remain visible
- **Natural pacing** — The "camera" behaves like a human narrator, pausing appropriately
- **Multi-modal clarity** — Clear visual separation between walking, driving, and transit segments

## Summary

- **Three camera modes** exist, but only **dynamic following** is leg-aware ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) lines 59-68)
- **Legs and transfers** are detected via `buildLegs` using a scaled `transferThreshold` (lines 19-28, 31-48)
- **Viewport calculation** adapts context and padding when `leg.isTransfer` is true (`rawViewport`, lines 21-27)
- **Pre-computed tracks** with 480 samples enable smooth 60fps playback (`buildCameraTrack`, lines 78-85)
- **UI selection** in [`index.html`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/index.html) propagates through [`main.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/main.ts) to trigger leg-aware rendering

## Frequently Asked Questions

### What triggers a "transfer" in the leg-aware system?

A transfer is detected when the distance between two consecutive Timeline points exceeds the `transferThreshold`, calculated as `totalJourneyKm * FACTOR + MIN_TRANSFER_KM`. This scales with trip length so short walks and long-haul flights are both handled appropriately ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) lines 19-28).

### Can I use leg-aware movement without the dynamic following mode?

No. Leg awareness is hardcoded to the `dynamic` camera profile via `legAware: true` in its `CameraMovementProfile`. The `fixed` and `steady` modes set this flag to false, disabling leg detection entirely ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) lines 59-68).

### How does the camera behave differently during a transfer?

During transfers, the camera uses the exact leg length as its viewing context and applies larger `TRANSFER_PADDING`. This produces a zoomed-out, sustained view of the transfer location—useful for showing airport terminals, train stations, or extended waits—rather than treating the gap as rapid movement ([`camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/camera.ts) lines 21-27).

### Where is the camera track actually used for rendering?

The [`renderer.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/renderer.ts) module calls `cameraViewportAt` each animation frame, passing the current playback progress to retrieve the pre-calculated viewport. This viewport's bounds and zoom level then drive tile fetching and map composition ([`renderer.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/renderer.ts) lines 6-9).