Skip to content

Theme: calculate maxChromaAtLH with a closed formula - #82643

Open
jsnajdr wants to merge 1 commit into
trunkfrom
update/max-chroma-cardano
Open

jsnajdr wants to merge 1 commit into
trunkfrom
update/max-chroma-cardano

Conversation

@jsnajdr

@jsnajdr jsnajdr commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Alternative to #82591 that calculates maxInGamutChromaAtLH by a much faster and exactly precise algorithm.

The idea is:

  1. the RGB gamut is a cube where the xyz coordinates are between 0 and 1. Points inside the cube are in-gamut, points that are outside the cube, i.e., at least one of coordinates is <0 or >1, are out of gamut. Wide-gamut spaces like P3 or Rec2020 are also RGB cubes, just bigger.
  2. When expressed in the OKLCH/OKLab space, it's still a cube, just distorted. This distorsion is a combination of two linear matrix transforms, and one cubic transform (x^3). The curves are therefore cubic:
Screenshot 2026-09-09 at 10 09 58
  1. "max chroma at given LH" is a point on the surface of the cube. The LH coordinates define a "ray" attached to the axis and perpendicular to it (at a certain "height" and "angle") and it intersect the cube at a certain point.
  2. The intersection of the ray and the cube is a solution of a cubic equation. We can solve that exactly using Cardano formula. This is what this PR does. The solution is precise, a result of a formula that takes L and H as parameters and spits out the chroma. No bisection, no approximation, no grid, no caching.

My initial measurements show that this is slightly slower than the original with cache and toGamutCSS. 467ms vs 505ms. But I'm not 100% sure yet. The advantage is that the results are exactly precise.

The current implementation is an unchanged AI output (Cursor with Opus 4.6). There are characteristically cryptic comments, and I don't yet have a full human understaning of it. I'd like to clean it up before merging.

@jsnajdr
jsnajdr requested a review from ciampo September 9, 2026 08:20
@jsnajdr jsnajdr self-assigned this Sep 9, 2026
@jsnajdr
jsnajdr requested a review from a team as a code owner September 9, 2026 08:20
@jsnajdr jsnajdr added [Type] Performance Related to performance efforts [Package] Theme /packages/theme labels Sep 9, 2026
@github-actions github-actions Bot added the [Package] Base styles /packages/base-styles label Sep 9, 2026
Co-authored-by: simison <simison@git.wordpress.org>
Co-authored-by: aduth <aduth@git.wordpress.org>
Co-authored-by: fcoveram <fcoveram@git.wordpress.org>
@jsnajdr
jsnajdr force-pushed the update/max-chroma-cardano branch from 7a1ac0c to f31873d Compare September 9, 2026 08:51
@jsnajdr
jsnajdr requested a review from fabiankaegy as a code owner September 9, 2026 08:51
@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: simison <simison@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

📦 Bundle size

Size Change: +960 B (+0.01%)

Total Size: 8.01 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 479 kB -5 B (0%)
build/scripts/block-library/index.min.js 367 kB -4 B (0%)
build/scripts/components/index.min.js 267 kB -1 B (0%)
build/scripts/customize-widgets/index.min.js 59 kB +19 B (+0.03%)
build/scripts/edit-site/index.min.js 379 kB -4 B (0%)
build/scripts/edit-widgets/index.min.js 66.8 kB +20 B (+0.03%)
build/scripts/editor/index.min.js 584 kB +53 B (+0.01%)
build/scripts/format-library/index.min.js 40.3 kB -1 B (0%)
build/scripts/media-utils/index.min.js 202 kB -2 B (0%)
build/scripts/theme/index.min.js 27.7 kB +717 B (+2.66%)
build/styles/components/style-rtl.css 16.1 kB +20 B (+0.12%)
build/styles/components/style-rtl.min.css 13.7 kB +21 B (+0.15%)
build/styles/components/style.css 16.2 kB +20 B (+0.12%)
build/styles/components/style.min.css 13.7 kB +21 B (+0.15%)
build/styles/theme/design-tokens-rtl.css 3.15 kB +20 B (+0.64%)
build/styles/theme/design-tokens-rtl.min.css 1.52 kB +23 B (+1.54%)
build/styles/theme/design-tokens.css 3.15 kB +20 B (+0.64%)
build/styles/theme/design-tokens.min.css 1.52 kB +23 B (+1.54%)

