feat(animation): Liquid UI V2 - Mayor fluidez y contraste

- ColorTransition: 200ms → 500ms (transiciones más perceptibles)
- deriveDarkPalette: 4%/20% base color (mayor contraste focus/unfocus)
- deriveLightPalette: 1%/6% base color (proporcional)
- Context: añadir requestAnimationFrame/needsAnimationFrame
- Tests actualizados para nuevos umbrales

🤖 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 19:24:36 +01:00
parent 0e913cda55
commit ed2701fbd8
3 changed files with 47 additions and 26 deletions

View file

@ -102,6 +102,10 @@ pub const Context = struct {
/// Used for idle detection (e.g., cursor stops blinking after inactivity)
last_input_time_ms: u64 = 0,
/// Flag set by widgets that have ongoing animations (e.g., color transitions).
/// Main loop should check this and request another frame if true.
needs_animation_frame: bool = false,
/// Optional text measurement function (set by application with TTF font)
/// Returns pixel width of text. If null, falls back to char_width * len.
text_measure_fn: ?*const fn ([]const u8) u32 = null,
@ -204,6 +208,9 @@ pub const Context = struct {
// Focus system frame start
self.focus.beginFrame();
// Reset animation request (set by widgets during draw)
self.needs_animation_frame = false;
self.frame += 1;
}
@ -363,6 +370,19 @@ pub const Context = struct {
return @intCast(@max(time_until_toggle, 16)); // Minimum 16ms to avoid busy loop
}
/// Request another animation frame (for color transitions, etc.).
/// Widgets call this during draw when they have ongoing animations.
/// Main loop should check needsAnimationFrame() after draw and schedule redraw.
pub fn requestAnimationFrame(self: *Self) void {
self.needs_animation_frame = true;
}
/// Check if any widget requested an animation frame.
/// Call after draw to determine if immediate redraw is needed.
pub fn needsAnimationFrame(self: Self) bool {
return self.needs_animation_frame;
}
/// Determina si el cursor debe ser visible basado en tiempo de actividad.
/// Usa parpadeo mientras hay actividad reciente, sólido cuando está idle.
/// @param last_activity_time_ms: Última vez que hubo actividad (edición, input)

View file

@ -1031,9 +1031,9 @@ pub fn derivePanelPalette(base: Color, mode: ThemeMode) PanelColorScheme {
}
/// Derive palette for dark mode (dark backgrounds, light text)
/// Z-Design V2: "Atmósfera, no fogonazo"
/// - fondo_sin_focus: 7% base color (sutil identidad del panel)
/// - fondo_con_focus: 15% base color (brilla al ganar foco)
/// Z-Design V2 + Liquid UI: Mayor contraste para transiciones perceptibles
/// - fondo_sin_focus: 4% base color (más oscuro, punto de partida bajo)
/// - fondo_con_focus: 20% base color (brilla al ganar foco, destino alto)
fn deriveDarkPalette(base: Color) PanelColorScheme {
// Reference colors for dark mode
const black = Color.soft_black; // RGB(17, 17, 20) - not pure black
@ -1042,11 +1042,11 @@ fn deriveDarkPalette(base: Color) PanelColorScheme {
const dark_border = Color.rgb(60, 60, 65);
return .{
// Backgrounds: Z-Design V2 - panel siempre tiene su "identidad"
// Focus: 15% base, 85% black (brilla más)
.fondo_con_focus = base.blendTowards(black, 85),
// Sin focus: 7% base, 93% black (sutil pero presente)
.fondo_sin_focus = base.blendTowards(black, 93),
// Backgrounds: Liquid UI V2 - mayor recorrido para transición perceptible
// Focus: 20% base, 80% black (destino luminoso)
.fondo_con_focus = base.blendTowards(black, 80),
// Sin focus: 4% base, 96% black (punto de partida oscuro)
.fondo_sin_focus = base.blendTowards(black, 96),
// Text: high contrast
.datos = white,
@ -1068,9 +1068,9 @@ fn deriveDarkPalette(base: Color) PanelColorScheme {
}
/// Derive palette for light mode (light backgrounds, dark text)
/// Z-Design V2: Sincronizado con dark mode
/// - fondo_sin_focus: 1% base (casi blanco, sutil identidad)
/// - fondo_con_focus: 3% base (brilla al ganar foco)
/// Z-Design V2 + Liquid UI: Mayor contraste para transiciones perceptibles
/// - fondo_sin_focus: 1% base (casi blanco, punto de partida)
/// - fondo_con_focus: 6% base (brilla al ganar foco, destino)
fn deriveLightPalette(base: Color) PanelColorScheme {
// Reference colors for light mode
const white = Color.soft_white; // RGB(250, 250, 252) - slight cool tint
@ -1079,10 +1079,10 @@ fn deriveLightPalette(base: Color) PanelColorScheme {
const light_border = Color.rgb(220, 220, 225);
return .{
// Backgrounds: Z-Design V2 - panel siempre tiene su "identidad"
// Focus: 3% base, 97% white
.fondo_con_focus = base.blendTowards(white, 97),
// Sin focus: 1% base, 99% white (sutil pero presente)
// Backgrounds: Liquid UI V2 - mayor recorrido para transición perceptible
// Focus: 6% base, 94% white (destino más tintado)
.fondo_con_focus = base.blendTowards(white, 94),
// Sin focus: 1% base, 99% white (punto de partida neutro)
.fondo_sin_focus = base.blendTowards(white, 99),
// Text: high contrast
@ -1151,11 +1151,11 @@ test "derivePanelPalette dark mode" {
try std.testing.expectEqual(Color.laravel_red.g, palette.seleccion_fondo_con_focus.g);
try std.testing.expectEqual(Color.laravel_red.b, palette.seleccion_fondo_con_focus.b);
// Background should be very dark (close to black, slight red tint)
// 95% blend towards black = mostly black with 5% of base color
try std.testing.expect(palette.fondo_con_focus.r < 35); // Red tint visible
try std.testing.expect(palette.fondo_con_focus.g < 25);
try std.testing.expect(palette.fondo_con_focus.b < 25);
// Background should be dark with visible tint (Liquid UI V2: 20% base color)
// 80% blend towards black = dark with noticeable tint for transitions
try std.testing.expect(palette.fondo_con_focus.r < 65); // Red tint visible
try std.testing.expect(palette.fondo_con_focus.g < 35);
try std.testing.expect(palette.fondo_con_focus.b < 35);
// The red component should be slightly higher than G/B (tint visible)
try std.testing.expect(palette.fondo_con_focus.r >= palette.fondo_con_focus.g);
@ -1167,10 +1167,11 @@ test "derivePanelPalette light mode" {
// Selection should be the full base color
try std.testing.expectEqual(Color.laravel_blue.r, palette.seleccion_fondo_con_focus.r);
// Background should be very light (close to white, slight blue tint)
try std.testing.expect(palette.fondo_con_focus.r > 240);
try std.testing.expect(palette.fondo_con_focus.g > 245);
try std.testing.expect(palette.fondo_con_focus.b > 250);
// Background should be light with visible tint (Liquid UI V2: 6% base color)
// 94% blend towards white = light with noticeable tint for transitions
try std.testing.expect(palette.fondo_con_focus.r > 230);
try std.testing.expect(palette.fondo_con_focus.g > 235);
try std.testing.expect(palette.fondo_con_focus.b > 240);
}
test "contrastTextColor" {

View file

@ -704,8 +704,8 @@ pub const ColorTransition = struct {
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,
/// Transition duration in milliseconds (500ms = Liquid UI feel)
duration_ms: f32 = 500.0,
const Self = @This();