Compare commits
No commits in common. "b5a4205c291821cb1fb3dad25e23f5dd685828a3" and "b2a408149361cceb7283232e1fb99777da366aac" have entirely different histories.
b5a4205c29
...
b2a4081493
1 changed files with 9 additions and 332 deletions
|
|
@ -134,46 +134,6 @@ pub const Color = struct {
|
|||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HSL-based transformations (more perceptually uniform)
|
||||
// =========================================================================
|
||||
|
||||
/// Convert this color to HSL representation
|
||||
/// Note: Uses forward declaration, actual function defined after Hsl struct
|
||||
pub fn toHsl(self: Self) Hsl {
|
||||
return rgbToHsl(self.r, self.g, self.b);
|
||||
}
|
||||
|
||||
/// Lighten using HSL (more perceptually uniform than RGB lighten)
|
||||
pub fn lightenHsl(self: Self, percent: f32) Self {
|
||||
return self.toHsl().lighten(percent).toRgb().withAlpha(self.a);
|
||||
}
|
||||
|
||||
/// Darken using HSL (more perceptually uniform than RGB darken)
|
||||
pub fn darkenHsl(self: Self, percent: f32) Self {
|
||||
return self.toHsl().darken(percent).toRgb().withAlpha(self.a);
|
||||
}
|
||||
|
||||
/// Increase saturation (make more vivid)
|
||||
pub fn saturate(self: Self, percent: f32) Self {
|
||||
return self.toHsl().saturate(percent).toRgb().withAlpha(self.a);
|
||||
}
|
||||
|
||||
/// Decrease saturation (make more gray)
|
||||
pub fn desaturate(self: Self, percent: f32) Self {
|
||||
return self.toHsl().desaturate(percent).toRgb().withAlpha(self.a);
|
||||
}
|
||||
|
||||
/// Rotate hue by degrees (color wheel shift)
|
||||
pub fn rotateHue(self: Self, degrees: f32) Self {
|
||||
return self.toHsl().rotate(degrees).toRgb().withAlpha(self.a);
|
||||
}
|
||||
|
||||
/// Get complementary color (opposite on color wheel)
|
||||
pub fn complementary(self: Self) Self {
|
||||
return self.rotateHue(180);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Predefined colors
|
||||
// =========================================================================
|
||||
|
|
@ -219,148 +179,6 @@ pub const Color = struct {
|
|||
pub const soft_white = Color.rgb(250, 250, 252); // Not pure white
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HSL Color Space
|
||||
// =============================================================================
|
||||
|
||||
/// HSL color representation
|
||||
/// H: Hue (0-360 degrees)
|
||||
/// S: Saturation (0.0-1.0)
|
||||
/// L: Lightness (0.0-1.0)
|
||||
pub const Hsl = struct {
|
||||
h: f32, // 0-360
|
||||
s: f32, // 0-1
|
||||
l: f32, // 0-1
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Convert HSL to RGB Color
|
||||
pub fn toRgb(self: Self) Color {
|
||||
return hslToRgb(self.h, self.s, self.l);
|
||||
}
|
||||
|
||||
/// Create HSL with clamped values
|
||||
pub fn init(h: f32, s: f32, l: f32) Self {
|
||||
return .{
|
||||
.h = @mod(h, 360.0),
|
||||
.s = std.math.clamp(s, 0.0, 1.0),
|
||||
.l = std.math.clamp(l, 0.0, 1.0),
|
||||
};
|
||||
}
|
||||
|
||||
/// Increase lightness by percentage (0-100)
|
||||
pub fn lighten(self: Self, percent: f32) Self {
|
||||
const delta = percent / 100.0;
|
||||
return Self.init(self.h, self.s, self.l + (1.0 - self.l) * delta);
|
||||
}
|
||||
|
||||
/// Decrease lightness by percentage (0-100)
|
||||
pub fn darken(self: Self, percent: f32) Self {
|
||||
const delta = percent / 100.0;
|
||||
return Self.init(self.h, self.s, self.l * (1.0 - delta));
|
||||
}
|
||||
|
||||
/// Increase saturation by percentage (0-100)
|
||||
pub fn saturate(self: Self, percent: f32) Self {
|
||||
const delta = percent / 100.0;
|
||||
return Self.init(self.h, self.s + (1.0 - self.s) * delta, self.l);
|
||||
}
|
||||
|
||||
/// Decrease saturation by percentage (0-100)
|
||||
pub fn desaturate(self: Self, percent: f32) Self {
|
||||
const delta = percent / 100.0;
|
||||
return Self.init(self.h, self.s * (1.0 - delta), self.l);
|
||||
}
|
||||
|
||||
/// Rotate hue by degrees
|
||||
pub fn rotate(self: Self, degrees: f32) Self {
|
||||
return Self.init(self.h + degrees, self.s, self.l);
|
||||
}
|
||||
};
|
||||
|
||||
/// Convert RGB (0-255) to HSL
|
||||
pub fn rgbToHsl(r: u8, g: u8, b: u8) Hsl {
|
||||
// Normalize to 0-1 range
|
||||
const rf: f32 = @as(f32, @floatFromInt(r)) / 255.0;
|
||||
const gf: f32 = @as(f32, @floatFromInt(g)) / 255.0;
|
||||
const bf: f32 = @as(f32, @floatFromInt(b)) / 255.0;
|
||||
|
||||
const max_val = @max(@max(rf, gf), bf);
|
||||
const min_val = @min(@min(rf, gf), bf);
|
||||
const delta = max_val - min_val;
|
||||
|
||||
// Lightness
|
||||
const l = (max_val + min_val) / 2.0;
|
||||
|
||||
// Achromatic (gray)
|
||||
if (delta == 0) {
|
||||
return .{ .h = 0, .s = 0, .l = l };
|
||||
}
|
||||
|
||||
// Saturation
|
||||
const s = if (l > 0.5)
|
||||
delta / (2.0 - max_val - min_val)
|
||||
else
|
||||
delta / (max_val + min_val);
|
||||
|
||||
// Hue
|
||||
var h: f32 = 0;
|
||||
if (max_val == rf) {
|
||||
h = (gf - bf) / delta;
|
||||
if (gf < bf) h += 6.0;
|
||||
} else if (max_val == gf) {
|
||||
h = (bf - rf) / delta + 2.0;
|
||||
} else {
|
||||
h = (rf - gf) / delta + 4.0;
|
||||
}
|
||||
h *= 60.0;
|
||||
|
||||
return .{ .h = h, .s = s, .l = l };
|
||||
}
|
||||
|
||||
/// Convert HSL to RGB Color
|
||||
pub fn hslToRgb(h: f32, s: f32, l: f32) Color {
|
||||
// Achromatic (gray)
|
||||
if (s == 0) {
|
||||
const gray: u8 = @intFromFloat(l * 255.0);
|
||||
return Color.rgb(gray, gray, gray);
|
||||
}
|
||||
|
||||
const q = if (l < 0.5)
|
||||
l * (1.0 + s)
|
||||
else
|
||||
l + s - l * s;
|
||||
const p = 2.0 * l - q;
|
||||
|
||||
const hue_norm = h / 360.0;
|
||||
|
||||
const r = hueToRgb(p, q, hue_norm + 1.0 / 3.0);
|
||||
const g = hueToRgb(p, q, hue_norm);
|
||||
const b = hueToRgb(p, q, hue_norm - 1.0 / 3.0);
|
||||
|
||||
return Color.rgb(
|
||||
@intFromFloat(r * 255.0),
|
||||
@intFromFloat(g * 255.0),
|
||||
@intFromFloat(b * 255.0),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper for HSL to RGB conversion
|
||||
fn hueToRgb(p: f32, q: f32, t_in: f32) f32 {
|
||||
var t = t_in;
|
||||
if (t < 0) t += 1.0;
|
||||
if (t > 1) t -= 1.0;
|
||||
|
||||
if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t;
|
||||
if (t < 1.0 / 2.0) return q;
|
||||
if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
|
||||
return p;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Style
|
||||
// =============================================================================
|
||||
|
||||
/// Visual style for widgets
|
||||
pub const Style = struct {
|
||||
foreground: Color = Color.foreground,
|
||||
|
|
@ -846,117 +664,6 @@ test "ThemeManager setTheme" {
|
|||
try std.testing.expect(std.mem.eql(u8, tm.current.name, "high_contrast_dark"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HSL Tests
|
||||
// =============================================================================
|
||||
|
||||
test "rgbToHsl pure red" {
|
||||
const hsl = rgbToHsl(255, 0, 0);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.0), hsl.h, 0.1);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 1.0), hsl.s, 0.01);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.5), hsl.l, 0.01);
|
||||
}
|
||||
|
||||
test "rgbToHsl pure green" {
|
||||
const hsl = rgbToHsl(0, 255, 0);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 120.0), hsl.h, 0.1);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 1.0), hsl.s, 0.01);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.5), hsl.l, 0.01);
|
||||
}
|
||||
|
||||
test "rgbToHsl pure blue" {
|
||||
const hsl = rgbToHsl(0, 0, 255);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 240.0), hsl.h, 0.1);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 1.0), hsl.s, 0.01);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.5), hsl.l, 0.01);
|
||||
}
|
||||
|
||||
test "rgbToHsl white" {
|
||||
const hsl = rgbToHsl(255, 255, 255);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.0), hsl.s, 0.01); // No saturation
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 1.0), hsl.l, 0.01); // Max lightness
|
||||
}
|
||||
|
||||
test "rgbToHsl black" {
|
||||
const hsl = rgbToHsl(0, 0, 0);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.0), hsl.s, 0.01); // No saturation
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.0), hsl.l, 0.01); // Min lightness
|
||||
}
|
||||
|
||||
test "rgbToHsl gray" {
|
||||
const hsl = rgbToHsl(128, 128, 128);
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.0), hsl.s, 0.01); // No saturation
|
||||
try std.testing.expectApproxEqAbs(@as(f32, 0.5), hsl.l, 0.02); // Mid lightness
|
||||
}
|
||||
|
||||
test "hslToRgb pure red" {
|
||||
const color = hslToRgb(0, 1.0, 0.5);
|
||||
try std.testing.expectEqual(@as(u8, 255), color.r);
|
||||
try std.testing.expectEqual(@as(u8, 0), color.g);
|
||||
try std.testing.expectEqual(@as(u8, 0), color.b);
|
||||
}
|
||||
|
||||
test "hslToRgb roundtrip" {
|
||||
// Test that RGB -> HSL -> RGB preserves color (within rounding tolerance)
|
||||
const original = Color.rgb(200, 100, 50);
|
||||
const hsl = original.toHsl();
|
||||
const recovered = hsl.toRgb();
|
||||
|
||||
// Allow 1-2 units of rounding error in each channel
|
||||
try std.testing.expect(@abs(@as(i16, original.r) - @as(i16, recovered.r)) <= 2);
|
||||
try std.testing.expect(@abs(@as(i16, original.g) - @as(i16, recovered.g)) <= 2);
|
||||
try std.testing.expect(@abs(@as(i16, original.b) - @as(i16, recovered.b)) <= 2);
|
||||
}
|
||||
|
||||
test "Color.saturate red" {
|
||||
// Test saturating a color that has hue
|
||||
const muted_red = Color.rgb(200, 100, 100); // A muted red
|
||||
const saturated = muted_red.saturate(30);
|
||||
// Saturating should increase the difference between R and G/B
|
||||
const orig_diff = @as(i16, muted_red.r) - @as(i16, muted_red.g);
|
||||
const new_diff = @as(i16, saturated.r) - @as(i16, saturated.g);
|
||||
try std.testing.expect(new_diff >= orig_diff);
|
||||
}
|
||||
|
||||
test "Color.desaturate" {
|
||||
const red = Color.rgb(255, 0, 0);
|
||||
const desaturated = red.desaturate(100); // Full desaturation = gray
|
||||
// Should become a gray (equal R, G, B)
|
||||
try std.testing.expectEqual(desaturated.r, desaturated.g);
|
||||
try std.testing.expectEqual(desaturated.g, desaturated.b);
|
||||
}
|
||||
|
||||
test "Color.rotateHue" {
|
||||
const red = Color.rgb(255, 0, 0); // Hue = 0
|
||||
const green = red.rotateHue(120); // Hue = 120 = green
|
||||
// Should be mostly green
|
||||
try std.testing.expect(green.g > green.r);
|
||||
try std.testing.expect(green.g > green.b);
|
||||
}
|
||||
|
||||
test "Color.complementary" {
|
||||
const red = Color.rgb(255, 0, 0);
|
||||
const cyan = red.complementary(); // 180 degrees = cyan
|
||||
// Cyan has high G and B, low R
|
||||
try std.testing.expect(cyan.g > 200);
|
||||
try std.testing.expect(cyan.b > 200);
|
||||
try std.testing.expect(cyan.r < 50);
|
||||
}
|
||||
|
||||
test "Hsl.lighten" {
|
||||
var hsl = Hsl.init(0, 1.0, 0.5);
|
||||
hsl = hsl.lighten(50); // Lighten 50%
|
||||
try std.testing.expect(hsl.l > 0.5);
|
||||
try std.testing.expect(hsl.l < 1.0);
|
||||
}
|
||||
|
||||
test "Hsl.darken" {
|
||||
var hsl = Hsl.init(0, 1.0, 0.5);
|
||||
hsl = hsl.darken(50); // Darken 50%
|
||||
try std.testing.expect(hsl.l < 0.5);
|
||||
try std.testing.expect(hsl.l > 0.0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Z-DESIGN: Panel Color Derivation System
|
||||
// =============================================================================
|
||||
|
|
@ -1048,13 +755,12 @@ fn deriveDarkPalette(base: Color) PanelColorScheme {
|
|||
.etiquetas = white.darken(30), // ~70% brightness
|
||||
.placeholder = gray,
|
||||
|
||||
// Header: darkened using HSL (preserves hue better than RGB darken)
|
||||
.header = base.darkenHsl(50),
|
||||
// Header: darkened base color
|
||||
.header = base.darken(60),
|
||||
|
||||
// Selection: base color at full strength when focused
|
||||
.seleccion_fondo_con_focus = base,
|
||||
// Unfocused: desaturated and slightly lightened (HSL-based, more elegant)
|
||||
.seleccion_fondo_sin_focus = base.desaturate(60).lightenHsl(10),
|
||||
// Selection: where the base color SHINES
|
||||
.seleccion_fondo_con_focus = base, // Full color!
|
||||
.seleccion_fondo_sin_focus = base.blendTowards(gray, 50), // Muted
|
||||
|
||||
// Borders: accent on focus, subtle otherwise
|
||||
.borde_con_focus = base,
|
||||
|
|
@ -1080,13 +786,12 @@ fn deriveLightPalette(base: Color) PanelColorScheme {
|
|||
.etiquetas = black.lighten(40), // ~60% darkness
|
||||
.placeholder = gray,
|
||||
|
||||
// Header: lightened using HSL (preserves hue better)
|
||||
.header = base.lightenHsl(40),
|
||||
// Header: very light version of base
|
||||
.header = base.blendTowards(white, 85),
|
||||
|
||||
// Selection: base color at full strength when focused
|
||||
// Selection: base color (slightly lightened for readability)
|
||||
.seleccion_fondo_con_focus = base,
|
||||
// Unfocused: desaturated and lightened (HSL-based)
|
||||
.seleccion_fondo_sin_focus = base.desaturate(50).lightenHsl(30),
|
||||
.seleccion_fondo_sin_focus = base.blendTowards(white, 60),
|
||||
|
||||
// Borders: accent on focus, subtle otherwise
|
||||
.borde_con_focus = base,
|
||||
|
|
@ -1094,17 +799,6 @@ fn deriveLightPalette(base: Color) PanelColorScheme {
|
|||
};
|
||||
}
|
||||
|
||||
/// Get appropriate text color (black or white) based on background luminosity.
|
||||
/// Uses the HSL lightness value to determine contrast.
|
||||
pub fn contrastTextColor(background: Color) Color {
|
||||
const hsl = background.toHsl();
|
||||
// If background is light (L > 0.5), use dark text; otherwise use light text
|
||||
return if (hsl.l > 0.5)
|
||||
Color.rgb(20, 20, 25) // Dark text for light backgrounds
|
||||
else
|
||||
Color.rgb(245, 245, 245); // Light text for dark backgrounds
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Z-Design Tests
|
||||
// =============================================================================
|
||||
|
|
@ -1162,20 +856,3 @@ test "derivePanelPalette light mode" {
|
|||
try std.testing.expect(palette.fondo_con_focus.g > 245);
|
||||
try std.testing.expect(palette.fondo_con_focus.b > 250);
|
||||
}
|
||||
|
||||
test "contrastTextColor" {
|
||||
// Dark background should get light text
|
||||
const dark_bg = Color.rgb(30, 30, 30);
|
||||
const text_on_dark = contrastTextColor(dark_bg);
|
||||
try std.testing.expect(text_on_dark.r > 200); // Light text
|
||||
|
||||
// Light background should get dark text
|
||||
const light_bg = Color.rgb(240, 240, 240);
|
||||
const text_on_light = contrastTextColor(light_bg);
|
||||
try std.testing.expect(text_on_light.r < 50); // Dark text
|
||||
|
||||
// Mid-gray (128/255 = 0.502 > 0.5) gets dark text
|
||||
const mid_gray = Color.rgb(128, 128, 128);
|
||||
const text_on_mid = contrastTextColor(mid_gray);
|
||||
try std.testing.expect(text_on_mid.r < 50); // Dark text (L = 0.502 > 0.5)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue