DEV Community

Cover image for Building a 60fps Canvas Frustum Camera for 4K System Architecture Presentations
xiangc
xiangc

Posted on Originally published at tumio.hashnode.dev

Building a 60fps Canvas Frustum Camera for 4K System Architecture Presentations

When presenting a complex enterprise topology—whether it's a multi-region Kafka cluster, a distributed consensus state machine, or an intricate domain-driven microservices map—traditional slide decks fail fundamentally: they shatter continuous spatial relationships into disconnected 2D rectangles.

When you jump from a slide showing the global ingress gateway to another slide showing an internal Raft voting loop, your audience has to mentally reconstruct the spatial hierarchy from scratch. Spatial infinite canvases (like Miro, Figma, or tldraw) solve the continuum problem, but they introduce severe engineering trade-offs when repurposed for technical presentations:

  1. Security & Data Sovereignty: Confidential banking topologies or internal infra diagrams cannot be pushed to SaaS multi-tenant cloud storage.
  2. Missing Cinematic Director Controls: Freeform whiteboards lack strict, broadcast-grade camera frustums (aspect ratio safety frames) and deterministic, physics-based flight transitions.
  3. The 120Hz/144Hz ProMotion Performance Trap: High-frequency trackpad wheel streams trigger synchronous forced reflows, subpixel raster jitter, and React reconciliation bottlenecks.

In this deep dive, we break down how we built the zero-dependency, 60fps spatial camera engine behind FocusFlow—an open-source, local-first technical presentation studio. We'll explore the affine coordinate transformations, subpixel DPR raster stabilization, mathematical frustum safety clamping, and how to completely decouple high-frequency gesture loops from the React tree.


1. Affine Viewport Math & Zoom-to-Cursor Inversion

The first UX requirement of any intuitive infinite canvas is Zoom-to-Cursor: the exact point in world coordinates directly beneath the user's mouse cursor must remain stationary on the physical screen throughout the zoom gesture.

   Screen Viewport (Px, Py)
   +------------------------------------+
   |                                    |
   |              Cursor [Px, Py]       |
   |                    *               |
   |                  /   \             |
   |                 /     \            |
   +----------------/-------\-----------+
                   /         \
                  /  World    \
                 /  Canvas     \
                +---------------+
                |    * [Wx, Wy] |
                +---------------+
Enter fullscreen mode Exit fullscreen mode

1.1 The Forward & Inverse Projection

We define two distinct coordinate spaces:

  • Screen Viewport Space (P = [Px, Py]): Physical pixel offsets relative to the viewport container bounding box.
  • World Space (W = [Wx, Wy]): Unscaled, unshifted absolute coordinates of the underlying architectural canvas.

Given a viewport translation vector T = [Tx, Ty] and an isotropic scale factor S, the forward affine transformation is:

P = S * W + T
Enter fullscreen mode Exit fullscreen mode

Inverting this yields the unprojected world point under any arbitrary screen coordinate P:

W = (P - T) / S
Enter fullscreen mode Exit fullscreen mode

1.2 The O(1) Zoom Anchor Derivation

Let (S1, T1) represent the canvas state prior to a wheel step, and (S2, T2) represent the target state. To lock the world focal point W under cursor position P:

P = S1 * W + T1  ==>  W = (P - T1) / S1
P = S2 * W + T2  ==>  T2 = P - S2 * W
Enter fullscreen mode Exit fullscreen mode

Substituting W:

T2 = P - S2 * ((P - T1) / S1) = P - (P - T1) * (S2 / S1)
Enter fullscreen mode Exit fullscreen mode

This closed-form solution runs in O(1) time with zero heavy matrix allocations. In our production gesture hook (useCanvasGesture.ts):

