# How the Camera Track Is Precomputed and Smoothed in Google Timeline Visualizer

> Discover how the Google Timeline Visualizer precomputes and smooths camera tracks using span hysteresis and cubic easing for fluid, lightweight runtime playback.

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

---

**The Google Timeline Visualizer front-loads geometric calculations into a deterministic camera track using span hysteresis and cubic easing, enabling fluid playback through lightweight runtime interpolation.**

The `mahlernim/google-timeline-visualizer` project eliminates animation jitter by precomputing camera positions before playback begins. This article examines the `buildCameraTrack` function in [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts) and explains how the system smooths zoom transitions and camera movements using mathematical hysteresis and easing curves.

## Precomputing the Camera Track in buildCameraTrack

The heavy lifting occurs in [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts) at line 378 inside the **`buildCameraTrack`** function. This routine accepts the journey’s cumulative distance, projected X/Y coordinates, latitude/longitude arrays, a selected camera movement mode (`fixed`, `dynamic`, or `steady`), and a distance-to-progress mapper. It returns a `CameraTrack` object containing an array of precomputed frames that eliminate the need for expensive geometric calculations during animation playback.

### Frame Generation with buildCameraTrackFrame

For every integer frame index from 0 to N, the algorithm invokes **`buildCameraTrackFrame`** (lines 394–418). This helper computes raw center coordinates (`rawX`, `rawY`) and calculates an initial *span*—the zoom level required to keep the entire visible journey path within the viewport boundaries.

```typescript
// Simplified structure based on web/src/camera.ts
interface CameraFrame {
  x: number;      // center X
  y: number;      // center Y
  span: number;   // zoom level (visible distance)
  aspect: number; // width / height ratio
}

function buildCameraTrack(journey: Journey, mode: CameraMode): CameraTrack {
  const frames: CameraFrame[] = [];
  for (let i = 0; i <= totalFrames; i++) {
    const frame = buildCameraTrackFrame(journey, i, mode);
    frames.push(applySmoothing(frame, previousFrame));
  }
  return { frames, aspect: canvasWidth / canvasHeight };
}

```

### Span Hysteresis for Zoom Stability

Rapid zoom changes create visual jitter, so the implementation applies **`SPAN_HYSTERESIS`** between lines 426–435. When the difference between the newly calculated span and the previous frame’s span exceeds a `HYSTERESIS_THRESHOLD`, the algorithm blends the values rather than snapping to the new zoom level. This `Math.abs(span - previousSpan) > HYSTERESIS_THRESHOLD` check ensures that sudden GPS inaccuracies or sharp turns do not trigger jarring zoom adjustments.

### Cubic Easing for Dynamic Movement

When the camera mode is set to **`dynamic`**—the default for long journeys—the system interpolates between successive raw positions using an **`easeInOutCubic`** curve (lines 440–452). This generates acceleration at the start of movements and deceleration at the end, masking sparse or unevenly distributed GPS data points with mathematically smooth trajectories.

```typescript
// Cubic easing implementation as used in camera.ts
function easeInOutCubic(t: number): number {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

// Applied during frame generation for dynamic mode
const smoothedX = previousX + (rawX - previousX) * easeInOutCubic(progress);

```

### Aspect Ratio Enforcement

Each `CameraFrame` stores an **`aspect`** field calculated at lines 452–459, defined as `width / height` of the rendering canvas. This enforcement guarantees that the precomputed viewport maintains consistent geometric proportions across different devices and screen sizes, preventing distortion when the camera track is consumed by the renderer.

## Runtime Interpolation with cameraViewportAt

During interactive playback or timeline scrubbing, the visualizer calls **`cameraViewportAt`** (line 445) to retrieve camera states between the discrete precomputed frames. This function clamps the input progress to the range [0, 1], maps the floating-point progress to indices in the `track.frames` array, and performs linear interpolation between the surrounding `from` and `to` frames.

Because the geometric complexity was resolved during `buildCameraTrack`, this runtime operation requires only cheap arithmetic:

