feat(animation): ColorTransition para transiciones suaves de color

Transiciones Suaves (Acabado Espectacular mejora #4):
- Nuevo struct ColorTransition en animation.zig
- Interpola colores en ~200ms usando lerp
- Se inicializa automáticamente en primer uso
- Exportado en zcatgui.zig junto con HoverTransition

Uso: state.bg_transition.update(target_color, delta_ms)
     ctx.drawRect(..., state.bg_transition.current)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2025-12-30 15:34:43 +01:00
parent c657583e06
commit f0f9120da0
2 changed files with 99 additions and 0 deletions

View file

@ -678,6 +678,103 @@ pub const HoverTransition = struct {
}
};
// =============================================================================
// Color Transition (for panel backgrounds)
// =============================================================================
const Style = @import("../core/style.zig");
/// Smooth color transition for panel backgrounds.
/// Interpolates from current to target color over ~200ms.
///
/// Usage:
/// ```zig
/// pub const PanelState = struct {
/// bg_transition: ColorTransition = .{},
/// };
///
/// // In panel draw:
/// const target_color = if (has_focus) focus_color else normal_color;
/// state.bg_transition.update(target_color, ctx.delta_ms);
/// const display_color = state.bg_transition.current;
/// ctx.drawRect(..., display_color);
/// ```
pub const ColorTransition = struct {
/// Current displayed color
current: Style.Color = Style.Color.rgb(0, 0, 0),
/// Is initialized with a color?
initialized: bool = false,
/// Transition duration in milliseconds
duration_ms: f32 = 200.0,
const Self = @This();
/// Update transition towards target color
/// Returns true if color changed (for requesting redraw)
pub fn update(self: *Self, target: Style.Color, delta_ms: u64) bool {
// First call: snap to target immediately
if (!self.initialized) {
self.current = target;
self.initialized = true;
return true;
}
// Already at target?
if (self.current.r == target.r and
self.current.g == target.g and
self.current.b == target.b and
self.current.a == target.a)
{
return false;
}
// Calculate interpolation factor (chase towards target)
const t = @min(1.0, @as(f32, @floatFromInt(delta_ms)) / self.duration_ms);
// Interpolate each channel
self.current = Style.Color{
.r = lerpU8(self.current.r, target.r, t),
.g = lerpU8(self.current.g, target.g, t),
.b = lerpU8(self.current.b, target.b, t),
.a = lerpU8(self.current.a, target.a, t),
};
return true;
}
/// Reset to uninitialized state
pub fn reset(self: *Self) void {
self.initialized = false;
}
};
/// Interpolate between two u8 values
fn lerpU8(a: u8, b: u8, t: f32) u8 {
const af: f32 = @floatFromInt(a);
const bf: f32 = @floatFromInt(b);
return @intFromFloat(af + (bf - af) * t);
}
test "ColorTransition basic" {
var trans = ColorTransition{};
const black = Style.Color.rgb(0, 0, 0);
const white = Style.Color.rgb(255, 255, 255);
// First update snaps to target
_ = trans.update(black, 16);
try std.testing.expect(trans.initialized);
try std.testing.expectEqual(@as(u8, 0), trans.current.r);
// Transition towards white over time
_ = trans.update(white, 100); // ~50% transition
try std.testing.expect(trans.current.r > 0);
try std.testing.expect(trans.current.r < 255);
// Full duration reaches target
_ = trans.update(white, 200);
try std.testing.expectEqual(@as(u8, 255), trans.current.r);
}
test "HoverTransition basic" {
var hover = HoverTransition{};

View file

@ -109,6 +109,8 @@ pub const lerp = render.animation.lerp;
pub const lerpInt = render.animation.lerpInt;
pub const Spring = render.animation.Spring;
pub const SpringConfig = render.animation.SpringConfig;
pub const HoverTransition = render.animation.HoverTransition;
pub const ColorTransition = render.animation.ColorTransition;
// Effects re-exports
pub const Shadow = render.effects.Shadow;