const zoomTo = useCallback(
  (newScale: number, cursorScreenX?: number, cursorScreenY?: number) => {
    const container = containerRef.current;
    if (!container) return;

    // Use cached bounding box to avoid layout thrashing
    const rect = cachedRectRef.current || container.getBoundingClientRect();
    const Px = cursorScreenX !== undefined ? cursorScreenX - rect.left : rect.width / 2;
    const Py = cursorScreenY !== undefined ? cursorScreenY - rect.top : rect.height / 2;

    const prev = currentTransformRef.current;
    const clampedScale = Math.min(Math.max(newScale, minScale), maxScale);
    if (clampedScale === prev.scale) return;

    const ratio = clampedScale / prev.scale;
    const nextTransform: CanvasTransform = {
      x: Px - (Px - prev.x) * ratio,
      y: Py - (Py - prev.y) * ratio,
      scale: clampedScale,
    };

    currentTransformRef.current = nextTransform;
    setTransform(nextTransform);
  },
  [minScale, maxScale]
);
Enter fullscreen mode Exit fullscreen mode

2. Dynamic Scale Compensation & Focal Locking

On massive 4K/8K architecture diagrams, standard linear zoom sensitivity falls apart:

  • At macro zoom (S < 0.3), zooming in feels lethargic because the geometric delta is tiny compared to the total canvas span.
  • At micro zoom (S > 3.0), small trackpad impulses catapult the camera too far.

2.1 Exponential Zoom with Low-Scale Acceleration

We model the zoom delta exponentially relative to mouse wheel delta:

deltaScale = exp(-deltaY * sensitivity * scaleBoost)
Enter fullscreen mode Exit fullscreen mode

Where sensitivity is device-calibrated (e.g. 0.009 for Mac pinch gestures, 0.04 for wheel line mode, 0.0025 for pixel mode), and scaleBoost dynamically scales up when viewing the canvas from a high altitude:

// Dynamically boost zoom velocity when viewing expansive panoramic overviews
const prev = currentTransformRef.current;
const scaleBoost = prev.scale < 0.8 
  ? 1 + 1.5 * Math.max(0, (0.8 - prev.scale) / 0.8) 
  : 1.0;

const sensitivity = baseSensitivity * scaleBoost;
const zoomFactor = Math.exp(-e.deltaY * sensitivity);
const clampedZoomFactor = Math.min(Math.max(zoomFactor, 0.35), 2.5);
Enter fullscreen mode Exit fullscreen mode

2.2 Continuous Focal Point Persistence

When panning and zooming simultaneously on trackpads, discrete event ticks can introduce micro-drift in the computed cursor world anchor. We lock the active focal point during sustained gestures:

const isOngoingGesture =
  activeFocalPointRef.current && now - activeFocalPointRef.current.lastTime < 180;

let worldX: number;
let worldY: number;

if (isOngoingGesture && activeFocalPointRef.current) {
  // Lock world coordinates to prevent threshold skipping
  worldX = activeFocalPointRef.current.worldX;
  worldY = activeFocalPointRef.current.worldY;
  activeFocalPointRef.current.lastTime = now;
} else {
  worldX = (rawPx - prev.x) / prev.scale;
  worldY = (rawPy - prev.y) / prev.scale;
  activeFocalPointRef.current = { worldX, worldY, lastTime: now };
}
Enter fullscreen mode Exit fullscreen mode

3. Subpixel Jitter Elimination: Physical Raster Snapping

A subtle bug in many canvas implementations is subpixel text breathing.

When translation values Tx or Ty land on non-integer subpixels (e.g., x = 245.333px), the GPU's bilinear filtering interpolates font glyph edges across physical LCD subpixels. During slight pans, fonts visibly alternate between crisp and blurry ("breathing").

Rounding to raw integers (Math.round(x)) fixes font blur on standard 1x displays, but on Retina/HiDPI screens (DPR = 2 or 3), it discards sub-CSS-pixel precision that the display can physically render.

The mathematically correct solution is Device Pixel Ratio Snapping:

Tx_snapped = Math.round(Tx * DPR) / DPR
Enter fullscreen mode Exit fullscreen mode
const panBy = useCallback((dx: number, dy: number) => {
  markGestureActive();

  const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
  const rawX = currentTransformRef.current.x + dx;
  const rawY = currentTransformRef.current.y + dy;

  // Snap to physical device pixels, eliminating subpixel raster shimmering
  const nextTransform: CanvasTransform = {
    ...currentTransformRef.current,
    x: Math.round(rawX * dpr) / dpr,
    y: Math.round(rawY * dpr) / dpr,
  };
  currentTransformRef.current = nextTransform;

  // Batch through requestAnimationFrame
  if (wheelRafIdRef.current === null) {
    wheelRafIdRef.current = requestAnimationFrame(() => {
      wheelRafIdRef.current = null;
      setTransform(currentTransformRef.current);
    });
  }
}, [markGestureActive]);
Enter fullscreen mode Exit fullscreen mode

