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>
220 lines
6.8 KiB
Zig
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;
|
|
}
|
|
}
|
|
}
|