Skip to content

Parallax Engine

The parallax engine (src/core/parallaxEngine.ts) is the rendering core of the application. It runs a requestAnimationFrame loop that calculates and applies CSS transforms to the scene container and each individual layer.

Overview

The engine produces two levels of transforms:

  1. Container-level — 3D rotation of the entire scene based on pointer position
  2. Per-layer — Independent translate3d and scale for each layer, combining pointer parallax, sine-wave float, and scale boost

All transforms use GPU-accelerated CSS properties (transform, perspective) to maintain smooth 60fps rendering.

Container Rotation

The scene container receives rotateX and rotateY transforms:

  1. Pointer normalization — The pointer position is normalized to a -1..1 range relative to the viewport center
  2. Target rotation — The normalized values are multiplied by scene.maxRot (the maximum rotation angle in degrees)
  3. Lerp smoothing — The current rotation is interpolated toward the target by scene.containerLerp each frame
  4. Perspectivescene.perspective is set as the CSS perspective value on the container, controlling 3D depth foreshortening

The result: the container subtly tilts in 3D as the pointer moves, creating a natural sense of depth.

Per-Layer Transforms

Each layer receives a translate3d(x, y, z) and scale transform, composed from three sources:

Pointer Parallax

Each layer has moveX and moveY values that determine how much it shifts in response to pointer position:

  • Layers with higher move values shift more, appearing closer to the viewer
  • Layers with lower move values shift less, appearing further away
  • The shift is derived from the same normalized pointer position used for container rotation

Float Animation

Idle sine-wave motion creates organic, looping movement even when the pointer is stationary:

  • X displacement: floatX × (sin(freqA × t) + sin(freqB × t) × ampB)
  • Y displacement: floatY × (sin(freqC × t) + cos(freqD × t) × ampD)

Where:

  • t = elapsed time × layer's floatSpeed
  • freqA, freqB, ampB = shared X-axis harmonic parameters
  • freqC, freqD, ampD = shared Y-axis harmonic parameters
  • floatX, floatY = per-layer amplitude scaling

The dual-frequency approach (primary + secondary oscillation) creates non-repeating patterns that look organic rather than mechanical.

Scale Boost

Each layer has a scaleBoost parameter that increases the layer's scale based on how close the pointer is to the viewport center:

  • When the pointer is at the center, scale boost is at maximum
  • When the pointer is at the edge, scale boost is minimal
  • This creates a subtle "zoom in" effect as the pointer approaches center

Lerp Smoothing

All per-layer values (position and scale) are smoothed via linear interpolation:

current = current + (target - current) × lerp

A lower lerp value (e.g. 0.01) produces slower, more inertial movement. A higher value (e.g. 0.2) makes layers track the pointer more tightly. Each layer has its own lerp setting, so foreground layers can feel snappier while background layers drift slowly.

Z Depth

Each layer's baseZ is applied as the Z component of translate3d. Combined with the container's perspective, this creates real 3D depth separation — layers at different Z depths appear at different sizes and move at different apparent speeds.

Frozen State

When geometry editing requests motion freeze (freezeMotion), the engine enters a frozen state:

  • All transforms are reset to zero (no rotation, no translation, no float, no scale boost)
  • The rAF loop continues running but produces identity transforms
  • This lets you inspect layer bounds and positions without motion interference

FPS Calculation

The engine calculates frames-per-second every 500ms by counting how many requestAnimationFrame callbacks occurred in that interval. The FPS value is exposed as a reactive ref used by the FpsBadge component.

Engine Lifecycle

The engine lifecycle is managed by useParallaxEngine:

  1. Create — The engine is instantiated with references to the container DOM element, layer elements, and the project's reactive state
  2. Start — The rAF loop begins on component mount
  3. Update — On every frame, the engine reads the latest project config and calculates transforms
  4. Stop — The rAF loop is cancelled on component unmount

Pointer position is tracked via mousemove events on document and normalized to [-1, 1] coordinates.

Parallax Designer Documentation

Scroll to zoom · Drag to pan