4. The 16:9 Cinematic Frustum & Mathematical Boundary Clamping

In presentation mode, FocusFlow acts as a virtual broadcast camera. A slide is not a blank sheet—it is a Camera Frustum Frame framed over the master architecture diagram.

+---------------------------------------------------------+
| Architecture Diagram (World Space, W x H)               |
|                                                         |
|         +---------------------------+                   |
|         | Camera Frustum (16:9)     |                   |
|         | +-----------------------+ |                   |
|         | | Target Microservice   | |                   |
|         | |                       | |                   |
|         | +-----------------------+ |                   |
|         +---------------------------+                   |
|                                                         |
+---------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

4.1 Safe Viewport Constraint Equation

If the camera pans too far off the diagram edges, the audience sees an unsightly void (black letterboxing).

Let Z >= 1.0 be the camera zoom multiplier. When zoomed in, the maximum allowed fractional offset from diagram center before the frustum frame exits the canvas boundary is given by:

|Tx|, |Ty| <= ((Z - 1.0) / (2.0 * Z)) * 100%
Enter fullscreen mode Exit fullscreen mode

For instance:

  • At Z = 1.0 (full diagram view), max offset is 0% (camera must remain centered).
  • At Z = 2.0 (2x zoom), the camera can travel up to +-25% from center.
  • At Z = 4.0 (4x zoom), the camera can travel up to +-37.5% from center.

We add a calibrated 15% visual bleed buffer so architects can frame boundary components comfortably without abrupt hard-stops:

export function clampCameraBounds(
  zoom: number, 
  x: number, 
  y: number
): { x: number; y: number } {
  if (zoom <= 1.0) {
    return { x: 0, y: 0 };
  }

  // Safe limit constraint: |T| <= ((Z - 1) / (2 * Z)) * 100%
  const maxOffset = ((zoom - 1.0) / (2.0 * zoom)) * 100;

  // 15% visual bleed tolerance for fluid authoring
  const safeLimit = maxOffset * 1.15;
  const clampedX = Math.max(-safeLimit, Math.min(safeLimit, x));
  const clampedY = Math.max(-safeLimit, Math.min(safeLimit, y));

  return {
    x: Math.round(clampedX * 10) / 10,
    y: Math.round(clampedY * 10) / 10,
  };
}
Enter fullscreen mode Exit fullscreen mode

4.2 Bi-directional Mapping: Parameters to Frustum Box

To let developers edit camera targets using intuitive on-canvas drag handles, we maintain exact bi-directional symmetry between camera parameters (Z, X, Y) and the world-space bounding box:

export function cameraToFrustumRect(
  camera: CameraConfig,
  naturalWidth: number,
  naturalHeight: number,
  aspectRatio: number = 16 / 9
): FrustumRect {
  const zoom = Math.max(camera.zoom, 0.1);
  const imgAspect = naturalWidth / Math.max(naturalHeight, 1);

  let frameWidth: number;
  let frameHeight: number;

  if (imgAspect >= aspectRatio) {
    frameWidth = naturalWidth / zoom;
    frameHeight = frameWidth / aspectRatio;
  } else {
    frameHeight = naturalHeight / zoom;
    frameWidth = frameHeight * aspectRatio;
  }

  const clamped = clampCameraBounds(zoom, camera.x || 0, camera.y || 0);
  const centerX = naturalWidth / 2 + (clamped.x / 100) * naturalWidth;
  const centerY = naturalHeight / 2 + (clamped.y / 100) * naturalHeight;

  return {
    x: Math.round(centerX - frameWidth / 2),
    y: Math.round(centerY - frameHeight / 2),
    width: Math.round(frameWidth),
    height: Math.round(frameHeight),
  };
}
Enter fullscreen mode Exit fullscreen mode

