[Color 4] Require an explicit gamut mapping method (#3836)

This commit is contained in:
Natalie Weizenbaum 2024-04-19 16:18:25 -07:00 committed by GitHub
parent f4cf151046
commit 465f0db7d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 136 additions and 45 deletions

View File

@ -1,3 +1,9 @@
## Draft 1.8
* Change `SassColor.toGamut()` to take named parameters instead of positional.
* Add a mandatory `method` parameter to `SassColor.toGamut()`.
## Draft 1.7
* Don't throw errors for out-of-gamut lightness values.

View File

@ -1,4 +1,4 @@
# CSS Color Level 4, New Color Spaces JavaScript API: Draft 1.7
# CSS Color Level 4, New Color Spaces JavaScript API: Draft 1.8
*([Issue](https://github.com/sass/sass/issues/2831),
[Changelog](color-4-new-spaces-js.changes.md))*
@ -116,6 +116,8 @@ export type HueInterpolationMethod =
| 'increasing'
| 'longer'
| 'shorter';
export type GamutMapMethod = 'clip' | 'local-minde';
```
### New Color Functions
@ -171,13 +173,16 @@ isInGamut(space?: KnownColorSpace): boolean;
#### `toGamut`
Returns the result of [`color.to-gamut(internal, space)`].
Returns the result of [`color.to-gamut(internal, space, method)`].
```ts
toGamut(space?: KnownColorSpace): SassColor;
toGamut(options: {
space?: KnownColorSpace;
method: 'clip' | 'local-minde';
}): SassColor;
```
[`color.to-gamut(internal, space)`]: ./color-4-new-spaces.md#colorto-gamut-1
[`color.to-gamut(internal, space, method)`]: ./color-4-new-spaces.md#colorto-gamut-1
#### `channelsOrNull`

View File

@ -1,3 +1,10 @@
## Draft 1.15
* Add a mandatory `$method` parameter to `color.to-gamut()` for
forwards-compatibility with better gamut-mapping algorithms.
* Add `clip` as a gamut-mapping algorithm.
## Draft 1.14
* Update the definition of powerless for HWB to match [the latest CSS

View File

@ -1,4 +1,4 @@
# CSS Color Level 4, New Color Spaces: Draft 1.14
# CSS Color Level 4, New Color Spaces: Draft 1.15
*([Issue](https://github.com/sass/sass/issues/2831))*
@ -54,6 +54,8 @@ colors outside the sRGB gamut.
* [Converting a Color](#converting-a-color)
* [CSS-Converting a Color Space](#css-converting-a-color-space)
* [Gamut Mapping](#gamut-mapping-1)
* [`local-minde`](#local-minde)
* [`clip`](#clip)
* [Parsing Color Components](#parsing-color-components)
* [Percent-Converting a Number](#percent-converting-a-number)
* [Validating a Color Channel](#validating-a-color-channel)
@ -325,20 +327,21 @@ one or more of its channels out of bounds, like `rgb(300 0 0)`).
#### `color.to-gamut()`
This function returns a color that is in the given gamut, using the recommended
[CSS Gamut Mapping Algorithm][css-mapping] to 'map' out-of-gamut colors into
the desired gamut with as little perceptual change as possible. In many cases
this can be more reliable for generating fallback values, rather than the
'channel clipping' approach used by current browsers.
This function returns a color that is in the given gamut, using the given
mapping algorithm to convert out-of-gamut colors into the desired gamut with as
little perceptual change as possible. The current recommended algorithm is
`local-minde`, which is defined in the current candidate recommendation of CSS
Color 4, which can in many cases be more reliable for generating fallback values
than the "channel clipping" approach used by current browsers.
```scss
$green: oklch(0.8 2 150);
// oklch(0.91 0.14 164)
$rgb: color.to-gamut($green, "srgb");
$rgb: color.to-gamut($green, "srgb", $method: local-minde);
// oklch(0.91 0.16 163)
$p3: color.to-gamut($green, "display-p3");
$p3: color.to-gamut($green, "display-p3", $method: local-minde);
```
#### `color.is-powerless()`
@ -508,14 +511,27 @@ and mapping in Sass color functions:
#### Gamut Mapping
Browsers currently use channel-clipping rather than the proposed
[css gamut mapping algorithm][css-mapping] to handle colors that cannot be
shown correctly on a given display. We've decided to provide `color.to-gamut()`
as a way for authors to opt-into the proposed behavior, aware that browsers
may eventually choose to provide a different algorithm. If that happens, we
will consider adding an additional algorithm-selection argument. However, the
primary goal of this function is not to match CSS behavior, but to provide a
better mapping than the default channel-clipping.
Browsers currently use channel-clipping rather than the proposed [css gamut
mapping algorithm][css-mapping] to handle colors that cannot be shown correctly
on a given display. Moreover, there is still active discussion among the CSS
working group and browser vendors about what the best gamut mapping algorithm
is, with the currently-specified algorithm widely considered to be sub-optimal.
As such, we want to avoid baking it in as the default for Sass such that any
change would require a deprecation period.
At the same time, we expect gamut-mapping to be very useful for Sass authors
working with wide-gamut color spaces, enough so that we want it to be available
without needing to wait on the CSSWG to come to a consensus on the best
algorithm. To that end, we've decided to provide `color.to-gamut()` but
*require* a `$method` argument for forwards-compatibility with better gamut
mapping algorithms.
Initially, there will only be two available arguments for `$method`:
`local-minde`, the gamut mapping algorithm specified in Color 4 at the time this
spec is written, and `clip` which will just clip any channel values that are
out-of-range for a given color space. However, we expect to expand this to
additional algorithms in the future, and to eventually make it optional and have
its default match the behavior of CSS.
#### CSS Color 5
@ -1016,28 +1032,48 @@ The individual conversion algorithms are:
> available 'in-gamut' color. Gamut mapping is the process of finding an
> in-gamut color with the least objectionable change in visual appearance.
Gamut mapping in Sass follows the [CSS gamut mapping algorithm][css-mapping].
This procedure accepts a color `origin`, and a [known color space]
`destination`. It returns the result of a [CSS gamut map][css-map] procedure,
converted back into the original color space.
Gamut mapping color `origin`, a [known color space] `destination`, and an
unquoted string `method`. It returns a color in `origin`'s color space.
* Let `origin-space` be `origin`'s color space.
* If either `origin-space` or `destination` is not a [known color space], throw
an error.
* Let `mapped` be the result of [CSS gamut mapping][css-mapping] `origin`
color, with an origin color space of `origin-space`, and destination of
`destination`.
* Let `color` be the result of [converting] `origin` into `destination`.
* If `method` is not one of the methods defined below, throw an error.
* Let `mapped` be the result of running the method defined below whose name
matches `method`. If no such method matches, throw an error.
* Return the result of [converting] `mapped` into `origin-space`.
> This algorithm implements a relative colorimetric intent, and colors inside
> the destination gamut are unchanged. Since the process is lossy, authors
> should be encouraged to let the browser handle gamut mapping when possible.
#### `local-minde`
[css-mapping]: https://www.w3.org/TR/css-color-4/#css-gamut-mapping-algorithm
[css-map]: https://www.w3.org/TR/css-color-4/#css-gamut-map
The `local-minde` gamut mapping procedure in Sass follows the 13 February 2024
draft of CSS Color Module Level 4. It returns the result of [CSS gamut
mapping][css-mapping] `origin` with an origin color space of `origin-space` and
destination of `destination`.
[css-mapping]: https://www.w3.org/TR/2024/CRD-css-color-4-20240213/#css-gamut-mapping-algorithm
> This algorithm implements a relative colorimetric intent, and colors inside
> the destination gamut are unchanged.
#### `clip`
The `clip` gamut mapping procedure is not expected to produce good-looking
results, but it can be useful to match the current behavior of browsers.
* Let `new-color` be a copy of `color`.
* For each `channel` in `destination`:
* If `channel` is bounded, set `new-color`'s `channel` value to the result of
clamping the original value within `channel`'s minimum and maximum values.
* Return `new-color`.
### Parsing Color Components
@ -1626,11 +1662,14 @@ is-in-gamut($color, $space: null)
### `color.to-gamut()`
```
to-gamut($color, $space: null)
to-gamut($color, $space: null, $method: null)
```
* If `$color` is not a color, throw an error.
* If `$method` is not an unquoted string whose value is either `local-minde` or
`clip`, throw an error.
* If `$space` is null:
* Let `origin-space` be the result of calling `color.space($color)`.
@ -1641,8 +1680,8 @@ to-gamut($color, $space: null)
* Otherwise, let `target-space` be the result of [looking up a known color
space] named `$space`.
* Return the result of [gamut mapping] `$color` with a `target-space`
destination.
* Return the result of [gamut mapping] `$color` with destination `target-space`
and method `$method`.
[gamut mapping]: #gamut-mapping

View File

@ -58,6 +58,7 @@ export {
ColorSpaceLab,
ColorSpaceRgb,
ColorSpaceXyz,
GamutMapMethod,
HueInterpolationMethod,
KnownColorSpace,
ListSeparator,

View File

@ -79,6 +79,25 @@ export type HueInterpolationMethod =
| 'longer'
| 'shorter';
/**
* Methods by which colors in bounded spaces can be mapped to within their
* gamut.
*
* * `local-minde`: The algorithm specified in [the original Color Level 4
* candidate recommendation]. This maps in the Oklch color space, using the
* [deltaEOK] color difference formula and the [local-MINDE] improvement.
*
* * `clip`: Clamp each color channel that's outside the gamut to the minimum or
* maximum value for that channel. This algorithm will produce poor visual
* results, but it may be useful to match the behavior of other situations in
* which a color can be clipped.
*
* [the original Color Level 4 candidate recommendation]: https://www.w3.org/TR/2024/CRD-css-color-4-20240213/#css-gamut-mapping
* [deltaEOK]: https://www.w3.org/TR/2024/CRD-css-color-4-20240213/#color-difference-OK
* [local-MINDE]: https://www.w3.org/TR/2024/CRD-css-color-4-20240213/#GM-chroma-local-MINDE
*/
export type GamutMapMethod = 'clip' | 'local-minde';
/**
* Sass's [color type](https://sass-lang.com/documentation/values/colors).
*
@ -269,14 +288,13 @@ export class SassColor extends Value {
/**
* Returns a copy of this color, modified so it is in-gamut for the specified
* `space`or the current color space if `space` is not specifiedusing the
* recommended [CSS Gamut Mapping Algorithm][css-mapping] to map out-of-gamut
* colors into the desired gamut with as little perceptual change as possible.
*
* [css-mapping]:
* https://www.w3.org/TR/css-color-4/#css-gamut-mapping-algorithm
* `space`or the current color space if `space` is not specifiedusing
* `method` to map out-of-gamut colors into the desired gamut.
*/
toGamut(space?: KnownColorSpace): SassColor;
toGamut(options: {
space?: KnownColorSpace;
method: GamutMapMethod;
}): SassColor;
/**
* A list of this color's channel values (excluding alpha), with [missing

View File

@ -34,6 +34,7 @@ export {
ColorSpaceXyz,
ChannelNameXyz,
ChannelName,
GamutMapMethod,
KnownColorSpace,
PolarColorSpace,
RectangularColorSpace,

View File

@ -95,6 +95,7 @@ export {
ColorSpaceLab,
ColorSpaceRgb,
ColorSpaceXyz,
GamutMapMethod,
HueInterpolationMethod,
KnownColorSpace,
ListSeparator,

View File

@ -26,6 +26,7 @@ import {Value} from './index';
* [`PolarColorSpace`](#polarcolorspace)
* [`RectangularColorSpace`](#rectangularcolorspace)
* [`HueInterpolationMethod`](#hueinterpolationmethod)
* [`GamutMapMethod`](#gamutmapmethod)
* [`SassColor`](#sasscolor)
* [`internal`](#internal)
* [Constructor](#constructor)
@ -224,6 +225,14 @@ export type HueInterpolationMethod =
| 'shorter';
```
### `GamutMapMethod`
Methods by which colors in bounded spaces can be mapped to within their gamut.
```ts
export type GamutMapMethod = 'clip' | 'local-minde';
```
### `SassColor`
The JS API representation of a Sass color.
@ -552,13 +561,16 @@ isInGamut(space?: KnownColorSpace): boolean;
#### `toGamut`
Returns the result of [`color.to-gamut(internal, space)`].
Returns the result of [`color.to-gamut(internal, space, method)`].
```ts
toGamut(space?: KnownColorSpace): SassColor;
toGamut(options: {
space?: KnownColorSpace;
method: GamutMapMethod;
}): SassColor;
```
[`color.to-gamut(internal, space)`]: ../../../accepted/color-4-new-spaces.md#colorto-gamut-1
[`color.to-gamut(internal, space, method)`]: ../../../accepted/color-4-new-spaces.md#colorto-gamut-1
#### `channelsOrNull`

View File

@ -37,6 +37,7 @@ export {
ColorSpaceXyz,
ChannelNameXyz,
ChannelName,
GamutMapMethod,
KnownColorSpace,
PolarColorSpace,
RectangularColorSpace,