# How Fixed Camera Movement Works in Google Timeline Visualizer

> Discover how fixed camera movement in Google Timeline Visualizer keeps a constant zoom. Learn how it calculates static Y-span and disables dynamic zoom for smooth animations. Get the details.

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

---

**Fixed camera movement maintains a constant zoom level throughout the animation by calculating a static Y-span from the 80th percentile of all viewport samples and disabling dynamic zoom smoothing.**

The Google Timeline Visualizer renders travel history animations by programmatically moving a virtual camera along recorded routes. When configured to use **fixed camera movement**, the visualizer maintains a consistent map scale rather than dynamically adjusting the zoom based on point density. This behaviour is controlled by the `CameraMovement` enum and `MOVEMENT_PROFILES` configuration defined in [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts).

## What Defines the "Fixed" Movement Profile

The movement behaviour is governed by a profile table that specifies how the camera frames the journey. For the **fixed** profile, the configuration differs from `steady` and `dynamic` modes in several critical parameters:

- **`fixedZoom: true`** – Forces the Y-span of the viewport to remain constant across all animation frames
- **`zoomOutAlpha: 0`** and **`zoomInAlpha: 0`** – Disables exponential smoothing of zoom levels, preventing gradual zoom transitions
- **`contextFraction: 0.10`** – Uses only 10% of the total route distance as a context window, clamped between 25 km and 350 km
- **`padding: 2.6`** – Applies extra margin around visible points
- **`legAware: false`** – Treats all route segments uniformly without special handling for transfer legs

These settings appear in the movement profile definition around lines 36-47 of [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts). The combination of `fixedZoom = true` and alpha values of zero instructs the renderer to ignore dynamic zoom calculations that would otherwise expand or contract the view based on geographic point density.

## How the Fixed Camera Track Is Generated

The `buildCameraTrack` function constructs the animation path through a multi-step process that enforces zoom stability while allowing smooth panning.

### Raw Sample Generation

The system first generates approximately 480 sample points (defined by `CAMERA_TRACK_SAMPLES`) distributed evenly across the journey timeline. Each sample calculates a raw viewport and marker position:

```typescript
const rawSamples = Array.from({ length: CAMERA_TRACK_SAMPLES + 1 }, (_, s) => {
  const progress = s / CAMERA_TRACK_SAMPLES;
  return {
    viewport: rawViewport(journey, progress, size, movement, legs),
    marker: worldPositionAtProgress(journey, progress).point,
  };
});

```

This occurs for every frame of the animation to establish baseline visibility boundaries.

### Deriving the Constant Y-Span

Because `fixedZoom` is enabled, the code calculates a single Y-span value to use for the entire animation. It extracts all raw Y-spans from the samples, sorts them, and selects the value at the 80th percentile (controlled by `FIXED_ZOOM_PERCENTILE`):

```typescript
const fixedSpanY = movement.fixedZoom
  ? rawSamples
      .map(s => s.viewport.maxY - s.viewport.minY)
      .sort((a, b) => a - b)[Math.floor(CAMERA_TRACK_SAMPLES * FIXED_ZOOM_PERCENTILE)]
  : null;

```

This percentile-based approach ensures the constant zoom accommodates the majority of the route while filtering out extreme outliers that would otherwise compress the view.

### Frame Construction and Clamping

For each frame, the Y-span is forced to the calculated `fixedSpanY` rather than the raw calculated bounds. The X-span derives from the aspect ratio to maintain proportions:

```typescript
const rawSpanY = clamp(
  fixedSpanY ?? raw.maxY - raw.minY,
  movement.minimumViewportSpan,
  MAX_VIEWPORT_SPAN,
);
const spanX = spanY * aspect;

```

The `minimumViewportSpan` (set to `0.00060` in the fixed profile) prevents extreme zoom-ins when coordinates are very close together.

### Dead Zone Stabilization

To prevent jitter while maintaining smooth following, the camera only pans when the traveller marker exits a dead zone comprising 20% of the viewport (defined by `CAMERA_DEAD_ZONE_HALF`):

```typescript
const deadHalfX = spanX * CAMERA_DEAD_ZONE_HALF;
const deadHalfY = spanY * CAMERA_DEAD_ZONE_HALF;
if (markerX < centerX - deadHalfX) centerX = markerX + deadHalfX;

```

This keeps the camera stationary during minor position fluctuations while ensuring the traveller remains visible during significant movement.

## Implementing Fixed Camera Movement

To generate a camera track with fixed movement behaviour, import the `buildCameraTrack` function and specify `'fixed'` as the movement type:

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

const size: RenderSize = { width: 1280, height: 720 };
const track = buildCameraTrack(journey, size, 'fixed');

console.log(`Generated ${track.frames.length} frames with constant zoom`);

```

To retrieve the viewport for a specific animation progress (0.0 to 1.0):

```typescript
import { cameraViewportAt } from './camera';

const viewport = cameraViewportAt(track, 0.25); // 25% through animation
console.log(`Center: ${(viewport.minX + viewport.maxX) / 2}, ${(viewport.minY + viewport.maxY) / 2}`);

```

When rendering, the viewport maintains consistent dimensions throughout the animation:

```typescript
function render(progress: number) {
  const vp = cameraViewportAt(track, progress);
  map.setView(
    [(vp.minY + vp.maxY) / 2, (vp.minX + vp.maxX) / 2],
    vp.zoom // Tile zoom remains stable due to fixed profile
  );
}

```

## Summary

- **Fixed camera movement** uses a constant zoom level calculated once from the 80th percentile of all possible viewport spans across the journey.
- The profile disables zoom smoothing (`zoomOutAlpha` and `zoomInAlpha` set to zero) and enables `fixedZoom` to bypass dynamic calculations.
- The implementation resides primarily in [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts), with profile definitions around lines 36-47 and frame generation logic near lines 93-144.
- A 20% dead zone prevents unnecessary panning while keeping the traveller in view.

## Frequently Asked Questions

### How does fixed camera movement differ from dynamic or steady modes?

Fixed mode maintains a constant geographic span throughout the animation, while dynamic and steady modes adjust the zoom level based on point density or velocity. The steady profile uses exponential smoothing (`zoomOutAlpha` and `zoomInAlpha` > 0) to gradually transition between zoom levels, whereas fixed mode sets these values to zero and enforces a static Y-span via `fixedZoom: true`.

### Why is the 80th percentile used for the fixed Y-span calculation?

The 80th percentile (`FIXED_ZOOM_PERCENTILE`) provides a robust statistical measure that accommodates the majority of the route's geographic extents without being skewed by isolated outliers. This ensures the constant zoom level remains appropriate for most of the journey while preventing occasional distant points from compressing the entire visualization.

### Does fixed camera movement completely eliminate panning?

No, fixed movement only stabilizes the zoom level. The camera still pans horizontally and vertically to follow the traveller using dead-zone logic. The camera center updates only when the marker moves beyond 20% of the viewport dimensions from the current center, creating smooth tracking behaviour while eliminating zoom fluctuations.

### Where can I modify the padding or context distance for fixed movement?

These parameters are defined in the `MOVEMENT_PROFILES` table within [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts). The `padding` (default 2.6) and context clamping values (`minimumContextKm: 25`, `maximumContextKm: 350`) can be adjusted in the profile definition around lines 36-47. Changes affect how much geographic context appears around the route points during the fixed zoom animation.