f31873d Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric aaa52c8 trunk % Change
timeToFirstByte 47.5 ms +2.11% -17.16% 47.75 ms +6.49% -5.24% -0.52%
largestContentfulPaint 72 ms +0% -5.56% 76 ms +7.89% -5.26% -5.26%
lcpMinusTtfb 24.25 ms +16.29% -3.09% 26.55 ms +15.63% -6.21% -8.66%
wpBeforeTemplate 23.11 ms +10.95% -17.05% 24.48 ms +8.86% -9.64% -5.6%
wpTemplate 17.81 ms +3.76% -4.49% 18.67 ms +4.07% -1.66% -4.61%
wpTotal 43.11 ms +2.27% -15.19% 43.23 ms +6.96% -4.4% -0.28%
wpMemoryUsage 7.52 MB +0% -0% 7.48 MB +0% -0% 0.46%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric aaa52c8 trunk % Change
timeToFirstByte 30.25 ms +16.86% -3.14% 32.6 ms +14.72% -6.13% -7.21%
largestContentfulPaint 64 ms +9.38% -0% 70 ms +5.71% -8.57% -8.57%
lcpMinusTtfb 34.45 ms +6.39% -5.22% 36 ms +4.58% -4.44% -4.31%
wpBeforeTemplate 17.06 ms +4.28% -3.22% 18.12 ms +11.26% -7.45% -5.85%
wpTemplate 10.4 ms +7.31% -4.13% 11.4 ms +5.35% -8.16% -8.77%
wpTotal 27.67 ms +16.34% -3.87% 29.86 ms +13.73% -7% -7.33%
wpMemoryUsage 5.51 MB +0% -0% 5.47 MB +0% -0% 0.64%
wpDbQueries 14 +0% -0% 14 +0% -0% 0%

media-processing

Metric aaa52c8 trunk % Change
mediaProcessingJpeg 287.15 ms +2.86% -1.18% 293.87 ms +9.11% -0.75% -2.29%
mediaProcessingAvif 4123.61 ms +1.49% -0.7% 4281.43 ms +1.89% -0.75% -3.69%
mediaProcessingJpegToAvif 2789.37 ms +1.54% -0.07% 2889.39 ms +1.03% -0.21% -3.46%

media-upload

Metric aaa52c8 trunk % Change
jpegUploadProcessing 1405.37 ms +0.76% -1.23% 1411.99 ms +1% -0.54% -0.47%
pngUploadProcessing 179.28 ms +29.1% -7.59% 196.5 ms +27.99% -5.11% -8.76%
largeJpegUploadProcessing 1420.14 ms +0.38% -0.89% 1407.24 ms +0.45% -0.22% 0.92%
multipleImageUploadProcessing 1473.3 ms +1.35% -0.96% 1474.57 ms +2.52% -1.03% -0.09%

post-editor

Metric aaa52c8 trunk % Change
serverResponse 432.68 ms +7.95% -0.87% 407.22 ms +11.82% -4.86% 6.25%
firstPaint 231.09 ms +33.03% -15.73% 196.27 ms +21.69% -6.15% 17.74%
domContentLoaded 1197.32 ms +15.21% -0.98% 1182.17 ms +1.16% -0.52% 1.28%
loaded 1198.42 ms +15.21% -0.97% 1183.33 ms +1.15% -0.53% 1.28%
firstContentfulPaint 409.46 ms +5.18% -4.8% 388.12 ms +4.07% -3.99% 5.5%
firstBlock 3236.99 ms +12.16% -3.5% 3136.34 ms +0.66% -0.23% 3.21%
type 17.28 ms +5.27% -3.7% 14.2 ms +7.75% -3.17% 21.69%
typeWithoutInspector 17.98 ms +5.12% -5.62% 14.53 ms +7.64% -4.06% 23.74%
typeWithTopToolbar 22.86 ms +3.06% -7.48% 18.45 ms +7.43% -3.9% 23.9%
typeContainer 10.92 ms +14.47% -13.19% 8.63 ms +7.42% -4.29% 26.54%
focus 86.36 ms +20.91% -8.52% 72.72 ms +9.72% -9.47% 18.76%
firstFocus 202.22 ms +0% -0% 154.63 ms +0% -0% 30.78%
selectAll 427.31 ms +9.85% -0.04% 428.74 ms +0.3% -1.29% -0.33%
listViewOpen 61.69 ms +8.43% -6.76% 63.57 ms +19.22% -9.19% -2.96%
inserterOpen 21.06 ms +12.35% -1.38% 22.17 ms +8.07% -0.5% -5.01%
inserterHover 3.98 ms +8.29% -7.29% 3.95 ms +15.19% -14.68% 0.76%
inserterSearch 8.4 ms +9.76% -8.93% 8.6 ms +20.7% -8.49% -2.33%
loadPatterns 582.96 ms +4.02% -3.15% 614.66 ms +8.07% -3.25% -5.16%
wpTotal 422.38 ms +7.91% -0.92% 397.22 ms +12.04% -5.06% 6.33%
wpMemoryUsage 13.05 MB +0% -0% 13.02 MB +0% -0% 0.28%
wpDbQueries 54 +0% -1.85% 54 +0% -0% 0%