```typescript
function cameraViewportAt(track: CameraTrack, progress: number): Viewport {
  const clamped = Math.max(0, Math.min(1, progress));
  const index = clamped * (track.frames.length - 1);
  const from = track.frames[Math.floor(index)];
  const to = track.frames[Math.ceil(index)];
  const t = index - Math.floor(index); // fractional part
  
  return {
    x: from.x + (to.x - from.x) * easeInOutCubic(t),
    y: from.y + (to.y - from.y) * easeInOutCubic(t),
    span: from.span + (to.span - from.span) * t,
    aspect: track.aspect
  };
}

```

The runtime system re-applies the same cubic easing curve used during precomputation, ensuring that ad-hoc seeking remains as smooth as pre-generated playback.

## Cross-Platform Camera Consistency

The camera smoothing algorithm is not limited to the TypeScript web implementation. The same mathematical logic appears in **[`visualizer.py`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/visualizer.py)** at line 580 for server-side MP4 video generation, and in **[`TimelinePainter.kt`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/TimelinePainter.kt)** (line 138) for the Android native renderer. This shared approach guarantees that exported videos and mobile playback exhibit identical camera behavior to the web interface, including the same hysteresis thresholds and easing curves.

## Summary

- **Front-loaded geometry**: The `buildCameraTrack` function in [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts) (line 378) precomputes camera positions for every frame before animation begins.
- **Zoom stability**: `SPAN_HYSTERESIS` logic (lines 426–435) prevents rapid zoom fluctuations by thresholding span differences between adjacent frames.
- **Smooth motion**: The `dynamic` camera mode applies `easeInOutCubic` interpolation (lines 440–452) to eliminate jitter from sparse GPS data.
- **Cheap runtime**: `cameraViewportAt` (line 445) performs lightweight linear interpolation between cached frames during playback or scrubbing.
- **Multi-platform**: Python and Kotlin implementations mirror the TypeScript algorithm for consistent rendering across web, video export, and Android native apps.

## Frequently Asked Questions

### What is the purpose of precomputing the camera track instead of calculating it during playback?

Precomputing the track in `buildCameraTrack` moves expensive geometric operations—such as great-circle distance calculations, span fitting, and hysteresis blending—outside the animation loop. This separation ensures that interactive scrubbing and 60fps playback remain performant even when processing journeys containing thousands of GPS points, as the runtime only executes the lightweight `cameraViewportAt` interpolation.

### How does span hysteresis prevent zoom jitter when GPS data is noisy?

The hysteresis algorithm stores the previous frame’s span value and compares it to the newly calculated span using `Math.abs(span - previousSpan) > HYSTERESIS_THRESHOLD`. If the difference exceeds the threshold, the code blends the new value toward the old one rather than adopting it immediately, effectively filtering out sudden zoom spikes caused by single inaccurate GPS coordinates or sharp directional changes.

### Can the easing function be customized for different camera movement styles?

While [`web/src/camera.ts`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/web/src/camera.ts) hardcodes `easeInOutCubic` for the `dynamic` mode at lines 440–452, the modular structure of `buildCameraTrack` allows extending the `CameraMode` union type to support alternative easing curves. Implementing a custom mode would involve swapping the cubic interpolation for linear, quadratic, or bounce easing within the frame generation loop while maintaining the same `CameraFrame` interface.

### Where does the visualizer handle camera logic for video export versus live web playback?

Server-side MP4 rendering uses the identical algorithm implemented in Python within [`visualizer.py`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/visualizer.py) (line 580), ensuring exported videos match the web preview pixel-for-pixel. For Android native playback, the Kotlin class [`TimelinePainter.kt`](https://github.com/mahlernim/google-timeline-visualizer/blob/main/TimelinePainter.kt) consumes the precomputed track at line 138, applying the same aspect-ratio enforcement and interpolation logic to render frames on mobile GPUs without recalculating the underlying geometry.