Keyframe Timelines
For time-based animations with precise control, use AnimatedTimeline. Timelines support multiple animation entries, looping, alternate (ping-pong) mode, and coordinated playback.
Creating Timelines
In WindowedContext
#![allow(unused)]
fn main() {
fn my_component(ctx: &WindowedContext) -> impl ElementBuilder {
// Create a persisted timeline
let timeline = ctx.use_animated_timeline();
// With a custom key
let loader_timeline = ctx.use_animated_timeline_for("loader");
// ...
}
}
In StateContext (Stateful Elements)
#![allow(unused)]
fn main() {
stateful::<ButtonState>().on_state(|ctx| {
// use_timeline returns (entry_ids, TimelineHandle)
let ((entry1, entry2), timeline) = ctx.use_timeline("fade", |t| {
let e1 = t.add(0, 500, 0.0, 1.0);
let e2 = t.add(250, 500, 0.0, 100.0);
t.set_loop(-1);
t.start();
(e1, e2)
});
let value1 = timeline.get(entry1).unwrap_or(0.0);
let value2 = timeline.get(entry2).unwrap_or(0.0);
div()
})
}
Configuring Timelines
Use the configure() method to set up animations once:
#![allow(unused)]
fn main() {
let timeline = ctx.use_animated_timeline();
// Configure returns entry IDs for accessing values later
let entry_id = timeline.lock().unwrap().configure(|t| {
let id = t.add(0, 1000, 0.0, 360.0); // 0ms start, 1000ms duration, 0° to 360°
t.set_loop(-1); // Loop forever (-1 = infinite)
t.start();
id
});
}
The closure only runs on first call. Subsequent calls return existing entry IDs.
Adding Animations
Basic Entry
#![allow(unused)]
fn main() {
timeline.lock().unwrap().configure(|t| {
// add(offset_ms, duration_ms, start_value, end_value)
let rotation = t.add(0, 1000, 0.0, 360.0);
let scale = t.add(0, 500, 1.0, 1.5); // Same start, shorter duration
let opacity = t.add(500, 500, 1.0, 0.0); // Starts at 500ms
(rotation, scale, opacity) // Return tuple of IDs
});
}
With Easing
#![allow(unused)]
fn main() {
use blinc_animation::Easing;
timeline.lock().unwrap().configure(|t| {
// add_with_easing(offset_ms, duration_ms, start, end, easing)
let smooth = t.add_with_easing(0, 500, 0.0, 60.0, Easing::EaseInOut);
let bouncy = t.add_with_easing(0, 500, 0.0, 1.0, Easing::EaseOutQuad);
(smooth, bouncy)
});
}
Using StaggerBuilder
For multiple entries with automatic offset calculation:
#![allow(unused)]
fn main() {
timeline.lock().unwrap().configure(|t| {
// stagger(base_offset, stagger_amount)
let mut stagger = t.stagger(0, 100); // 0ms base, 100ms between each
let bar1 = stagger.add(500, 0.0, 60.0); // offset: 0ms
let bar2 = stagger.add(500, 0.0, 60.0); // offset: 100ms
let bar3 = stagger.add(500, 0.0, 60.0); // offset: 200ms
// With easing
let bar4 = stagger.add_with_easing(500, 0.0, 60.0, Easing::EaseInOut);
(bar1, bar2, bar3, bar4)
});
}
Reading Values
#![allow(unused)]
fn main() {
let value = timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);
// Get entry progress (0.0 to 1.0)
let progress = timeline.lock().unwrap().entry_progress(entry_id);
// Get overall timeline progress
let total_progress = timeline.lock().unwrap().progress();
}
Playback Control
#![allow(unused)]
fn main() {
let mut t = timeline.lock().unwrap();
t.start(); // Start playing
t.pause(); // Pause (can resume)
t.resume(); // Resume from pause
t.stop(); // Stop and reset
t.restart(); // Start from beginning
t.reverse(); // Toggle playback direction
t.seek(500.0); // Jump to 500ms position
t.set_loop(3); // Loop 3 times
t.set_loop(-1); // Loop forever
t.set_alternate(true); // Ping-pong mode
t.set_playback_rate(2.0); // 2x speed
t.is_playing(); // Check if playing
t.progress(); // Overall progress (0.0 to 1.0)
}
Alternate (Ping-Pong) Mode
Enable alternate mode for back-and-forth animations that maintain stagger across loops:
#![allow(unused)]
fn main() {
let ((bar1, bar2, bar3), timeline) = ctx.use_timeline("bars", |t| {
// Three staggered entries
let b1 = t.add_with_easing(0, 500, 0.0, 60.0, Easing::EaseInOut);
let b2 = t.add_with_easing(100, 500, 0.0, 60.0, Easing::EaseInOut);
let b3 = t.add_with_easing(200, 500, 0.0, 60.0, Easing::EaseInOut);
t.set_alternate(true); // Reverse on each loop
t.set_loop(-1); // Loop forever
t.start();
(b1, b2, b3)
});
}
With alternate mode:
- Timeline plays forward (0 → duration)
- On completion, reverses direction (duration → 0)
- Stagger offsets maintain their relative timing
- No jump back to start - smooth continuous motion
Example: Staggered Wave Animation
#![allow(unused)]
fn main() {
fn sliding_bars() -> impl ElementBuilder {
stateful::<NoState>().on_state(|ctx| {
let ((bar1_id, bar2_id, bar3_id), timeline) = ctx.use_timeline("bars", |t| {
// Staggered entries with easing
let bar1 = t.add_with_easing(0, 500, 0.0, 60.0, Easing::EaseInOut);
let bar2 = t.add_with_easing(100, 500, 0.0, 60.0, Easing::EaseInOut);
let bar3 = t.add_with_easing(200, 500, 0.0, 60.0, Easing::EaseInOut);
t.set_alternate(true);
t.set_loop(-1);
t.start();
(bar1, bar2, bar3)
});
let bar1_x = timeline.get(bar1_id).unwrap_or(0.0);
let bar2_x = timeline.get(bar2_id).unwrap_or(0.0);
let bar3_x = timeline.get(bar3_id).unwrap_or(0.0);
div()
.flex_col()
.gap(12.0)
.child(div().w(30.0).h(12.0).bg(Color::GREEN)
.transform(Transform::translate(bar1_x, 0.0)))
.child(div().w(30.0).h(12.0).bg(Color::YELLOW)
.transform(Transform::translate(bar2_x, 0.0)))
.child(div().w(30.0).h(12.0).bg(Color::RED)
.transform(Transform::translate(bar3_x, 0.0)))
})
}
}
Example: Spinning Loader
#![allow(unused)]
fn main() {
use std::f32::consts::PI;
fn spinning_loader(ctx: &WindowedContext) -> impl ElementBuilder {
let timeline = ctx.use_animated_timeline();
let entry_id = timeline.lock().unwrap().configure(|t| {
let id = t.add(0, 1000, 0.0, 360.0);
t.set_loop(-1);
t.start();
id
});
let render_timeline = Arc::clone(&timeline);
canvas(move |draw_ctx, bounds| {
let angle_deg = render_timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);
let angle_rad = angle_deg * PI / 180.0;
let cx = bounds.width / 2.0;
let cy = bounds.height / 2.0;
let radius = 30.0;
// Draw spinning arc
// ... drawing code
})
.w(80.0)
.h(80.0)
}
}
Example: Pulsing Ring
#![allow(unused)]
fn main() {
fn pulsing_ring() -> impl ElementBuilder {
stateful::<ButtonState>().on_state(|ctx| {
let is_running = ctx.use_signal("running", || false);
// Keyframe animations with ping-pong
let scale = ctx.use_keyframes("scale", |k| {
k.at(0, 0.8)
.at(800, 1.2)
.ease(Easing::EaseInOut)
.ping_pong()
.loop_infinite()
});
let opacity = ctx.use_keyframes("opacity", |k| {
k.at(0, 0.4)
.at(800, 1.0)
.ease(Easing::EaseInOut)
.ping_pong()
.loop_infinite()
});
// Toggle on click
if let Some(event) = ctx.event() {
if event.event_type == POINTER_UP {
if is_running.get() {
scale.stop();
opacity.stop();
is_running.set(false);
} else {
scale.start();
opacity.start();
is_running.set(true);
}
}
}
let s = scale.get();
let o = opacity.get();
div()
.w(60.0).h(60.0)
.border(4.0, Color::rgba(1.0, 0.5, 0.3, o))
.rounded(30.0)
.transform(Transform::scale(s, s))
})
}
}
Example: Progress Bar
#![allow(unused)]
fn main() {
fn animated_progress(ctx: &WindowedContext) -> impl ElementBuilder {
let timeline = ctx.use_animated_timeline();
let entry_id = timeline.lock().unwrap().configure(|t| {
let id = t.add(0, 2000, 0.0, 1.0); // 2 second fill
t.start();
id
});
let click_timeline = Arc::clone(&timeline);
let render_timeline = Arc::clone(&timeline);
div()
.w(200.0)
.h(20.0)
.rounded(10.0)
.bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
.on_click(move |_| {
// Restart on click
let mut t = click_timeline.lock().unwrap();
t.stop();
t.start();
})
.child(
canvas(move |draw_ctx, bounds| {
let progress = render_timeline.lock().unwrap()
.get(entry_id)
.unwrap_or(0.0);
let fill_width = bounds.width * progress;
draw_ctx.fill_rect(
Rect::new(0.0, 0.0, fill_width, bounds.height),
CornerRadius::uniform(10.0),
Brush::Solid(Color::rgba(0.4, 0.6, 1.0, 1.0)),
);
})
.w_full()
.h_full()
)
}
}
ConfigureResult Types
The configure() method supports various return types:
#![allow(unused)]
fn main() {
// Single entry
let id: TimelineEntryId = t.configure(|t| t.add(...));
// Tuple of entries
let (a, b): (TimelineEntryId, TimelineEntryId) = t.configure(|t| {
(t.add(...), t.add(...))
});
// Triple
let (a, b, c) = t.configure(|t| {
(t.add(...), t.add(...), t.add(...))
});
// Vec for dynamic counts
let ids: Vec<TimelineEntryId> = t.configure(|t| {
(0..5).map(|i| t.add(i * 100, 500, 0.0, 1.0)).collect()
});
}
Available Easing Functions
#![allow(unused)]
fn main() {
use blinc_animation::Easing;
Easing::Linear // No easing
Easing::EaseIn // Slow start (cubic)
Easing::EaseOut // Slow end (cubic)
Easing::EaseInOut // Slow start and end (cubic)
Easing::EaseInQuad // Quadratic ease in
Easing::EaseOutQuad // Quadratic ease out
Easing::EaseInOutQuad // Quadratic ease in-out
Easing::EaseInCubic // Cubic ease in
Easing::EaseOutCubic // Cubic ease out
Easing::EaseInOutCubic // Cubic ease in-out
Easing::EaseInQuart // Quartic ease in
Easing::EaseOutQuart // Quartic ease out
Easing::EaseInOutQuart // Quartic ease in-out
Easing::CubicBezier(x1, y1, x2, y2) // Custom bezier curve
}
Timeline vs Spring
| Feature | Timeline | Spring |
|---|---|---|
| Duration | Fixed | Physics-based |
| Looping | Built-in | Manual |
| Multiple values | Single timeline | Individual values |
| Ping-pong | set_alternate(true) | Manual reverse |
| Interruption | Restart needed | Natural blend |
| Use case | Continuous loops, sequences | Interactive, responsive |
Use timelines for:
- Loading spinners
- Background animations
- Sequenced animations
- Staggered wave effects
- Precise timing control
Use springs for:
- User interactions
- Drag and drop
- Hover effects
- Natural motion