zcatui/examples/animation_demo.zig
reugenio 7c6515765e Add animation system with easing functions and timers
New features:
- Easing functions: linear, ease-in/out, cubic, expo, bounce, elastic, back
- Animation struct for tweening values over time
- AnimationGroup for parallel/sequential animations
- Timer for frame-based updates (one-shot and repeating)
- Helper functions: lerp, inverseLerp, mapRange

Animation features:
- Configurable repeat count (-1 for infinite)
- Ping-pong mode (reverse on repeat)
- Delay before start
- Pause/resume control
- getValue as f64, i64, or u16

Example: animation_demo.zig comparing 5 easing functions

Tests: 12 animation tests, all pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 13:40:07 +01:00

220 lines
6.8 KiB
Zig

//! Animation demo for zcatui.
//!
//! Demonstrates the animation system with:
//! - Smooth gauge transitions
//! - Bouncing progress bar
//! - Multiple easing functions comparison
//! - Timer-based updates
//!
//! Run with: zig build animation-demo
const std = @import("std");
const zcatui = @import("zcatui");
const Terminal = zcatui.Terminal;
const Buffer = zcatui.Buffer;
const Rect = zcatui.Rect;
const Style = zcatui.Style;
const Color = zcatui.Color;
const Event = zcatui.Event;
const KeyCode = zcatui.KeyCode;
const Layout = zcatui.Layout;
const Constraint = zcatui.Constraint;
const Block = zcatui.widgets.Block;
const Borders = zcatui.widgets.Borders;
const Gauge = zcatui.widgets.Gauge;
const Animation = zcatui.Animation;
const Easing = zcatui.Easing;
const Timer = zcatui.Timer;
const AppState = struct {
// Animations for different easing demos
linear_anim: Animation,
ease_in_anim: Animation,
ease_out_anim: Animation,
bounce_anim: Animation,
elastic_anim: Animation,
// Timer for frame tracking
frame_timer: Timer,
tick_count: u64 = 0,
running: bool = true,
last_time_ms: i64 = 0,
fn init() AppState {
// All animations: 0 to 100 over 2 seconds, repeating ping-pong
return .{
.linear_anim = Animation.init(0, 100, 2000)
.setEasing(Easing.linear)
.setRepeat(-1)
.setPingPong(true),
.ease_in_anim = Animation.init(0, 100, 2000)
.setEasing(Easing.easeInCubic)
.setRepeat(-1)
.setPingPong(true),
.ease_out_anim = Animation.init(0, 100, 2000)
.setEasing(Easing.easeOutCubic)
.setRepeat(-1)
.setPingPong(true),
.bounce_anim = Animation.init(0, 100, 2000)
.setEasing(Easing.easeOutBounce)
.setRepeat(-1)
.setPingPong(true),
.elastic_anim = Animation.init(0, 100, 2000)
.setEasing(Easing.easeOutElastic)
.setRepeat(-1)
.setPingPong(true),
.frame_timer = Timer.repeating(1000), // 1 second for FPS counting
};
}
fn update(self: *AppState, delta_ms: u64) void {
// Advance all animations
self.linear_anim.advance(delta_ms);
self.ease_in_anim.advance(delta_ms);
self.ease_out_anim.advance(delta_ms);
self.bounce_anim.advance(delta_ms);
self.elastic_anim.advance(delta_ms);
// Update timer
if (self.frame_timer.advance(delta_ms)) {
self.tick_count += 1;
}
}
fn restartAnimations(self: *AppState) void {
self.linear_anim.reset();
self.ease_in_anim.reset();
self.ease_out_anim.reset();
self.bounce_anim.reset();
self.elastic_anim.reset();
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var term = try Terminal.init(allocator);
defer term.deinit();
var state = AppState.init();
// Get initial timestamp
state.last_time_ms = std.time.milliTimestamp();
while (state.running) {
// Calculate delta time
const current_time_ms = std.time.milliTimestamp();
const delta_ms: u64 = @intCast(@max(0, current_time_ms - state.last_time_ms));
state.last_time_ms = current_time_ms;
// Update animations
state.update(delta_ms);
// Render
try term.drawWithContext(&state, render);
// Poll events with short timeout for smooth animation
if (try term.pollEvent(16)) |event| { // ~60 FPS target
handleEvent(&state, event);
}
}
}
fn handleEvent(state: *AppState, event: Event) void {
switch (event) {
.key => |key| {
switch (key.code) {
.esc => state.running = false,
.char => |c| {
switch (c) {
'q', 'Q' => state.running = false,
'r', 'R' => state.restartAnimations(),
else => {},
}
},
else => {},
}
},
else => {},
}
}
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
// Main layout
const chunks = Layout.vertical(&.{
Constraint.length(3), // Title
Constraint.length(3), // Linear
Constraint.length(3), // Ease-in
Constraint.length(3), // Ease-out
Constraint.length(3), // Bounce
Constraint.length(3), // Elastic
Constraint.min(0), // Help
}).split(area);
// Title
renderTitle(state, chunks.get(0), buf);
// Animation gauges
renderGauge("Linear", state.linear_anim.getValueU16(), Color.cyan, chunks.get(1), buf);
renderGauge("Ease-In (Cubic)", state.ease_in_anim.getValueU16(), Color.green, chunks.get(2), buf);
renderGauge("Ease-Out (Cubic)", state.ease_out_anim.getValueU16(), Color.yellow, chunks.get(3), buf);
renderGauge("Bounce", state.bounce_anim.getValueU16(), Color.magenta, chunks.get(4), buf);
renderGauge("Elastic", state.elastic_anim.getValueU16(), Color.red, chunks.get(5), buf);
// Help
renderHelp(chunks.get(6), buf);
}
fn renderTitle(state: *AppState, area: Rect, buf: *Buffer) void {
const title_block = Block.init()
.title(" Animation Demo ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.white));
title_block.render(area, buf);
const inner = title_block.inner(area);
var info_buf: [64]u8 = undefined;
const info = std.fmt.bufPrint(&info_buf, "Tick: {d} | All animations ping-pong 0-100 over 2s", .{state.tick_count}) catch "???";
_ = buf.setString(inner.left(), inner.top(), info, Style.default);
}
fn renderGauge(title: []const u8, value: u16, color: Color, area: Rect, buf: *Buffer) void {
// Clamp value to 0-100
const percent: u16 = @min(100, value);
const gauge = Gauge.init()
.setBlock(Block.init().title(title).setBorders(Borders.all))
.percent(percent)
.gaugeStyle(Style.default.fg(color));
gauge.render(area, buf);
}
fn renderHelp(area: Rect, buf: *Buffer) void {
const help_block = Block.init()
.title(" Controls ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.blue));
help_block.render(area, buf);
const inner = help_block.inner(area);
var y = inner.top();
const lines = [_][]const u8{
"r - Restart all animations",
"q/ESC - Quit",
"",
"Compare how different easing functions",
"create different motion feels!",
};
for (lines) |line| {
if (y < inner.bottom()) {
_ = buf.setString(inner.left(), y, line, Style.default);
y += 1;
}
}
}