Aperture UI
Engineer Notes

Aperture UI Animation Interface

Aperture UI Animation Interface

Overview

The Aperture UI Animation Interface provides a W3C Web Animations-compliant animation system for CSS animations, transitions, and the JavaScript Element.animate() API. The system is designed so that libraries like framer-motion and anime.js can target it via the standard Web Animations API surface.

Runtime Architecture (2025.02 Refresh)

  • AnimationService is a singleton that ticks on the main UI thread and fans frame deltas out to every registered AnimationTimeline. It measures real time via nsTime::Now() when Tick() is used, or honors explicit deltas via AdvanceBy() when the host loop already knows its frame interval. Registration is mutex-protected.
  • CSS cubic-bezier easing backs every stock AnimationEasing option. Animation.cpp routes easing through AnimationRegistry::ApplyTimingFunction() which uses a Newton-Raphson cubic-bezier solver per the CSS Easing Level 2 spec. Tweeny is no longer used for easing.
  • CSS priority with per-property overrides lives on each timeline through animation::AnimationOverrideMask. CSS-created timelines keep the mask empty, meaning script animations may run but cannot stomp CSS output. Hosts may flip bits to allow script overrides on a property-by-property basis.
  • Service ownership expectations: Layout and JS subsystems remain free to run on their own threads, but everything else (including the animation tick) occurs on the UI thread to match Harrlow's renderer contract.

Host Integration

Host applications must call UIView::tick(float deltaSeconds) every frame to advance the animation system. This method:

  1. Ticks the global AnimationService singleton via AdvanceBy() with the provided delta
  2. Updates all active video players and Lottie animations
  3. Updates CSS transitions with actual frame timing
  4. Flushes requestAnimationFrame callbacks (W3C spec: after animations, before paint)
// In your game loop / frame update:
float deltaSeconds = /* your frame delta */;
myUIView->tick(deltaSeconds);

// Then build and render commands as usual:
const auto& commandList = myUIView->buildCommandList();

Easing System

The easing system uses AnimationRegistry::ApplyTimingFunction() as the sole easing engine, which implements:

EasingCSS EquivalentBezier Values
Linearlinear-
Easeease0.25, 0.1, 0.25, 1.0
EaseInease-in0.42, 0, 1.0, 1.0
EaseOutease-out0, 0, 0.58, 1.0
EaseInOutease-in-out0.42, 0, 0.58, 1.0
CubicBeziercubic-bezier(...)User-defined
StepStartstep-start-
StepEndstep-end-
Stepssteps(n)-

Custom cubic-bezier control points can be set via AnimationOptions::bezierControlPoints[4] or per-keyframe via AnimationKeyframe::bezierControlPoints[4].

CSS Animation Pipeline

CSS Parse (@keyframes)
  -> ComputedValues::AnimationDefinition
  -> CSSAnimationTimelineBuilder::BuildInstance()
  -> AnimationTimeline (registered with AnimationService)
  -> AnimationService::AdvanceBy() ticks all timelines
  -> Animation::Update() + Animation::Sample()
  -> ApplyAnimationSample() writes to ComputedValues / AnimatedTransformState
  -> HTMLPaintBuilder::PopulateStyle() reads the animated values
  -> HTMLPainter::RenderStackingContext() -> HarrlowCommandExecutor

Transform Handling

Animated transforms use a typed AnimatedTransformState struct on each BaseElement instead of string-keyed data. This struct stores 2D components (translateX, translateY, scaleX, scaleY, rotateZ), 3D components (translateZ, scaleZ, rotateX, rotateY), and an active flag. PopulateStyle() in HTMLPaintBuilder reads from this typed struct and emits TransformOperation entries that feed into the Harrlow render pipeline.

CSS transform shorthand strings (e.g., "translate3d(10px, 20px, 5px) scale(2) rotateX(45deg)") are decomposed into individual component tracks by DecomposeTransformString() in CSSAnimationTimelineBuilder.cpp. Supported functions: translate, translateX/Y/Z, translate3d, scale, scaleX/Y/Z, scale3d, rotate, rotateX/Y/Z.

The AnimatedTransformState::Has3D() method detects whether any 3D components are active, which signals the paint pipeline to use 3D transform operations (TranslateZ, ScaleZ, RotateX, RotateY).

Transform animations are correctly classified as AnimationStyleImpact::Paint (not Layout), avoiding unnecessary layout recomputation.

