//! Animation system for zcatui. //! //! Provides tools for creating smooth animations and transitions: //! - Easing functions (linear, ease-in, ease-out, etc.) //! - Tween animations between values //! - Animation sequences and timelines //! - Frame-based animation state management //! //! ## Example //! //! ```zig //! var anim = Animation.init(0, 100, 1000); // 0 to 100 over 1000ms //! anim.easing = Easing.easeInOut; //! //! while (!anim.isComplete()) { //! const value = anim.getValue(); //! // Use value for rendering //! anim.advance(delta_ms); //! } //! ``` const std = @import("std"); // ============================================================================ // Easing Functions // ============================================================================ /// Collection of easing functions for smooth animations. /// /// All functions take a progress value t in [0, 1] and return /// the eased value in [0, 1]. pub const Easing = struct { /// Linear interpolation (no easing). pub fn linear(t: f64) f64 { return t; } /// Quadratic ease-in (starts slow, accelerates). pub fn easeIn(t: f64) f64 { return t * t; } /// Quadratic ease-out (starts fast, decelerates). pub fn easeOut(t: f64) f64 { return 1.0 - (1.0 - t) * (1.0 - t); } /// Quadratic ease-in-out (smooth start and end). pub fn easeInOut(t: f64) f64 { if (t < 0.5) { return 2.0 * t * t; } else { return 1.0 - std.math.pow(f64, -2.0 * t + 2.0, 2) / 2.0; } } /// Cubic ease-in. pub fn easeInCubic(t: f64) f64 { return t * t * t; } /// Cubic ease-out. pub fn easeOutCubic(t: f64) f64 { return 1.0 - std.math.pow(f64, 1.0 - t, 3); } /// Cubic ease-in-out. pub fn easeInOutCubic(t: f64) f64 { if (t < 0.5) { return 4.0 * t * t * t; } else { return 1.0 - std.math.pow(f64, -2.0 * t + 2.0, 3) / 2.0; } } /// Exponential ease-in. pub fn easeInExpo(t: f64) f64 { if (t == 0) return 0; return std.math.pow(f64, 2, 10 * (t - 1)); } /// Exponential ease-out. pub fn easeOutExpo(t: f64) f64 { if (t == 1) return 1; return 1.0 - std.math.pow(f64, 2, -10 * t); } /// Bounce ease-out (bounces at the end). pub fn easeOutBounce(t: f64) f64 { const n1: f64 = 7.5625; const d1: f64 = 2.75; if (t < 1.0 / d1) { return n1 * t * t; } else if (t < 2.0 / d1) { const t2 = t - 1.5 / d1; return n1 * t2 * t2 + 0.75; } else if (t < 2.5 / d1) { const t2 = t - 2.25 / d1; return n1 * t2 * t2 + 0.9375; } else { const t2 = t - 2.625 / d1; return n1 * t2 * t2 + 0.984375; } } /// Elastic ease-out (spring-like). pub fn easeOutElastic(t: f64) f64 { const c4: f64 = (2.0 * std.math.pi) / 3.0; if (t == 0) return 0; if (t == 1) return 1; return std.math.pow(f64, 2, -10 * t) * @sin((t * 10 - 0.75) * c4) + 1.0; } /// Back ease-out (overshoots then returns). pub fn easeOutBack(t: f64) f64 { const c1: f64 = 1.70158; const c3: f64 = c1 + 1; const t2 = t - 1; return 1.0 + c3 * std.math.pow(f64, t2, 3) + c1 * std.math.pow(f64, t2, 2); } }; /// Type alias for easing function pointer. pub const EasingFn = *const fn (f64) f64; // ============================================================================ // Animation // ============================================================================ /// A single value animation (tween). /// /// Animates a value from `start` to `end` over a specified duration. pub const Animation = struct { /// Starting value. start_value: f64, /// Ending value. end_value: f64, /// Total duration in milliseconds. duration_ms: u64, /// Current elapsed time in milliseconds. elapsed_ms: u64 = 0, /// Easing function to use. easing: EasingFn = Easing.linear, /// Whether the animation is paused. paused: bool = false, /// Number of times to repeat (-1 for infinite). repeat_count: i32 = 0, /// Current repeat iteration. current_repeat: i32 = 0, /// Whether to reverse on repeat (ping-pong). reverse_on_repeat: bool = false, /// Current direction (true = forward). forward: bool = true, /// Delay before starting (ms). delay_ms: u64 = 0, /// Remaining delay. remaining_delay_ms: u64 = 0, /// Creates a new animation. pub fn init(start: f64, end: f64, duration_ms: u64) Animation { return .{ .start_value = start, .end_value = end, .duration_ms = duration_ms, .remaining_delay_ms = 0, }; } /// Creates an animation from an integer value. pub fn fromInt(start: i64, end: i64, duration_ms: u64) Animation { return init(@floatFromInt(start), @floatFromInt(end), duration_ms); } /// Sets the easing function. pub fn setEasing(self: Animation, easing: EasingFn) Animation { var anim = self; anim.easing = easing; return anim; } /// Sets the repeat count (-1 for infinite). pub fn setRepeat(self: Animation, count: i32) Animation { var anim = self; anim.repeat_count = count; return anim; } /// Sets ping-pong mode (reverse on repeat). pub fn setPingPong(self: Animation, enabled: bool) Animation { var anim = self; anim.reverse_on_repeat = enabled; return anim; } /// Sets the delay before starting. pub fn setDelay(self: Animation, delay_ms: u64) Animation { var anim = self; anim.delay_ms = delay_ms; anim.remaining_delay_ms = delay_ms; return anim; } /// Advances the animation by the given delta time. pub fn advance(self: *Animation, delta_ms: u64) void { if (self.paused) return; // Handle delay if (self.remaining_delay_ms > 0) { if (delta_ms >= self.remaining_delay_ms) { const remaining = delta_ms - self.remaining_delay_ms; self.remaining_delay_ms = 0; if (remaining > 0) { self.advance(remaining); } } else { self.remaining_delay_ms -= delta_ms; } return; } // Advance time if (self.forward) { self.elapsed_ms +|= delta_ms; } else { if (delta_ms >= self.elapsed_ms) { self.elapsed_ms = 0; } else { self.elapsed_ms -= delta_ms; } } // Check for completion if (self.forward and self.elapsed_ms >= self.duration_ms) { self.elapsed_ms = self.duration_ms; self.handleRepeat(); } else if (!self.forward and self.elapsed_ms == 0) { self.handleRepeat(); } } fn handleRepeat(self: *Animation) void { if (self.repeat_count == 0) return; // No repeat, stay complete if (self.repeat_count < 0 or self.current_repeat < self.repeat_count) { self.current_repeat += 1; if (self.reverse_on_repeat) { self.forward = !self.forward; } else { self.elapsed_ms = 0; } } } /// Returns the current progress (0.0 to 1.0). pub fn getProgress(self: *const Animation) f64 { if (self.duration_ms == 0) return 1.0; const raw = @as(f64, @floatFromInt(self.elapsed_ms)) / @as(f64, @floatFromInt(self.duration_ms)); return @min(1.0, @max(0.0, raw)); } /// Returns the current eased progress. pub fn getEasedProgress(self: *const Animation) f64 { return self.easing(self.getProgress()); } /// Returns the current animated value. pub fn getValue(self: *const Animation) f64 { const t = self.getEasedProgress(); return self.start_value + (self.end_value - self.start_value) * t; } /// Returns the current value as an integer. pub fn getValueInt(self: *const Animation) i64 { return @intFromFloat(@round(self.getValue())); } /// Returns the current value as a u16. pub fn getValueU16(self: *const Animation) u16 { const val = self.getValueInt(); if (val < 0) return 0; if (val > 65535) return 65535; return @intCast(val); } /// Returns true if the animation is complete. pub fn isComplete(self: *const Animation) bool { if (self.repeat_count < 0) return false; // Infinite if (self.repeat_count > 0 and self.current_repeat < self.repeat_count) return false; // For ping-pong, also check if we're going backward (not yet done) if (self.reverse_on_repeat and !self.forward) return false; return self.elapsed_ms >= self.duration_ms; } /// Returns true if the animation is running. pub fn isRunning(self: *const Animation) bool { return !self.paused and !self.isComplete() and self.remaining_delay_ms == 0; } /// Pauses the animation. pub fn pause(self: *Animation) void { self.paused = true; } /// Resumes the animation. pub fn unpause(self: *Animation) void { self.paused = false; } /// Resets the animation to the beginning. pub fn reset(self: *Animation) void { self.elapsed_ms = 0; self.current_repeat = 0; self.forward = true; self.remaining_delay_ms = self.delay_ms; } /// Seeks to a specific time. pub fn seekTo(self: *Animation, time_ms: u64) void { self.elapsed_ms = @min(time_ms, self.duration_ms); } }; // ============================================================================ // AnimationGroup // ============================================================================ /// A group of animations that can be run in parallel or sequence. pub const AnimationGroup = struct { /// Animations in the group. animations: []Animation, /// Whether to run in parallel (true) or sequence (false). is_parallel: bool = true, /// Index of current animation (for sequential mode). current_index: usize = 0, /// Creates a parallel animation group. pub fn parallel(animations: []Animation) AnimationGroup { return .{ .animations = animations, .is_parallel = true, }; } /// Creates a sequential animation group. pub fn sequential(animations: []Animation) AnimationGroup { return .{ .animations = animations, .is_parallel = false, }; } /// Advances all animations. pub fn advance(self: *AnimationGroup, delta_ms: u64) void { if (self.is_parallel) { for (self.animations) |*anim| { anim.advance(delta_ms); } } else { // Sequential if (self.current_index < self.animations.len) { self.animations[self.current_index].advance(delta_ms); if (self.animations[self.current_index].isComplete()) { self.current_index += 1; } } } } /// Returns true if all animations are complete. pub fn isComplete(self: *const AnimationGroup) bool { if (self.is_parallel) { for (self.animations) |*anim| { if (!anim.isComplete()) return false; } return true; } else { return self.current_index >= self.animations.len; } } /// Resets all animations. pub fn reset(self: *AnimationGroup) void { for (self.animations) |*anim| { anim.reset(); } self.current_index = 0; } }; // ============================================================================ // Timer // ============================================================================ /// A simple timer for frame-based updates. pub const Timer = struct { /// Duration in milliseconds. duration_ms: u64, /// Elapsed time in milliseconds. elapsed_ms: u64 = 0, /// Whether the timer repeats. is_repeating: bool = false, /// Whether the timer is running. running: bool = true, /// Callback data (for user purposes). user_data: usize = 0, /// Creates a one-shot timer. pub fn oneShot(duration_ms: u64) Timer { return .{ .duration_ms = duration_ms, .is_repeating = false, }; } /// Creates a repeating timer. pub fn repeating(duration_ms: u64) Timer { return .{ .duration_ms = duration_ms, .is_repeating = true, }; } /// Advances the timer. Returns true if the timer triggered. pub fn advance(self: *Timer, delta_ms: u64) bool { if (!self.running) return false; self.elapsed_ms +|= delta_ms; if (self.elapsed_ms >= self.duration_ms) { if (self.is_repeating) { self.elapsed_ms = self.elapsed_ms % self.duration_ms; } else { self.running = false; } return true; } return false; } /// Returns the progress (0.0 to 1.0). pub fn progress(self: *const Timer) f64 { if (self.duration_ms == 0) return 1.0; return @as(f64, @floatFromInt(self.elapsed_ms)) / @as(f64, @floatFromInt(self.duration_ms)); } /// Resets the timer. pub fn reset(self: *Timer) void { self.elapsed_ms = 0; self.running = true; } /// Stops the timer. pub fn stop(self: *Timer) void { self.running = false; } /// Starts/resumes the timer. pub fn start(self: *Timer) void { self.running = true; } }; // ============================================================================ // Interpolation helpers // ============================================================================ /// Linear interpolation between two values. pub fn lerp(a: f64, b: f64, t: f64) f64 { return a + (b - a) * t; } /// Linear interpolation for integers. pub fn lerpInt(a: i64, b: i64, t: f64) i64 { return @intFromFloat(@round(lerp(@floatFromInt(a), @floatFromInt(b), t))); } /// Inverse linear interpolation (find t given value). pub fn inverseLerp(a: f64, b: f64, value: f64) f64 { if (b == a) return 0; return (value - a) / (b - a); } /// Maps a value from one range to another. pub fn mapRange(value: f64, in_min: f64, in_max: f64, out_min: f64, out_max: f64) f64 { const t = inverseLerp(in_min, in_max, value); return lerp(out_min, out_max, t); } // ============================================================================ // Tests // ============================================================================ test "Easing.linear" { try std.testing.expectEqual(@as(f64, 0.0), Easing.linear(0.0)); try std.testing.expectEqual(@as(f64, 0.5), Easing.linear(0.5)); try std.testing.expectEqual(@as(f64, 1.0), Easing.linear(1.0)); } test "Easing.easeIn" { try std.testing.expectEqual(@as(f64, 0.0), Easing.easeIn(0.0)); try std.testing.expectEqual(@as(f64, 0.25), Easing.easeIn(0.5)); try std.testing.expectEqual(@as(f64, 1.0), Easing.easeIn(1.0)); } test "Easing.easeOut" { try std.testing.expectEqual(@as(f64, 0.0), Easing.easeOut(0.0)); try std.testing.expectEqual(@as(f64, 0.75), Easing.easeOut(0.5)); try std.testing.expectEqual(@as(f64, 1.0), Easing.easeOut(1.0)); } test "Animation basic" { var anim = Animation.init(0, 100, 1000); try std.testing.expectEqual(@as(f64, 0), anim.getValue()); try std.testing.expect(!anim.isComplete()); anim.advance(500); try std.testing.expectEqual(@as(f64, 50), anim.getValue()); try std.testing.expect(!anim.isComplete()); anim.advance(500); try std.testing.expectEqual(@as(f64, 100), anim.getValue()); try std.testing.expect(anim.isComplete()); } test "Animation with easing" { var anim = Animation.init(0, 100, 1000).setEasing(Easing.easeIn); anim.advance(500); // easeIn at 0.5 = 0.25, so value should be 25 try std.testing.expectEqual(@as(f64, 25), anim.getValue()); } test "Animation repeat" { var anim = Animation.init(0, 100, 100).setRepeat(2); // First iteration anim.advance(100); try std.testing.expect(!anim.isComplete()); // Second iteration anim.advance(100); try std.testing.expect(!anim.isComplete()); // Third iteration (should complete) anim.advance(100); try std.testing.expect(anim.isComplete()); } test "Animation ping-pong" { var anim = Animation.init(0, 100, 100).setRepeat(1).setPingPong(true); anim.advance(100); // Forward complete try std.testing.expect(!anim.isComplete()); try std.testing.expect(!anim.forward); anim.advance(50); // Half back try std.testing.expectEqual(@as(f64, 50), anim.getValue()); } test "Timer one-shot" { var timer = Timer.oneShot(100); try std.testing.expect(!timer.advance(50)); try std.testing.expect(timer.advance(50)); try std.testing.expect(!timer.running); } test "Timer repeating" { var timer = Timer.repeating(100); try std.testing.expect(timer.advance(100)); try std.testing.expect(timer.running); try std.testing.expect(timer.advance(100)); try std.testing.expect(timer.running); } test "lerp" { try std.testing.expectEqual(@as(f64, 0), lerp(0, 100, 0)); try std.testing.expectEqual(@as(f64, 50), lerp(0, 100, 0.5)); try std.testing.expectEqual(@as(f64, 100), lerp(0, 100, 1)); try std.testing.expectEqual(@as(f64, 25), lerp(0, 100, 0.25)); } test "inverseLerp" { try std.testing.expectEqual(@as(f64, 0), inverseLerp(0, 100, 0)); try std.testing.expectEqual(@as(f64, 0.5), inverseLerp(0, 100, 50)); try std.testing.expectEqual(@as(f64, 1), inverseLerp(0, 100, 100)); } test "mapRange" { // Map 0-100 to 0-1 try std.testing.expectEqual(@as(f64, 0.5), mapRange(50, 0, 100, 0, 1)); // Map 0-100 to 100-200 try std.testing.expectEqual(@as(f64, 150), mapRange(50, 0, 100, 100, 200)); }