From 31037c11d5638f828e7dda670f59339d4801018d Mon Sep 17 00:00:00 2001
From: facelessuser
Date: Thu, 29 Jan 2026 22:30:52 -0700
Subject: [PATCH 1/3] [css-color-4] Correction to ray trace to avoid throwing
off our intersection calculation
The suggested fix via https://github.com/w3c/csswg-drafts/issues/10579#issuecomment-3815362986
was not the ideal fix and artificially limited `tnear`. This is a
numerical instability issue and should be addressed as such by using an
epsilon compare to prevent rays that are too small.
Related #10579
---
css-color-4/Overview.bs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/css-color-4/Overview.bs b/css-color-4/Overview.bs
index b8757467da33..fa4f016b5a20 100644
--- a/css-color-4/Overview.bs
+++ b/css-color-4/Overview.bs
@@ -6135,7 +6135,7 @@ Sample Pseudocode for the Ray Trace Gamut Mapping
let |d| be |b| - |a|
let |direction|[i] be |d|
- if (d != 0):
+ if abs(|d|) < 1E-15
- let |inv_d| be 1 / |d|
- let |t1| be (|bmin|[i] - |a|) * |inv_d|
@@ -6165,7 +6165,7 @@ Sample Pseudocode for the Ray Trace Gamut Mapping
- if (|tnear| > 10)
+ if |tnear| is infinite (or matches the initial very large value)
- return INTERSECTION NOT FOUND
From db955e104b2aa2869f172dfadbe0d9647a4e1686 Mon Sep 17 00:00:00 2001
From: facelessuser
Date: Sun, 1 Feb 2026 21:16:55 -0700
Subject: [PATCH 2/3] A little more buffer
---
css-color-4/Overview.bs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/css-color-4/Overview.bs b/css-color-4/Overview.bs
index fa4f016b5a20..513d42fe1a60 100644
--- a/css-color-4/Overview.bs
+++ b/css-color-4/Overview.bs
@@ -6135,7 +6135,7 @@ Sample Pseudocode for the Ray Trace Gamut Mapping
let |d| be |b| - |a|
let |direction|[i] be |d|
- if abs(|d|) < 1E-15
+ if abs(|d|) < 1E-12
- let |inv_d| be 1 / |d|
- let |t1| be (|bmin|[i] - |a|) * |inv_d|
From b9863d39c02e67de9071ef03bce7e383d6e515e1 Mon Sep 17 00:00:00 2001
From: facelessuser
Date: Mon, 2 Feb 2026 07:55:40 -0700
Subject: [PATCH 3/3] Match the flow CSS
- Return unbounded colors
- Return black/white if SDR lightness is exceeded.
- Then if color is out of gamut continue chroma reduction
---
css-color-4/Overview.bs | 74 ++++++++++++++++++++++-------------------
1 file changed, 39 insertions(+), 35 deletions(-)
diff --git a/css-color-4/Overview.bs b/css-color-4/Overview.bs
index 513d42fe1a60..34261b7de22d 100644
--- a/css-color-4/Overview.bs
+++ b/css-color-4/Overview.bs
@@ -6052,9 +6052,7 @@ Sample Pseudocode for the Ray Trace Gamut Mapping
- - if |origin| is in gamut for |destination|,
- convert |origin| to |destination| and return it as the gamut mapped color
-
+ - if |destination| has no gamut limits (XYZ-D65, XYZ-D50, Lab, LCH, Oklab, OkLCh) convert |origin| to |destination| and return it as the gamut mapped color
- let |origin_OkLCh| be |origin| converted from |origin color space|
to the OkLCh color space
@@ -6071,45 +6069,51 @@ Sample Pseudocode for the Ray Trace Gamut Mapping
let |origin_rgb| be |origin_OkLCh|
converted to the linear-light form of |destination|
- let |low| be 1E-6 1
- let |high| be 1.0 - |low| 2
- let |last| be |origin_rgb|
- for (i=0; i<4; i++)
+
+ if |origin_rgb| is not in gamut
- - if (i > 0)
+
+
- let |low| be 0.0 + 1E-6 1
+ - let |high| be 1.0 - 1E-6 2
+ - let |last| be |origin_rgb|
+ - for (i=0; i<4; i++)
- - let |current_OkLCh| be |origin_rgb| converted to OkLCh
- - let the lightness of |current_OkLCh| be |l_origin|
- - let the hue of |current_OkLCh| be |h_origin|
- 3
- - let |origin_rgb| be |current_OkLCh|
- converted to the linear-light form of |destination|
+
- if (i > 0)
+
+ - let |current_OkLCh| be |origin_rgb| converted to OkLCh
+ - let the lightness of |current_OkLCh| be |l_origin|
+ - let the hue of |current_OkLCh| be |h_origin|
+ 3
+ - let |origin_rgb| be |current_OkLCh|
+ converted to the linear-light form of |destination|
+
+
+
+ - Cast a ray from |anchor| to |origin_rgb|
+ and let |intersection| be
+ the intersection of this ray
+ with the gamut boundary
+ - if an intersection was not found,
+ let |origin_rgb| be |last| and exit the loop
+ 5
+
+ - if (i >0) AND (each component of |origin_rgb| is between |low| and |high|) then
+ let |anchor| be |origin_rgb|
+ 4
+
+ - let |origin_rgb| be |intersection|
+ - let |last| be |intersection|
- - Cast a ray from |anchor| to |origin_rgb|
- and let |intersection| be
- the intersection of this ray
- with the gamut boundary
-
- - if an intersection was not found,
- let |origin_rgb| be |last| and exit the loop
- 5
-
- - if (i >0) AND (each component of |origin_rgb| is between |low| and |high|) then
- let |anchor| be |origin_rgb|
- 4
-
- - let |origin_rgb| be |intersection|
- - let |last| be |intersection|
-
- let |clipped| be |origin_rgb| clipped to gamut (components in range 0 to 1),
- thus trimming off any noise due to floating point inaccuracy
-
- return |clipped|, converted to |destination| as the gamut mapped color
-
+
+ let clip(|color|) be a function which converts |color| to |destination|,
+ clamps each component to the bounds of the reference range for that component
+ and returns the result
+ set |clipped| to clip(|current|)
+ return |clipped| as the gamut mapped color