Fill-Mode

  • Forwards/Both: Timeline stays active after animations finish to preserve final values
  • Backwards/Both: Initial computed values are applied during the delay period (progress = 0.0)

Animatable Properties

Over 40 CSS properties are animatable, organized by impact:

Paint-only (no layout recomputation): Opacity, BackgroundColor, Color, TransformTranslateX/Y, TransformScaleX/Y, TransformRotateZ, BorderTopColor/RightColor/BottomColor/LeftColor, Visibility

Layout (triggers relayout): Display, HeightAuto, WidthAuto, MarginTop/Right/Bottom/Left, PaddingTop/Right/Bottom/Left, Top/Right/Bottom/Left, FontSize, LetterSpacing, BorderTopLeftRadius/TopRightRadius/BottomRightRadius/BottomLeftRadius, BorderTopWidth/RightWidth/BottomWidth/LeftWidth, MinWidth/MaxWidth/MinHeight/MaxHeight, LineHeight

Web Animations API (W3C)

Element.animate()

// C++ equivalent of: element.animate([{opacity: 0}, {opacity: 1}], {duration: 500})
nsDynamicArray<html::AnimationPropertyTrack> tracks;
html::AnimationPropertyTrack opacityTrack;
opacityTrack.propertyId = animation::AnimationPropertyId::Opacity;
opacityTrack.keyframes.PushBack(html::AnimationKeyframe(0.0, nsVariant(0.0f)));
opacityTrack.keyframes.PushBack(html::AnimationKeyframe(1.0, nsVariant(1.0f)));
tracks.PushBack(std::move(opacityTrack));

html::AnimationOptions opts;
opts.duration = 0.5;
opts.fillMode = html::AnimationFillMode::Forwards;

auto anim = domElement->animate(std::move(tracks), opts);
// anim->Play(), Pause(), Stop(), Reverse(), Seek() all available

Element.getAnimations()

Returns all active (non-finished, non-idle) animations on an element.

requestAnimationFrame / cancelAnimationFrame

auto window = uiView->getDomWindow();
uint32_t handle = window->requestAnimationFrame([](double timestamp) {
    // Called once per frame, after animation updates, before paint
});
window->cancelAnimationFrame(handle);

Per W3C spec, callbacks registered during a RAF callback are deferred to the next frame (snapshot-then-swap).

Composite Operations

The CompositeOperation enum supports W3C composite modes:

  • Replace (default): Animated value replaces the underlying value
  • Add: Animated value is added to the underlying value
  • Accumulate: Same as Add for numeric properties

Set via AnimationOptions::composite or AnimationSample::composite. Applied in ApplyAnimationSample() for all numeric properties (opacity, transforms, dimensions).

Architecture Components

1. Animation

Core animation class managing individual animations with duration, delay, iteration count, direction, fill mode, easing, and per-keyframe bezier control points.

2. AnimationTimeline

Manages multiple animations together. Registered with AnimationService for automatic ticking.

3. AnimationController

Advanced animation control with keyframe support and property-specific animations.

4. AnimationGroup

Groups multiple animations for coordinated playback with bezier propagation.

5. AnimationService

Singleton tick source that owns the global list of timelines and dispatches per-frame updates. CSS and script producers register their timelines here.

6. AnimationRegistry

Contains the easing implementation: SolveCubicBezier() (Newton-Raphson) and ApplyTimingFunction().

Remaining Work

The following areas need further implementation to reach full W3C Web Animations Level 2 conformance:

  1. JS binding layer: Element.animate() and window.requestAnimationFrame() are implemented in C++ but need V8/scripting bridge to be callable from JavaScript
  2. KeyframeEffect: The W3C KeyframeEffect class is not yet a separate object; keyframes are passed directly to Element.animate()
  3. Animation.finished / Animation.ready promises: Promise-based completion tracking not yet implemented
  4. animation.playbackRate: Per-animation playback rate control exists on AnimationController but not on Animation
  5. Composite operations for color properties: Add/Accumulate compositing is implemented for numeric properties but not yet for color blending
  6. AnimationEvent dispatching to JS: animationstart, animationend, animationiteration DOM events are not yet dispatched to the script layer
  7. Tweeny library removal: The Animation/tweeny/ directory is dead code (no longer included anywhere) and can be deleted from the repository
Copyright © 2026