Sass 色彩空间&广色域 颜色

发布于 2024 年 9 月 11 日,作者:Miriam Suzanne

广色域颜色即将登陆 Sass!

我应该澄清一下。像 oklch(…)color(display-p3 …) 这样的广色域 CSS 颜色格式自 2023 年 5 月以来已在所有主流浏览器中可用。但在此之前,Sass 中就允许使用这些新的颜色格式。这是我最喜欢的 Sass 功能之一:大多数新的 CSS都能正常工作,无需任何“官方”支持或更新。当 Sass 遇到未知的 CSS 时,它会将该代码传递给浏览器。并非所有内容都需要 预处理。

通常,这正是我们所需要的。当级联层和容器查询在浏览器中推出时,Sass 无需做任何额外操作。但是新的 CSS 颜色格式略有不同。由于颜色是 Sass 中的一级数据类型,因此我们并不总是希望按原样传递它们。我们通常希望在颜色传递到浏览器之前对其进行操作和管理。 浏览器。

已经了解所有关于色彩空间的知识了吗?跳到新的 Sass 功能

颜色格式的权衡颜色格式的权衡 永久链接

CSS 从历史上看一直局限于 sRGB 颜色格式,它们共享两个主要 特征

  • 它们使用底层的 RGB 颜色模型 来表示&通过控制redgreenblue 光的相对量来以数学方式操作颜色。
  • 它们只能表示 sRGB 色域 中的颜色——自 20 世纪 90 年代中期以来,彩色显示器上可以显示的默认颜色范围。

清晰的色域边界清晰的色域边界 永久链接

之前在 CSS 中可用的格式——命名颜色(例如 red)、hex 颜色(例如 #f00)和颜色函数(例如 rgb()/rgba()hsl()/hsla(),以及最近的 hwb())——都是描述 sRGB 颜色的方法。命名颜色是特殊的,但其他格式使用“坐标”系统,就像色域的颜色被投影到 3d 空间中一样

sRGB gamut rendered in sRGB space forms a rainbow colored cube sRGB gamut rendered in hsl space forms a rainbow-edged cylinder with black at the bottom and white at the top sRGB gamut rendered in hwb space forms a rainbow-core top surface with a black-to-gray bottom and gray-to-white outside edge
使用 Isaac Muse 的 ColorAide 生成的图像。

看看那些漂亮的几何形状!RGB 为我们提供了彩虹立方体,而 HSLHWB(及其“极性”hue 通道)将相同颜色排列成圆柱体。清晰的边界使我们能够轻松地(以数学方式)知道哪些颜色在色域内超出色域。在 rgb() 中,我们使用 0-255 的值。该范围内的任何内容都将在立方体内部,但如果某个通道低于 0 或高于 255,我们就不再位于 sRGB 色域内。在 hsl()hwb() 中,hue 坐标可以继续围绕圆圈旋转而不会达到逃逸速度,但 saturationlightnesswhitenessblackness 通道则从 0-10%-100% 清晰地变化。同样,任何超出该范围的值都超出了颜色 空间。

匹配人类感知匹配人类感知 永久链接

但这种简单性也带来了局限性。最明显的是显示器不断改进。如今,许多显示器可以显示超出 sRGB 的颜色,尤其是在扩展可用亮绿色的范围方面。如果我们只是用新的可用颜色扩展我们的形状,那么我们就不再处理干净的 几何图形了!

display-p3 gamut rendered in sRGB space adds unequal red and green horns to the sRGB cube display-p3 gamut rendered in hsl space creates a boot-like bulge of green near the base of the hsl cylinder

sRGB 格式的清晰边缘和干净的数学运算之所以成为可能,是因为我们确切地知道可以显示哪些颜色,并且我们将这些颜色完美地排列到一个盒子中。但是人类的颜色感知并非如此明确,并且与市场上任何显示器的色域都不完全一致。当我们尝试根据人类感知而不是简单的数学运算来均匀间隔所有相同的颜色时,我们会得到一个具有弯曲边缘的完全不同的形状。这是 oklch 空间中的 display-p3 色域

display-p3 gamut rendered in oklch space forms a skewed cube with a conic black base

当我们比较 hsloklch 中相同“亮度”的颜色时,实际差异尤其明显。人类认为黄色色调比蓝色色调更亮。通过将其缩放以适合相同范围,hsl 为我们提供了比 蓝色亮得多的黄色

on the left a blue and much brighter yellow, on the right our yellow is much darker to match the blue tone

新的 CSS 格式为我们提供了选择新的 CSS 格式为我们提供了选择 永久链接

展望未来,我们可以用两种方式处理广色域 颜色

  • 将越来越大的色域重新拟合到简单的坐标中,拉伸颜色以保留干净的几何 边界。
  • 保持其感知上均匀的间距,而不考虑特定的 色域。

一方面,清晰的边界使我们能够轻松地保持在可用颜色的范围内。如果没有这些边界,很容易意外地请求实际上不存在的颜色。另一方面,我们希望这些颜色被其他人感知到——我们需要使事物看起来一致,并具有足够的对比度才能 可读。

CSS 颜色模块级别 4 定义了许多新的 CSS 颜色格式。其中一些保持了对特定颜色空间的几何访问。与更熟悉的 rgb()hsl() 函数一样,较新的 hwb() 函数仍然使用 huewhitenessblackness 通道描述 sRGB 色域中的颜色。这是一个有趣的格式,而且我之前写过关于它的文章 

其余的色域边界空间可以使用 color(<space> <3-channels> / <alpha>) 函数获得。使用该语法,我们可以在 sRGBsrbg-lineardisplay-p3(现代显示器常见)、a98-rgbprophoto-rgbrec2020 中定义颜色。这些都将指定的色域映射到 0-10%-100% 的(立方)坐标范围内。非常 干净。

在相同的 color() 函数中,我们还可以访问“设备无关”(且无色域)的 xyz 颜色空间——通常用作在不同颜色模型之间转换的国际基线。我在这里不会介绍白点,但我们可以明确指定 xyz-d65(默认值),或者使用 xyz-d50 代替。

xyz 向外扩展,我们得到了一些新的理论上无界的颜色格式——优先考虑感知上均匀的分布而不是干净的几何形状。这些可以通过其自身的函数获得,包括 lab()lightnessab)和 lch()lightnesschromahue)以及每个函数的较新的“ok”版本——oklab()oklch()。如果您想了解这些格式的完整历史,Eric Portis 写了一篇很棒的 解释

TL;DR 优先级最高的新格式TL;DR 优先级最高的新格式 永久链接

对于色彩专家来说,拥有所有这些灵活性非常棒。对于我们其他人来说,有一些突出的 格式

  • color(display-p3 …) 提供了对更广泛的颜色色域的访问,这些颜色在许多现代显示器上可用,同时保持了一组清晰的色域 边界。
  • oklch(…) 是最直观和感知上最均匀的空间,是 hsl(…) 的较新替代方案——chromasaturation 非常相似。但是这里几乎没有防护栏,很容易最终超出任何屏幕可能显示的色域。坐标系仍在描述一个圆柱体,但人类感知和显示技术的边缘并没有整齐地映射到该 空间中。
  • 对于过渡和渐变,如果我们想直接在色调之间进行转换(而不是围绕色轮),oklab(…) 是一个不错的线性选项。通常,两个色域内颜色的过渡或渐变将保持在色域内——但当我们处理饱和度或 亮度的极端情况时,我们不能总是依赖这一点。

CSS 中的 Sass 颜色函数CSS 中的 Sass 颜色函数 永久链接

Sass 现在接受所有新的 CSS 格式,并将它们视为我们可以操作、混合、转换和检查的一级颜色。这些函数在 全局范围内可用

  • lab()oklab()lch()oklch()
  • 使用 sRGBsrgb-lineardisplay-p3a98-rgbprophoto-rgbrec2020xyzxyz-d65xyz-d50 颜色空间的 color()
  • hwb()(Sass 之前有一个 color.hwb() 函数,现在已弃用,取而代之的是全局 函数)

Sass 颜色函数使用与 CSS 函数相同的语法,这意味着给定的颜色可以用各种不同的空间表示。例如,这些都是相同的 颜色

游乐场

SCSS 语法

@debug MediumVioletRed;
@debug #C71585;
@debug hsl(322.2 80.91% 43.14%);
@debug oklch(55.34% 0.2217 349.7);
@debug color(display-p3 0.716 0.1763 0.5105);
游乐场

