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 viansTime::Now()whenTick()is used, or honors explicit deltas viaAdvanceBy()when the host loop already knows its frame interval. Registration is mutex-protected. - CSS cubic-bezier easing backs every stock
AnimationEasingoption.Animation.cpproutes easing throughAnimationRegistry::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:
- Ticks the global
AnimationServicesingleton viaAdvanceBy()with the provided delta - Updates all active video players and Lottie animations
- Updates CSS transitions with actual frame timing
- Flushes
requestAnimationFramecallbacks (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:
| Easing | CSS Equivalent | Bezier Values |
|---|---|---|
Linear | linear | - |
Ease | ease | 0.25, 0.1, 0.25, 1.0 |
EaseIn | ease-in | 0.42, 0, 1.0, 1.0 |
EaseOut | ease-out | 0, 0, 0.58, 1.0 |
EaseInOut | ease-in-out | 0.42, 0, 0.58, 1.0 |
CubicBezier | cubic-bezier(...) | User-defined |
StepStart | step-start | - |
StepEnd | step-end | - |
Steps | steps(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 valuesBackwards/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:
- JS binding layer:
Element.animate()andwindow.requestAnimationFrame()are implemented in C++ but need V8/scripting bridge to be callable from JavaScript - KeyframeEffect: The W3C
KeyframeEffectclass is not yet a separate object; keyframes are passed directly toElement.animate() - Animation.finished / Animation.ready promises: Promise-based completion tracking not yet implemented
- animation.playbackRate: Per-animation playback rate control exists on AnimationController but not on Animation
- Composite operations for color properties: Add/Accumulate compositing is implemented for numeric properties but not yet for color blending
- AnimationEvent dispatching to JS:
animationstart,animationend,animationiterationDOM events are not yet dispatched to the script layer - Tweeny library removal: The
Animation/tweeny/directory is dead code (no longer included anywhere) and can be deleted from the repository

