no-bug: Rewrite color filter algorithm

This commit is contained in:
Mr. M
2026-04-01 16:17:05 +02:00
parent a15c6ecab4
commit 3eb3474324
5 changed files with 135 additions and 135 deletions

View File

@@ -120,7 +120,7 @@ class nsZenBoostsManager {
brightness: 0.5,
saturation: 0.5,
contrast: 0.25,
contrast: 0.5,
fontFamily: "",

View File

@@ -8,7 +8,7 @@
content/browser/zen-styles/zen-advanced-color-options.css (../../zen/boosts/zen-advanced-color-options.css)
# Windows
* content/browser/zen-components/windows/zen-boost-editor.xhtml (../../zen/boosts/zen-boost-editor.xhtml)
* content/browser/zen-components/windows/zen-boost-editor.xhtml (../../zen/boosts/zen-boost-editor.inc.xhtml)
# Images
content/browser/zen-images/boost-indicator.svg (../../zen/images/boost-indicator.svg)

View File

@@ -84,63 +84,79 @@ nsZenAccentOklab nsZenBoostsBackend::mCachedAccent{0};
namespace {
struct Lab {
float L, a, b;
};
struct RGB {
float r, g, b;
};
/**
* @brief Clamps a value to the range [0, 255] using branchless operations.
* @param v The value to clamp.
* @return The clamped value in the range [0, 255].
* @brief Converts an sRGB color component to linear space.
* @param c The sRGB color component value (0.0 to 1.0).
* @return The linear color component value.
*/
static __inline int32_t clamp255(int32_t v) {
// llvm x86 is poor at ternary operator, so use branchless min/max.
v = v & ~(v >> 31);
return (v | ((255 - v) >> 31)) & 255;
static inline float srgbToLinear(float c) {
return c <= 0.04045f ? c * (1.0f / 12.92f)
: std::pow((c + 0.055f) * (1.0f / 1.055f), 2.4f);
}
/**
* @brief A fast approximation of the cube root function using bit manipulation
* and two Newton-Raphson iterations. This is used to optimize the Oklab color
* conversion in the color filtering process.
* @brief Converts a linear color component to sRGB space.
* @param c The linear color component value.
* @return The sRGB color component value (0.0 to 1.0).
*/
inline static float fast_cbrt(float x) {
// Bit-level initial approximation (works for positive floats only — fine
// here)
uint32_t bits;
memcpy(&bits, &x, 4);
bits = (bits / 3) + 0x2A512400u; // magic constant for cube root
float y;
memcpy(&y, &bits, 4);
// Two Newton-Raphson iterations: y = y - (y³ - x) / (3y²)
y = (2.0f / 3.0f) * y + (1.0f / 3.0f) * x / (y * y);
y = (2.0f / 3.0f) * y + (1.0f / 3.0f) * x / (y * y);
return y;
static inline float linearToSrgb(float c) {
c = std::max(0.0f, c);
return c <= 0.0031308f ? 12.92f * c
: 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f;
}
/*
* @brief Fast approximation of the cube root of a number.
* @param x The input value.
* @return The approximate cube root of the input value.
*/
static inline float fastCbrt(float x) {
if (x == 0.0f) return 0.0f;
float a = std::abs(x);
union {
float f;
uint32_t i;
} u = {a};
u.i = u.i / 3 + 0x2a504a2e;
float y = u.f;
y = (2.0f * y + a / (y * y)) * (1.0f / 3.0f);
y = (2.0f * y + a / (y * y)) * (1.0f / 3.0f);
return x < 0.0f ? -y : y;
}
/**
* @brief Converts an Oklab color back to the RGB color space.
* @param c The Oklab color to convert.
* @return The corresponding RGB color.
* @brief Precomputes the Oklab values for a given accent color. This allows us
* to efficiently apply the accent color as a filter to other colors without
* having to convert the accent color from sRGB to Oklab space on every filter
* operation.
* @param aAccentColor The accent color in nscolor format.
* @return A struct containing the precomputed Oklab values and contrast factor
* for the accent color.
*/
[[nodiscard]]
static inline auto oklab2rgb(Lab c) -> RGB {
float l_ = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b;
float m_ = c.L - 0.1055613458f * c.a - 0.0638541728f * c.b;
float s_ = c.L - 0.0894841775f * c.a - 1.2914855480f * c.b;
inline static auto zenPrecomputeAccent(nscolor aAccentColor) {
const float inv255 = 1.0f / 255.0f;
// Cubing is just 2 multiplies — no cbrtf needed on the way back
return {
4.0767416621f * (l_ * l_ * l_) - 3.3077115913f * (m_ * m_ * m_) +
0.2309699292f * (s_ * s_ * s_),
-1.2684380046f * (l_ * l_ * l_) + 2.6097574011f * (m_ * m_ * m_) -
0.3413193965f * (s_ * s_ * s_),
-0.0041960863f * (l_ * l_ * l_) - 0.7034186147f * (m_ * m_ * m_) +
1.7076147010f * (s_ * s_ * s_),
const float r = NS_GET_R(aAccentColor) * inv255;
const float g = NS_GET_G(aAccentColor) * inv255;
const float b = NS_GET_B(aAccentColor) * inv255;
const float lr = srgbToLinear(r);
const float lg = srgbToLinear(g);
const float lb = srgbToLinear(b);
const float l_ =
fastCbrt(0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb);
const float m_ =
fastCbrt(0.2119034982f * lr + 0.6806995451f * lg + 0.1073969566f * lb);
const float s_ =
fastCbrt(0.0883024619f * lr + 0.2817188376f * lg + 0.6299787005f * lb);
return nsZenAccentOklab{
.accentNS = aAccentColor,
.accL = 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_,
.accA = 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_,
.accB = 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_,
.contrastFactor = NS_GET_CONTRAST(aAccentColor) * inv255,
};
}
@@ -155,85 +171,71 @@ static inline auto oklab2rgb(Lab c) -> RGB {
* @return The filtered color with transformations applied.
*/
[[nodiscard]]
static inline Lab rgb2oklab_fast(RGB c) {
float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b;
float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b;
float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b;
float l_ = fast_cbrt(l), m_ = fast_cbrt(m), s_ = fast_cbrt(s);
return {
0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_,
1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_,
0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_,
};
}
static inline nscolor zenFilterColorChannel(nscolor aOriginalColor,
const nsZenAccentOklab& aAccent) {
const uint8_t oL = NS_GET_A(aOriginalColor);
if (oL == 0) {
return aOriginalColor;
}
inline static auto zenPrecomputeAccent(nscolor aAccentColor)
-> nsZenAccentOklab {
constexpr float kInv255 = 1.0f / 255.0f;
RGB rgb = {NS_GET_R(aAccentColor) * kInv255, NS_GET_G(aAccentColor) * kInv255,
NS_GET_B(aAccentColor) * kInv255};
auto lab = rgb2oklab_fast(rgb);
float contrast = NS_GET_CONTRAST(aAccentColor);
float vibranceBase = 1.0f - ((contrast - 128.0f) * (1.0f / 128.0f));
return {lab.L, lab.a, lab.b, vibranceBase, 0.25f + lab.L};
}
const float inv255 = 1.0f / 255.0f;
const float blendFactor = oL * inv255;
const float preserveFactor = 1.0f - aAccent.contrastFactor;
/**
* @brief Applies a color filter to transform an original color toward an accent
* color. Preserves the original color's perceived luminance while shifting
* hue/chroma toward the accent. Uses the alpha channel of the accent color to
* store contrast information.
* @param aOriginalColor The original color to filter.
* @param aAccentColor The accent color to filter toward (alpha channel contains
* contrast value).
* @return The filtered color with transformations applied.
*/
[[nodiscard]]
static nscolor zenFilterColorChannel(nscolor aOriginalColor,
const nsZenAccentOklab& aAccent) {
const uint8_t a1 = NS_GET_A(aOriginalColor);
if (a1 == 0) return aOriginalColor;
// sRGB -> linear
const float lr = srgbToLinear(NS_GET_R(aOriginalColor) * inv255);
const float lg = srgbToLinear(NS_GET_G(aOriginalColor) * inv255);
const float lb = srgbToLinear(NS_GET_B(aOriginalColor) * inv255);
constexpr float kInv255 = 1.0f / 255.0f;
constexpr float kTint = 0.6f;
constexpr float kInvTint = 1.0f - kTint;
// Linear RGB -> LMS -> cube root -> Oklab (fused)
const float l_ =
fastCbrt(0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb);
const float m_ =
fastCbrt(0.2119034982f * lr + 0.6806995451f * lg + 0.1073969566f * lb);
const float s_ =
fastCbrt(0.0883024619f * lr + 0.2817188376f * lg + 0.6299787005f * lb);
RGB orig = {
NS_GET_R(aOriginalColor) * kInv255,
NS_GET_G(aOriginalColor) * kInv255,
NS_GET_B(aOriginalColor) * kInv255,
};
const auto lab = rgb2oklab_fast(orig);
const float origL =
0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_;
const float origA =
1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_;
const float origB =
0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_;
const float aBlend = kInvTint * lab.a + kTint * aAccent.a;
const float bBlend = kInvTint * lab.b + kTint * aAccent.b;
// Blend chroma toward accent
const float fA = origA + (aAccent.accA - origA) * blendFactor;
const float fB = origB + (aAccent.accB - origB) * blendFactor;
// Avoid sqrt: compare squared chroma against squared threshold (0.4^2 = 0.16)
// vibranceFactor = chromaMixed/chromaBlend which simplifies to just
// vibranceFactor since the sqrt cancels in the normalize+scale round-trip
// (see below)
const float chromaSq = aBlend * aBlend + bBlend * bBlend;
const float saturation =
(chromaSq < 0.16f) ? chromaSq * (1.0f / 0.16f) : 1.0f;
const float vibranceFactor =
1.0f + aAccent.vibranceBase * (1.0f - saturation);
// Luminance: preserve spread at low contrast, flatten at high
const float deltaL = origL - aAccent.accL;
const float baseL = origL + (aAccent.accL - origL) * blendFactor;
const float fL = std::clamp(baseL + deltaL * preserveFactor, 0.0f, 1.0f);
// sqrt cancellation: chromaMixed/chromaBlend =
// (chromaBlend*vibranceFactor)/chromaBlend = vibranceFactor, so we multiply
// directly with no sqrt needed
const float aMixed = aBlend * vibranceFactor;
const float bMixed = bBlend * vibranceFactor;
// Oklab -> LMS
const float fl_ = fL + 0.3963377774f * fA + 0.2158037573f * fB;
const float fm_ = fL - 0.1055613458f * fA - 0.0638541728f * fB;
const float fs_ = fL - 0.0894841775f * fA - 1.2914855480f * fB;
float LMixed = 0.5f + (lab.L - 0.5f) * (1.0f + aAccent.vibranceBase * 0.5f);
LMixed *= aAccent.accentLOffset;
if (LMixed < 0.0f) LMixed = 0.0f;
if (LMixed > 1.0f) LMixed = 1.0f;
// Cube
const float fl = fl_ * fl_ * fl_;
const float fm = fm_ * fm_ * fm_;
const float fs = fs_ * fs_ * fs_;
const auto rgb = oklab2rgb({LMixed, aMixed, bMixed});
// LMS -> linear RGB
const float rF = 4.0767416621f * fl - 3.3077115913f * fm + 0.2309699292f * fs;
const float gF =
-1.2684380046f * fl + 2.6097574011f * fm - 0.3413193965f * fs;
const float bF =
-0.0041960863f * fl - 0.7034186147f * fm + 1.7076147010f * fs;
return NS_RGBA(clamp255((int32_t)(rgb.r * 255.0f + 0.5f)),
clamp255((int32_t)(rgb.g * 255.0f + 0.5f)),
clamp255((int32_t)(rgb.b * 255.0f + 0.5f)), a1);
// Linear -> sRGB -> uint8
return NS_RGBA(static_cast<uint8_t>(std::clamp(
linearToSrgb(rF) * 255.0f + 0.5f, 0.0f, 255.0f)),
static_cast<uint8_t>(std::clamp(
linearToSrgb(gF) * 255.0f + 0.5f, 0.0f, 255.0f)),
static_cast<uint8_t>(std::clamp(
linearToSrgb(bF) * 255.0f + 0.5f, 0.0f, 255.0f)),
oL);
}
/**
@@ -279,18 +281,18 @@ inline static void GetZenBoostsDataFromBrowsingContext(
if (!zenBoosts || (zenBoosts->mCurrentFrameIsAnonymousContent)) {
return;
}
auto browsingContext = zenBoosts->GetCurrentBrowsingContext();
if (aPresContext) {
if (auto document = aPresContext->Document()) {
if (auto browsingContext = document->GetBrowsingContext()) {
*aData = browsingContext->ZenBoostsData();
*aIsInverted = browsingContext->IsZenBoostsInverted();
}
browsingContext = document->GetBrowsingContext();
}
} else if (auto currentBrowsingContext =
zenBoosts->GetCurrentBrowsingContext()) {
*aData = currentBrowsingContext->ZenBoostsData();
*aIsInverted = currentBrowsingContext->IsZenBoostsInverted();
}
if (!browsingContext) {
return;
}
browsingContext = browsingContext->Top();
*aData = browsingContext->ZenBoostsData();
*aIsInverted = browsingContext->IsZenBoostsInverted();
}
} // namespace
@@ -360,8 +362,7 @@ auto nsZenBoostsBackend::ResolveStyleColor(mozilla::StyleAbsoluteColor aColor)
return aColor;
}
const auto resultColor = FilterColorFromPresContext(aColor.ToColor());
aColor = mozilla::StyleAbsoluteColor::FromColor(resultColor);
return aColor;
return mozilla::StyleAbsoluteColor::FromColor(resultColor);
}
} // namespace zen

View File

@@ -17,10 +17,9 @@ using ZenBoostData = nscolor; // For now, Zen boosts data is just a color.
namespace zen {
struct nsZenAccentOklab {
float L, a, b;
float vibranceBase; // 1.0f - ((contrast - 128) / 128)
float accentLOffset; // 0.25f + L, precomputed
nscolor accentNS; // Used to keep track of the original accent color
nscolor accentNS;
float accL, accA, accB;
float contrastFactor;
};
class nsZenBoostsBackend final {

View File

@@ -38,7 +38,7 @@
<!-- Loading in the window module -->
<script>
const { nsZenBoostEditor } = ChromeUtils.importESModule( "resource:///modules/zen/boosts/ZenBoostsEditor.mjs" );
window.addEventListener("load", () => {
window.addEventListener("load", () => {
window.boostEditor = new nsZenBoostEditor(document, window.domain, window, window.openerWindow);
});
</script>
@@ -73,7 +73,7 @@
</vbox>
<html:div class="visible-separator"></html:div>
<hbox flex="1" id="zen-boost-font-toolbar">
<html:select name="font" id="zen-boost-font-select" class="mod-button">
<html:select name="font" id="zen-boost-font-select" class="mod-button">
<!-- Additional font options will be injected here -->
</html:select>
<button data-l10n-id="zen-boost-text-case-toggle" id="zen-boost-text-case-toggle" class="subviewbutton mod-button"></button>
@@ -88,7 +88,7 @@
<button id="zen-boost-code" class="subviewbutton mod-button big-button">
<html:p data-l10n-id="zen-boost-code" id="zen-boost-code-text"></html:p>
</button>
<hbox flex="1" id="zen-boost-toolbar-wrapper">
<button data-l10n-id="zen-boost-save" id="zen-boost-save" class="subviewbutton mod-button med"></button>
<button data-l10n-id="zen-boost-load" id="zen-boost-load" class="subviewbutton mod-button med"></button>
@@ -101,7 +101,7 @@
<html:p data-l10n-id="zen-boost-back" id="zen-boost-back-text"></html:p>
</button>
</hbox>
<vbox flex="1" id="zen-boost-code-editor">
</vbox>