From a0817cdbb3e9a8925d9399ff9104ee1c1de2289b Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 15 Dec 2025 03:07:32 +0100 Subject: [PATCH] Fix ColorExtensions math (#41717) All of this was doing sRGB -> OkLAB conversions without linearizing the sRGB first, so it was broken. I could have sworn I pointed this out in review but I guess that got lost. Also, add a gamut clipping step since we have out-of-gamut colors and I don't want random negative values causing weird nightmare bugs somewhere. Shouldn't change anything in regular rendering. --- .../Stylesheets/Colorspace/ColorExtensions.cs | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs b/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs index 1a191992b1..6af9308bbc 100644 --- a/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs +++ b/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs @@ -14,10 +14,10 @@ public static class ColorExtensions { DebugTools.Assert(lightness is >= 0.0f and <= 1.0f); - var oklab = Color.ToLab(c); + var oklab = c.LabFromSrgb(); oklab.X = lightness; - return Color.FromLab(oklab); + return oklab.LabToSrgb(); } /// @@ -25,10 +25,10 @@ public static class ColorExtensions /// public static Color NudgeLightness(this Color c, float lightnessShift) { - var oklab = Color.ToLab(c); + var oklab = c.LabFromSrgb(); oklab.X = Math.Clamp(oklab.X + lightnessShift, 0, 1); - return Color.FromLab(oklab); + return oklab.LabToSrgb(); } /// @@ -39,12 +39,12 @@ public static class ColorExtensions /// public static Color NudgeChroma(this Color c, float chromaShift) { - var oklab = Color.ToLab(c); + var oklab = c.LabFromSrgb(); var oklch = Color.ToLch(oklab); oklch.Y = Math.Clamp(oklch.Y + chromaShift, 0, 1); - return Color.FromLab(Color.FromLch(oklch)); + return Color.FromLch(oklch).LabToSrgb(); } /// @@ -54,10 +54,43 @@ public static class ColorExtensions { DebugTools.Assert(factor is >= 0.0f and <= 1.0f); - var okFrom = Color.ToLab(from); - var okTo = Color.ToLab(to); + var okFrom = from.LabFromSrgb(); + var okTo = to.LabFromSrgb(); var blended = Vector4.Lerp(okFrom, okTo, factor); - return Color.FromLab(blended); + return blended.LabToSrgb(); + } + + /// + /// Converts a nonlinear sRGB ("normal") color to OkLAB. + /// + public static Vector4 LabFromSrgb(this Color from) + { + return Color.ToLab(Color.FromSrgb(from)); + } + + /// + /// Converts OkLAB to a nonlinear sRGB ("normal") color. + /// + public static Color LabToSrgb(this Vector4 from) + { + return Color.ToSrgb(Color.FromLab(from).SimpleClipGamut()); + } + + /// + /// Clips the gamut of the color so that all color channels are in the range 0 -> 1. + /// + /// + /// This uses no clever perceptual techniques, it literally just clamps the individual channels. + /// + public static Color SimpleClipGamut(this Color from) + { + return new Color + { + R = Math.Clamp(from.R, 0, 1), + G = Math.Clamp(from.G, 0, 1), + B = Math.Clamp(from.B, 0, 1), + A = from.A, + }; } } -- 2.52.0