site-editor

Metric aaa52c8 trunk % Change
serverResponse 518.26 ms +4.2% -1.99% 520.89 ms +3.89% -3.88% -0.5%
firstPaint 317.97 ms +45.63% -17.82% 314.46 ms +35.69% -12.94% 1.12%
domContentLoaded 1477.15 ms +0.49% -1.07% 1491.11 ms +0.76% -1.33% -0.94%
loaded 1478.5 ms +0.5% -1.08% 1492.48 ms +0.76% -1.33% -0.94%
firstContentfulPaint 463.3 ms +1.09% -2.18% 467.33 ms +4.72% -2.83% -0.86%
firstBlock 4706.37 ms +0.59% -0.81% 4707.77 ms +0.71% -0.58% -0.03%
type 17.33 ms +4.73% -1.9% 17.71 ms +2.65% -2.32% -2.15%
navigate 121.25 ms +1.96% -1.02% 123.99 ms +7.94% -8.54% -2.21%
loadPatterns 1479.13 ms +13.8% -3.8% 1623.05 ms +6.16% -9.37% -8.87%
loadPages 1185.89 ms +1.08% -4.44% 1173.26 ms +1.09% -2.69% 1.08%
wpTotal 508.27 ms +4.35% -1.99% 511.04 ms +3.89% -3.98% -0.54%
wpMemoryUsage 12.06 MB +0% -0% 12.03 MB +0% -0% 0.3%
wpDbQueries 43 +2.33% -0% 43 +2.33% -0% 0%

f31873d Run

@ciampo ciampo left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting approach! Also loved the illustration in the PR description :)

This PR currently lowers an existing text contrast case below its required level. With primary #608010 on background #4f386e, foreground.interactive.neutral-weak now has 4.488:1 contrast against both neutral interactive backgrounds instead of at least 4.5:1.

And more in general:

  • is it expected that so many computed colors change?
  • are you seeing any notable performance improvements? I could not, at least on my machine

Comment on lines +373 to +382
return (
crossings.find( ( chroma ) =>
channels.every( ( [ c3, c2, c1, c0 ] ) => {
const value =
( ( c3 * chroma + c2 ) * chroma + c1 ) * chroma + c0;
return (
value >= -SURFACE_EPSILON && value <= 1 + SURFACE_EPSILON
);
} )
) ?? 0

@ciampo ciampo Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check can turn a valid color gray.

For example, maxInGamutChromaAtLH( 0.9, 301, sRGB ) returns zero chroma, which means gray, even though the valid limit is about 0.05757. We should somehow make the check tolerate rounding differences, and make sure to test for a more granular set of hues between closer to pure black / whilte?

@jsnajdr

jsnajdr commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

is it expected that so many computed colors change?

The toGamutCSS method is not precise and it consistently overestimates the max chroma. It searches for a point on the ray that is less than 0.02 (deltaEOK < 0.02) far away from the surface of the cube. This can lead to big errors at places where the chroma chart has sharp edges. Look at this point for L=0.44 and H=261:

Screenshot 2026-09-10 at 9 32 44

At this point the max chroma is 0.19, but it's very close to the H=264 point where the max chroma is 0.30. If you're searching for a point whose distance from the surface is deltaEOK < 0.02, you can easily find one where the chroma is much higher than the precise 0.19.

AI originally pointed this out, and recommended to retune the alpha constants in taperChromaOptions, because these are obviously tuned by hand, but now the underlying base number has changed.

This check can turn a valid color gray.

Good find, this is a bug in the solver, where the SURFACE_EPSILON tolerance is too small. The H=301 point is where the cubic to solve has a peculiar shape:

Screenshot 2026-09-10 at 10 08 26

Note that the curve is very flat near zero, and there is a wide interval where the value is "almost zero". That reduces the numerical precison of calculating the root.

are you seeing any notable performance improvements?

I plan to check it once more after the recent refactors in #82591 and #82545. I expect a performance improvement, but most importantly, the answer to "what is the max chroma at LH" should be much more precise now, without any approximation.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Base styles /packages/base-styles [Package] Block library /packages/block-library [Package] Theme /packages/theme [Type] Performance Related to performance efforts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants