diff --git a/src/core/style.zig b/src/core/style.zig index d049f99..f3f9f39 100644 --- a/src/core/style.zig +++ b/src/core/style.zig @@ -37,6 +37,25 @@ pub fn isFancy() bool { return global_render_mode == .fancy; } +// ============================================================================= +// Perceptual Color Correction +// ============================================================================= + +/// Enable perceptual correction for panel colors. +/// When enabled, colors with low perceived luminance (e.g., red, magenta) +/// get a subtle boost to match the visual contrast of brighter colors (e.g., blue). +var perceptual_correction_enabled: bool = true; + +/// Get whether perceptual correction is enabled +pub fn isPerceptualCorrectionEnabled() bool { + return perceptual_correction_enabled; +} + +/// Set perceptual correction mode +pub fn setPerceptualCorrection(enabled: bool) void { + perceptual_correction_enabled = enabled; +} + /// RGBA Color pub const Color = struct { r: u8, @@ -134,6 +153,21 @@ pub const Color = struct { }; } + // ========================================================================= + // Perceptual luminance (ITU-R BT.709) + // ========================================================================= + + /// Calculate perceived luminance using ITU-R BT.709 weights. + /// Returns 0.0 (black) to 1.0 (white). + /// Human eyes perceive green as brightest, red medium, blue darkest. + /// Weights: R=0.2126, G=0.7152, B=0.0722 + pub fn perceptualLuminance(self: Self) f32 { + const r_norm = @as(f32, @floatFromInt(self.r)) / 255.0; + const g_norm = @as(f32, @floatFromInt(self.g)) / 255.0; + const b_norm = @as(f32, @floatFromInt(self.b)) / 255.0; + return r_norm * 0.2126 + g_norm * 0.7152 + b_norm * 0.0722; + } + // ========================================================================= // HSL-based transformations (more perceptually uniform) // ========================================================================= @@ -1034,6 +1068,7 @@ pub fn derivePanelPalette(base: Color, mode: ThemeMode) PanelColorScheme { /// 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) +/// Z-Design V3: Perceptual correction boosts low-luminance colors (red/magenta) fn deriveDarkPalette(base: Color) PanelColorScheme { // Reference colors for dark mode const black = Color.soft_black; // RGB(17, 17, 20) - not pure black @@ -1041,12 +1076,28 @@ fn deriveDarkPalette(base: Color) PanelColorScheme { const gray = Color.rgb(128, 128, 128); const dark_border = Color.rgb(60, 60, 65); + // Perceptual correction: boost low-luminance colors to match visual contrast + // Reference: laravel_blue has luminance ~0.48, we use 0.45 as target + const base_lum = base.perceptualLuminance(); + const target_lum: f32 = 0.45; // Reference luminance (approximately blue) + + // Calculate correction factor: if luminance is lower than target, reduce blend % + // This makes more of the base color visible, compensating for lower brightness + const correction: f32 = if (perceptual_correction_enabled and base_lum < target_lum) + @max(0.7, base_lum / target_lum) // Cap at 0.7 to avoid excessive boost + else + 1.0; + + // Apply correction to blend percentages (lower % = more base color visible) + const focus_blend: u8 = @intFromFloat(80.0 * correction); + const unfocus_blend: u8 = @intFromFloat(96.0 * correction); + return .{ // 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), + // Focus: 20% base, 80% black (destino luminoso) - adjusted by correction + .fondo_con_focus = base.blendTowards(black, focus_blend), + // Sin focus: 4% base, 96% black (punto de partida oscuro) - adjusted + .fondo_sin_focus = base.blendTowards(black, unfocus_blend), // Text: high contrast .datos = white, @@ -1071,6 +1122,7 @@ fn deriveDarkPalette(base: Color) PanelColorScheme { /// 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) +/// Z-Design V3: Perceptual correction boosts low-luminance colors (red/magenta) fn deriveLightPalette(base: Color) PanelColorScheme { // Reference colors for light mode const white = Color.soft_white; // RGB(250, 250, 252) - slight cool tint @@ -1078,12 +1130,26 @@ fn deriveLightPalette(base: Color) PanelColorScheme { const gray = Color.rgb(128, 128, 128); const light_border = Color.rgb(220, 220, 225); + // Perceptual correction: boost low-luminance colors to match visual contrast + // In light mode, lower blend % means more base color visible + const base_lum = base.perceptualLuminance(); + const target_lum: f32 = 0.45; + + const correction: f32 = if (perceptual_correction_enabled and base_lum < target_lum) + @max(0.7, base_lum / target_lum) + else + 1.0; + + // Apply correction (lower = more color visible on light background) + const focus_blend: u8 = @intFromFloat(94.0 * correction); + const unfocus_blend: u8 = @intFromFloat(99.0 * correction); + return .{ // 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), + // Focus: 6% base, 94% white (destino más tintado) - adjusted by correction + .fondo_con_focus = base.blendTowards(white, focus_blend), + // Sin focus: 1% base, 99% white (punto de partida neutro) - adjusted + .fondo_sin_focus = base.blendTowards(white, unfocus_blend), // Text: high contrast .datos = black, @@ -1151,16 +1217,47 @@ 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 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); + // Background should be dark with visible tint + // Z-Design V3: With perceptual correction, red gets boosted (less blend towards black) + // so the red component can be higher than before (~80 instead of ~60) + try std.testing.expect(palette.fondo_con_focus.r < 100); // Still dark + try std.testing.expect(palette.fondo_con_focus.g < 45); + try std.testing.expect(palette.fondo_con_focus.b < 45); - // The red component should be slightly higher than G/B (tint visible) + // The red component should be higher than G/B (tint visible) try std.testing.expect(palette.fondo_con_focus.r >= palette.fondo_con_focus.g); } +test "perceptualLuminance" { + // Pure red: 0.2126 + const red = Color.rgb(255, 0, 0); + try std.testing.expect(red.perceptualLuminance() > 0.2); + try std.testing.expect(red.perceptualLuminance() < 0.22); + + // Pure green: 0.7152 + const green = Color.rgb(0, 255, 0); + try std.testing.expect(green.perceptualLuminance() > 0.71); + try std.testing.expect(green.perceptualLuminance() < 0.72); + + // Pure blue: 0.0722 + const blue = Color.rgb(0, 0, 255); + try std.testing.expect(blue.perceptualLuminance() > 0.07); + try std.testing.expect(blue.perceptualLuminance() < 0.08); + + // White: 1.0 + const white = Color.rgb(255, 255, 255); + try std.testing.expect(white.perceptualLuminance() > 0.99); + + // Black: 0.0 + const black = Color.rgb(0, 0, 0); + try std.testing.expect(black.perceptualLuminance() < 0.01); + + // Blue has higher perceived luminance than red (at same saturation) + const laravel_red_lum = Color.laravel_red.perceptualLuminance(); + const laravel_blue_lum = Color.laravel_blue.perceptualLuminance(); + try std.testing.expect(laravel_blue_lum > laravel_red_lum); +} + test "derivePanelPalette light mode" { const palette = derivePanelPalette(Color.laravel_blue, .light);