Animations
Introduction
WIP. Azul has no CSS-animation runtime today — CSS animation: and transition: properties parse but don't interpolate. This page documents the shape of the user-facing API. Until the runtime is wired up, drive interpolation by hand from a timer.
The user-facing API is plain CSS. You write transition: opacity 200ms ease-out or a @keyframes block, and the framework interpolates between values. Until the runtime lands, the same effect is achieved with a timer that mutates your model and returns Update::RefreshDom per frame.
CSS transitions (planned)
.button {
opacity: 0.5;
transition: opacity 200ms ease-out;
}
.button:hover {
opacity: 1.0;
}
When :hover toggles, opacity interpolates from 0.5 to 1.0 over 200 milliseconds with an ease-out curve. Multiple properties separate with commas:
transition: opacity 200ms ease-out, transform 300ms ease-in-out;
The cheapest properties to animate are GPU-uploaded ones (opacity, transform) because the layout pass doesn't need to re-run. Width, height, padding, and font-size force a relayout per frame.
CSS keyframes (planned)
@keyframes pulse {
0% { opacity: 1.0; }
50% { opacity: 0.4; }
100% { opacity: 1.0; }
}
.notice {
animation: pulse 1s infinite;
}
@keyframes blocks define named animations. Apply them with the animation: shorthand or its longhands.
What works today: animate from a timer
This pattern is the floor. Once the animation runtime is wired, the framework will provide a more declarative version of the same thing. Animations driven by application logic (game state, simulation, custom physics) will always need a timer-based path.
1. Pick the property to animate
Anything you can express as a CSS property in your DOM. Prefer GPU-uploaded properties (opacity, transform) for the same reason as above.
2. Stash the animation start time and the target
Put the animation parameters in your model so the timer callback can read them:
struct State {
/// When the current animation started; None when idle
anim_start: Option<Instant>,
anim_duration: Duration,
anim_from_opacity: f32,
anim_to_opacity: f32,
/// The current interpolated value the layout callback reads
current_opacity: f32,
}
3. Install a timer when the animation should kick off
extern "C" fn on_click(data: RefAny, mut info: CallbackInfo) -> Update {
{
let mut state = data.downcast_mut::<State>().unwrap();
state.anim_start = Some(info.get_current_time());
state.anim_from_opacity = state.current_opacity;
state.anim_to_opacity = 1.0;
}
let timer = Timer::create(data.clone(), animate, info.get_system_time_fn())
.with_interval(Duration::System(SystemTimeDiff::from_millis(16)));
info.add_timer(TimerId::unique(), timer);
Update::DoNothing
}
4. The timer interpolates and terminates itself
extern "C" fn animate(data: RefAny, info: TimerCallbackInfo) -> TimerCallbackReturn {
let mut state = data.downcast_mut::<State>().unwrap();
let start = match state.anim_start {
Some(s) => s,
None => return TimerCallbackReturn::terminate_unchanged(),
};
let end = start.clone().add_duration(&state.anim_duration);
let t = info.frame_start.linear_interpolate(start, end);
let eased = ease_out_cubic(t);
state.current_opacity =
state.anim_from_opacity + (state.anim_to_opacity - state.anim_from_opacity) * eased;
if t >= 1.0 {
state.anim_start = None;
TimerCallbackReturn::terminate_and_refresh_dom()
} else {
TimerCallbackReturn::continue_and_refresh_dom()
}
}
fn ease_out_cubic(t: f32) -> f32 {
let inv = 1.0 - t;
1.0 - inv * inv * inv
}
Instant::linear_interpolate(start, end) returns a clamped 0..=1 fraction. Layer easing on top.
5. The layout callback reads the current value
extern "C" fn layout(data: RefAny, _: LayoutCallbackInfo) -> StyledDom {
let state = data.downcast_ref::<State>().unwrap();
let style = format!("opacity: {};", state.current_opacity);
Dom::create_div().with_css(&style).with_component_css(Css::empty())
}
The timer-driven path stays available even after the CSS runtime lands. Use it for animations driven by application state rather than CSS rules.
Animating images, not the DOM
For animations whose only effect is a pixel change (sprite sheet, video frame, GL texture), info.update_all_image_callbacks() re-invokes every image callback without touching layout.
Cross-references
timers: the timer mechanics this page builds on.