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:
- Container-level — 3D rotation of the entire scene based on pointer position
- Per-layer — Independent
translate3dandscalefor 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:
- Pointer normalization — The pointer position is normalized to a -1..1 range relative to the viewport center
- Target rotation — The normalized values are multiplied by
scene.maxRot(the maximum rotation angle in degrees) - Lerp smoothing — The current rotation is interpolated toward the target by
scene.containerLerpeach frame - Perspective —
scene.perspectiveis set as the CSSperspectivevalue 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'sfloatSpeedfreqA,freqB,ampB= shared X-axis harmonic parametersfreqC,freqD,ampD= shared Y-axis harmonic parametersfloatX,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) × lerpA 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:
- Create — The engine is instantiated with references to the container DOM element, layer elements, and the project's reactive state
- Start — The rAF loop begins on component mount
- Update — On every frame, the engine reads the latest project config and calculates transforms
- Stop — The rAF loop is cancelled on component unmount
Pointer position is tracked via mousemove events on document and normalized to [-1, 1] coordinates.