Skip to content

Animation Guide

Pulp provides a first-class animation system integrated into the view layer. Animations are deterministic, theme-aware, and work seamlessly with both C++ widgets and the JS bridge.

Core Concepts

FrameClock

The FrameClock is the single authoritative time source for all view animations. It lives on the UI thread and is advanced once per frame by the window host.

#include <pulp/view/frame_clock.hpp>

FrameClock clock;
root_view.set_frame_clock(&clock);

// In your render loop:
clock.tick(dt_seconds); // e.g., 0.016f for 60fps

Children access the clock via frame_clock(), which walks up the parent chain.

ValueAnimation

ValueAnimation is a lightweight, embeddable animator designed to be a member variable of a widget. No heap allocation.

#include <pulp/view/animation.hpp>

ValueAnimation opacity(0.0f);       // start at 0
opacity.animate_to(1.0f, 0.15f);    // fade in over 150ms
opacity.animate_to(1.0f, 0.15f, easing::ease_out_cubic); // with easing

// In your frame tick:
opacity.advance(dt);
float current = opacity.value();     // interpolated value
bool still_going = opacity.animating();

Easing Functions

Pulp includes these easing functions in pulp::view::easing:

Function Character
linear Constant speed
ease_in_quad Slow start
ease_out_quad Slow end
ease_in_out_quad Slow start and end
ease_in_cubic Slower start
ease_out_cubic Slower end (good default)
ease_in_out_cubic Smooth S-curve
ease_in_expo Exponential acceleration
ease_out_expo Exponential deceleration
ease_out_elastic Springy overshoot
ease_out_bounce Bouncing settle

Resolve by name with easing_by_name("ease_out_cubic").

Motion Tokens

Animation durations and easing choices are part of the design token system, not hardcoded constants. This means the AI Style Designer can change the "feel" of a plugin, not just its colors.

Duration Tokens

Token Dark default Pro Audio Purpose
motion.duration.fast 0.08s 0.06s Hover, focus ring
motion.duration.normal 0.15s 0.12s Toggle, button press
motion.duration.slow 0.30s 0.25s Panel open/close
motion.duration.meter_decay 0.30s 0.30s Meter RMS falloff
motion.duration.peak_hold 1.50s 1.50s Peak indicator hold

Easing Tokens

Token Default
motion.easing.interaction ease_out_cubic
motion.easing.enter ease_out_quad
motion.easing.exit ease_in_quad

Using Tokens in Widgets

void MyWidget::on_mouse_enter() {
    float dur = resolve_dimension("motion.duration.fast", 0.08f);
    hover_.animate_to(1.0f, dur, easing::ease_out_quad);
}

Built-in Widget Animations

These animations are built into Pulp's shipped widgets:

Widget Animation Token
Toggle Thumb slides between on/off motion.duration.normal
Toggle Hover highlight motion.duration.fast
Knob Hover glow ring motion.duration.fast
Fader Thumb scale on hover motion.duration.fast
Tooltip Fade in/out motion.duration.normal

JS Bridge Animation API

animate()

animate(widgetId, property, targetValue, durationMs, easingName);
// Example:
animate('volume', 'value', 0.75, 300, 'ease_out_cubic');

Motion Token Control

setMotionToken('motion.duration.fast', 0.05);  // make hover snappier
getMotionToken('motion.duration.fast');          // read current value

Widget Visibility

setVisible('panel', false);   // hide
setVisible('panel', true);    // show
removeWidget('panel');        // remove from tree entirely

Testing Animations

All animation tests use deterministic frame stepping -- no wall-clock timing:

FrameClock clock;
Toggle toggle;
toggle.set_on(true);

// Step through frames
for (int i = 0; i < 20; i++)
    toggle.advance_animations(0.016f);

// Assert settled state
REQUIRE(toggle.thumb_position() == Approx(1.0f));

Design Philosophy

  • Animation is subtle by default. The goal is polished, not flashy.
  • Motion timing belongs in the design system, not scattered constants.
  • Widget-local animations are cheap and ubiquitous -- they're member variables, not managed objects.
  • The FrameClock is deterministic -- tests never use wall-clock sleeps.