Sass 语法

@debug MediumVioletRed
@debug #C71585
@debug hsl(322.2 80.91% 43.14%)
@debug oklch(55.34% 0.2217 349.7)
@debug color(display-p3 0.716 0.1763 0.5105)

Sass 颜色保留其空间Sass 颜色保留其空间 永久链接

从历史上看,CSS 和 Sass 都将不同的颜色空间视为可互换的。当所有颜色格式使用相同的底层模型描述相同的颜色色域时,您可以使用 hsl() 语法提供颜色,解析器可以急切地将其转换为 rgb(),而不会冒任何数据丢失的风险。对于现代颜色 空间,情况已不再如此。

通常,在给定空间中定义的任何颜色都将保留在该空间中,并以该空间的形式发出。空间由使用的函数定义,可以是传递给 color() 的命名空间之一,也可以是函数名称(例如,使用 lab() 函数定义的颜色为 lab)。

但是,rgbhslhwb 空间被认为是“传统空间”,为了向后兼容,通常会对其进行特殊处理。传统颜色仍以最向后兼容的可用格式发出。这与 CSS 本身的向后兼容 行为相匹配。使用十六进制表示法或 CSS 颜色名称定义的颜色也被视为传统 rgb 颜色空间的一部分。

Sass 提供了各种用于检查和处理这些颜色 空间的工具

  • 我们可以使用 color.space($color) 检查颜色的空间
  • 我们可以使用 color.is-legacy($color) 查询颜色是否在传统空间中
  • 我们可以使用 color.to-space($color, $space) 将颜色从一个空间转换为另一个空间

所有这些函数都由内置的 Sass 颜色模块 提供

游乐场

SCSS 语法

@use 'sass:color';
$brand: MediumVioletRed;

// results: rgb, true
@debug color.space($brand);
@debug color.is-legacy($brand);

// result: oklch(55.34% 0.2217 349.7)
@debug color.to-space($brand, 'oklch');

// results: oklch, false
@debug color.space($brand);
@debug color.is-legacy($brand);
游乐场

Sass 语法

@use 'sass:color'
$brand: MediumVioletRed

// results: rgb, true
@debug color.space($brand)
@debug color.is-legacy($brand)

// result: oklch(55.34% 0.2217 349.7)
@debug color.to-space($brand, 'oklch')

// results: oklch, false
@debug color.space($brand)
@debug color.is-legacy($brand)

一旦我们在颜色空间之间进行转换,我们就不会再认为这些颜色是相等的。但我们可以使用color.same()函数询问它们是否会渲染为“相同”的颜色。

游乐场

SCSS 语法

@use 'sass:color';
$orange-rgb: #ff5f00;
$orange-oklch: oklch(68.72% 20.966858279% 41.4189852913deg);

// result: false
@debug $orange-rgb == $orange-oklch;

// result: true
@debug color.same($orange-rgb, $orange-oklch);
游乐场

Sass 语法

@use 'sass:color'
$orange-rgb: #ff5f00
$orange-oklch: oklch(68.72% 20.966858279% 41.4189852913deg)

// result: false
@debug $orange-rgb == $orange-oklch

// result: true
@debug color.same($orange-rgb, $orange-oklch)

我们可以使用color.channel()检查颜色的各个通道。默认情况下,它只支持颜色自身空间中可用的通道,但我们可以传递$space参数以在转换为给定空间后返回通道值的数值。

游乐场

SCSS 语法

@use 'sass:color';
$brand: hsl(0 100% 25.1%);

// result: 25.1%
@debug color.channel($brand, "lightness");

// result: 37.67%
@debug color.channel($brand, "lightness", $space: oklch);
游乐场

Sass 语法

@use 'sass:color'
$brand: hsl(0 100% 25.1%)

// result: 25.1%
@debug color.channel($brand, "lightness")

// result: 37.67%
@debug color.channel($brand, "lightness", $space: oklch)

CSS还引入了“无能量”和“缺失”颜色通道的概念。例如,饱和度为0%hsl颜色将始终为灰度。在这种情况下,我们可以认为色相通道是无能量的。更改其值不会对生成的颜色产生任何影响。Sass允许我们使用color.is-powerless()函数询问某个通道是否为无能量的。

游乐场

SCSS 语法

@use 'sass:color';
$gray: hsl(0 0% 60%);

// result: true, because saturation is 0
@debug color.is-powerless($gray, "hue");

// result: false
@debug color.is-powerless($gray, "lightness");
游乐场

Sass 语法

@use 'sass:color'
$gray: hsl(0 0% 60%)

// result: true, because saturation is 0
@debug color.is-powerless($gray, "hue")

// result: false
@debug color.is-powerless($gray, "lightness")

更进一步,CSS还允许我们显式地将某个通道标记为“缺失”或未知。如果我们将像gray这样的颜色转换为oklch这样的颜色空间,则可能会自动发生这种情况——我们没有任何关于色相的信息。我们还可以使用none关键字显式地创建具有缺失通道的颜色,并使用color.is-missing()函数检查颜色通道是否缺失。

游乐场

SCSS 语法

@use 'sass:color';
$brand: hsl(none 100% 25.1%);

// result: false
@debug color.is-missing($brand, "lightness");

// result: true
@debug color.is-missing($brand, "hue");
游乐场

Sass 语法

@use 'sass:color'
$brand: hsl(none 100% 25.1%)

// result: false
@debug color.is-missing($brand, "lightness")

// result: true
@debug color.is-missing($brand, "hue")

CSS一样,Sass会在有意义的地方保留缺失的通道,但在需要通道值时将其视为0

操作 Sass 颜色操作 Sass 颜色 链接

现有的color.scale()color.adjust()color.change()函数将继续按预期工作。默认情况下,所有颜色操作都在颜色提供的空间中执行。但我们现在也可以为转换指定显式颜色空间。

游乐场

SCSS 语法

@use 'sass:color';
$brand: hsl(0 100% 25.1%);

// result: hsl(0 100% 43.8%)
@debug color.scale($brand, $lightness: 25%);

// result: hsl(5.76 56% 45.4%)
@debug color.scale($brand, $lightness: 25%, $space: oklch);
游乐场

Sass 语法

@use 'sass:color'
$brand: hsl(0 100% 25.1%)

// result: hsl(0 100% 43.8%)
@debug color.scale($brand, $lightness: 25%)

// result: hsl(5.76 56% 45.4%)
@debug color.scale($brand, $lightness: 25%, $space: oklch)

请注意,即使调整是在不同的空间中执行的,返回的颜色仍然以原始颜色空间返回。这样,我们就可以开始在oklch等更高级的颜色空间中使用它们,而无需依赖浏览器支持这些格式。

现有的color.mix()函数在两个颜色都位于传统颜色空间中时也将保持现有行为。传统混合始终在rgb空间中完成。我们可以使用新的$method参数选择其他混合技术,该参数旨在匹配CSS用于描述插值方法的规范——用于CSS渐变、滤镜、动画和过渡,以及新的CSS color-mix()函数。

对于传统颜色,该方法是可选的。但对于非传统颜色,则需要一个方法。在大多数情况下,该方法可以简单地是一个颜色空间名称。但当我们使用具有“极坐标色相”通道的颜色空间(例如hslhwblchoklch)时,我们还可以指定要在色轮上移动的方向较短色相较长色相递增色相递减色相

游乐场

SCSS 语法

@use 'sass:color';

// result: #660099
@debug color.mix(red, blue, 40%);

// result: rgb(176.2950613593, -28.8924497904, 159.1757183525)
@debug color.mix(red, blue, 40%, $method: lab);

// result: rgb(-129.55249236, 149.0291922672, 77.9649510422)
@debug color.mix(red, blue, 40%, $method: oklch longer hue);
游乐场

Sass 语法

@use 'sass:color'

// result: #660099
@debug color.mix(red, blue, 40%)

// result: rgb(176.2950613593, -28.8924497904, 159.1757183525)
@debug color.mix(red, blue, 40%, $method: lab)

// result: rgb(-129.55249236, 149.0291922672, 77.9649510422)
@debug color.mix(red, blue, 40%, $method: oklch longer hue)

在这种情况下,混合中的第一个颜色被视为“原点”颜色。与上面的其他函数一样,我们可以使用不同的空间进行混合,但结果始终以该原点颜色空间返回。