5. Eliminating Layout Thrashing & React State Decoupling

A classic frontend pitfall is placing container.getBoundingClientRect() inside a wheel or pointermove listener.

On Apple Studio Displays and ProMotion MacBooks, trackpads emit upwards of 120 wheel events per second. Calling getBoundingClientRect() forces the browser to flush scheduled style calculations and execute a synchronous forced reflow (Layout Thrashing).

5.1 The ResizeObserver Cache Pattern

We cache the container geometry using a ResizeObserver, ensuring zero synchronous reflows inside the animation loop:

const cachedRectRef = useRef<{ left: number; top: number; width: number; height: number } | null>(null);

useEffect(() => {
  const container = containerRef.current;
  if (!container) return;

  const updateRect = () => {
    const r = container.getBoundingClientRect();
    cachedRectRef.current = {
      left: r.left,
      top: r.top,
      width: r.width,
      height: r.height,
    };
  };

  updateRect();
  const ro = new ResizeObserver(() => updateRect());
  ro.observe(container);

  return () => ro.disconnect();
}, []);
Enter fullscreen mode Exit fullscreen mode

5.2 Decoupling High-Frequency Loops from React

If you call React's setState on every wheel event, React's concurrent scheduler creates backpressure. Fiber nodes queue up, garbage collection spikes, and frame rates drop from 60fps to 24fps.

Our architecture applies a Dual-Layer Synchronization Pipeline:

  1. Synchronous Immediate Layer (currentTransformRef): Every incoming gesture mutates a lightweight ref immediately. Any subsequent event within the same 8ms frame reads the fresh mathematical baseline.
  2. Batched Presentation Layer (requestAnimationFrame): A single rAF callback coalesces all intra-frame events and pushes the final state update to React once per vsync pulse.
// Synchronous baseline mutation
currentTransformRef.current = nextTransform;

// Coalesced React state dispatch
if (wheelRafIdRef.current === null) {
  wheelRafIdRef.current = requestAnimationFrame(() => {
    wheelRafIdRef.current = null;
    setTransform(currentTransformRef.current);
  });
}
Enter fullscreen mode Exit fullscreen mode

5.3 Dynamic Hardware Layering

Setting will-change: transform on dozens of DOM elements permanently consumes excessive GPU texture memory (VRAM).

We dynamically toggle hardware layering only while gestures are actively in-flight:

const markGestureActive = useCallback(() => {
  setIsGesturing(true);
  if (gestureTimerRef.current) {
    clearTimeout(gestureTimerRef.current);
  }
  gestureTimerRef.current = setTimeout(() => {
    setIsGesturing(false);
    gestureTimerRef.current = null;
  }, 200);
}, []);
Enter fullscreen mode Exit fullscreen mode

6. The Local-First Philosophy: Zero-SaaS Presentation Sharing

Why build all this in-browser without relying on cloud canvas backends?

When presenting enterprise system architectures—such as payment ledger topologies, sovereign encryption enclaves, or healthcare HIPAA pipelines—uploading slides to an unvetted SaaS platform violates corporate security mandates.

FocusFlow operates on a Local-First, Self-Contained Architecture:

  • All canvas manipulation, frustum path planning, and voiceover synchronization execute 100% client-side in RAM.
  • Projects persist locally in IndexedDB with undo/redo transaction isolation.
  • Presentation exports can be packaged into single standalone offline HTML files with the entire scene tree serialized into inline JSON. You can AirDrop the file or host it behind an air-gapped intranet firewall, and it will render deterministically 10 years from now.

Conclusion & Live Playground

Building a spatial presentation engine requires treating the browser as a precision graphics workstation rather than a generic document renderer. By pairing affine coordinate inversion with DPR raster stabilization, 16:9 safe boundary clamping, and rAF gesture aggregation, you can achieve smooth 60fps cinematic camera motion across 4K architecture diagrams.

How do you tackle layout thrashing and camera continuity in your spatial web apps? Let's discuss in the comments below!

Top comments (0)