feat(colors): Add perceptual luminance correction (Z-Design V3)
E1 task from PLAN_REFINAMIENTO_UI_2025-12-30.md: - Add Color.perceptualLuminance() using ITU-R BT.709 weights - Add global perceptual_correction_enabled flag (default: true) - deriveDarkPalette/deriveLightPalette now boost low-luminance colors - Colors like red/magenta now have comparable contrast to blue - Correction capped at 0.7 to avoid excessive boost Algorithm: if base_lum < 0.45, reduce blend % by ratio (more color visible) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ded8946702
commit
16fc528415
1 changed files with 111 additions and 14 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue