//! 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; } } }