处理色域边界处理色域边界 链接

那么当您超出给定显示器的色域时会发生什么?浏览器仍在争论细节,但每个人都同意我们必须显示某些东西

  • 目前,浏览器将每种颜色转换为红色绿色蓝色通道以进行显示。如果这些通道中的任何一个对于给定的屏幕来说过高或过低,则它们会在允许的最高或最低值处裁剪。这通常被称为“通道裁剪”。它使数学变得简单,但如果某些通道比其他通道裁剪得更多,则可能会对色相亮度产生奇怪的影响。
  • CSS规范指出,保持亮度应该具有最高优先级,并提供了一种算法来降低彩度,直到颜色在色域内。这对于保持文本的可读性非常有用,但它会给浏览器带来更多工作,并且当颜色突然失去其活力时可能会让人感到意外。
  • 在一种折衷方案上取得了一些进展,即降低彩度以使颜色位于rec2020色域内,然后从那里进行裁剪。

由于浏览器的行为仍然不可靠,并且某些颜色空间(咳咳 oklch)很容易让我们超出任何可用的色域,因此在Sass中进行一些色域管理可能会有所帮助。

我们可以使用color.is-in-gamut()测试特定颜色是否在给定色域内。与我们其他的颜色函数一样,这将默认为颜色定义所在的空间,但我们可以提供$space参数以针对不同的色域进行测试。

游乐场

SCSS 语法

@use 'sass:color';
$extra-pink: color(display-p3 0.951 0.457 0.7569);

// result: true, for display-p3 gamut
@debug color.is-in-gamut($extra-pink);

// result: false, for srgb gamut
@debug color.is-in-gamut($extra-pink, $space: srgb);
游乐场

Sass 语法

@use 'sass:color'
$extra-pink: color(display-p3 0.951 0.457 0.7569)

// result: true, for display-p3 gamut
@debug color.is-in-gamut($extra-pink)

// result: false, for srgb gamut
@debug color.is-in-gamut($extra-pink, $space: srgb)

我们还可以使用color.to-gamut()函数显式地移动颜色,使其位于特定色域内。由于表上有几个选项,并且不清楚长期默认的CSS将使用什么,因此此函数目前需要一个显式的$method参数。当前选项为clip(如浏览器当前应用的)或local-minde(如当前指定的)。

游乐场

SCSS 语法

@use 'sass:color';
$extra-pink: oklch(90% 90% 0deg);

// result: oklch(68.3601568298% 0.290089749 338.3604392249deg)
@debug color.to-gamut($extra-pink, srgb, clip);

// result: oklch(88.7173946522% 0.0667320674 355.3282956627deg)
@debug color.to-gamut($extra-pink, srgb, local-minde);
游乐场

Sass 语法

@use 'sass:color'
$extra-pink: oklch(90% 90% 0deg)

// result: oklch(68.3601568298% 0.290089749 338.3604392249deg)
@debug color.to-gamut($extra-pink, srgb, clip)

// result: oklch(88.7173946522% 0.0667320674 355.3282956627deg)
@debug color.to-gamut($extra-pink, srgb, local-minde)

所有传统和RGB样式空间都表示有界颜色的色域。由于将颜色映射到色域是一个有损过程,因此通常应将其留给浏览器处理或谨慎操作。因此,即使在转换为有界颜色空间时,Sass也会保留色域外的通道值。

传统浏览器需要srgb色域中的颜色。但是,大多数现代显示器都支持更宽的display-p3色域。

已弃用的函数已弃用的函数 链接

许多现有函数仅对传统颜色有意义,因此正在被弃用,取而代之的是像color.channel()color.adjust()这样的颜色空间友好函数。最终,这些函数将完全从Sass中删除,但所有相同的功能仍然可以在更新的函数中使用。

  • color.red()
  • color.green()
  • color.blue()
  • color.hue()
  • color.saturation()
  • color.lightness()
  • color.whiteness()
  • color.blackness()
  • adjust-hue()
  • saturate()
  • desaturate()
  • transparentize()/fade-out()
  • opacify()/fade-in()
  • lighten()/darken()

我们添加了一个迁移工具,以便自动将这些传统函数转换为颜色空间友好的函数。

$ sass-migrator color --migrate-deps <path/to/style.scss>