惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

O
OpenAI News
Latest news
Latest news
T
Threat Research - Cisco Blogs
Project Zero
Project Zero
V
Vulnerabilities – Threatpost
T
The Exploit Database - CXSecurity.com
Cloudbric
Cloudbric
T
Threatpost
N
News | PayPal Newsroom
I
Intezer
L
LINUX DO - 热门话题
The Hacker News
The Hacker News
H
Hacker News: Front Page
P
Proofpoint News Feed
S
Secure Thoughts
H
Help Net Security
S
Schneier on Security
TaoSecurity Blog
TaoSecurity Blog
S
Security Archives - TechRepublic
V
Visual Studio Blog
博客园 - 司徒正美
博客园 - Franky
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏
AI
AI
V
V2EX - 技术
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
WordPress大学
WordPress大学
AWS News Blog
AWS News Blog
罗磊的独立博客
C
Cyber Attacks, Cyber Crime and Cyber Security
Webroot Blog
Webroot Blog
Forbes - Security
Forbes - Security
Engineering at Meta
Engineering at Meta
MyScale Blog
MyScale Blog
N
News and Events Feed by Topic
大猫的无限游戏
大猫的无限游戏
L
Lohrmann on Cybersecurity
H
Heimdal Security Blog
S
SegmentFault 最新的问题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
小众软件
小众软件
Recent Commits to openclaw:main
Recent Commits to openclaw:main
B
Blog
T
Troy Hunt's Blog
Stack Overflow Blog
Stack Overflow Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com

Miriam Eric Suzanne

Butter bells, fresh from the kiln Butter bells, fresh from the kiln Butter bells, fresh from the kiln I had to look it up I Laser-cut pottery throwing gauge Tech continues to be political Aggregating my distributed self A web component for CodePen embeds? We don Eleventy buckets & cascade layers A slash-why proposal User styles on the web Custom element, two ways New year, same (terrible) Mia CSS @scope Reclaiming my time Cascade Layers Javascript automation on Mac Personal Histories Ancient Web Browsers Critical CSS? Not So Fast! CSS tie-dye gradient backgrounds Personal website redesign Request for Comments: Sass Color Spaces A long-term plan for logical properties? Container queries in browsers! I never let things be small A whole cascade of layers This content won No demo [website] reno 2 days of cordwainery Body margin 8px Miriam, for the archive Complex vs compound selectors The spam has arrived Am I on the IndieWeb yet? My theatrical delusions A Complete Guide to CSS Cascade Layers Container queries explainer & proposal Very Extremely Practical CSS Art An open CSS notebook Custom Property “Stacks" Alcohol affects the frontal cortex Embracing the Universal Web CSS most normalizer-est Introducing Sass Modules F*CSS Not clear to me, an installation Framed | Born to choose this way Last Bullet, live music video A Dark Plain, live music video Guts | Let Rejecting maleness Chosen family (thank you) Mia Speaking of pride More CSS Charts, with Grid & Custom Properties (Mis)gender Stop Being Productive Fun with Viewport Units Gods on the Lam Body & gender fragments Trans Interviews & Photography Getting Started with CSS Grid Just Like That Adaptation: SideSaddle/Myths Justice [under construction] Some clarifications on trans language Some kind of resistance tour Loops in CSS Preprocessors An Interview with Miriam Suzanne Estrogentrification Miriam, a how-to guide Holes / SideSaddle midwest tour Underground music showcase Species of the stars PROPHETIA VETITUM MUNDI Pig Sez, song demo I'm Not Ready To Go Yet, song demo UMS day 4 (the end) UMS day 3 A Dark Plain, song demo UMS day 2 UMS day 1 Media Archeology Lab, Artist in Residence Stratified design (re)Thinking on your feet Five(5), song demo Poetry readings are terrible
The gray areas of HWB color
2022-06-29 · via Miriam Eric Suzanne

Working on Sass support for color spaces, I ran into a question about the proper handling of hwb() colors. That lead me down a rabbit hole, exploring the edges of hwb (and powerless color channels) in CSS.

Intro to HWB colors

The HWB format has been around in design tools for a long time, but it is fairly recent on the web. It works by taking a fully-saturated hue, and then mixing in whiteness and blackness to achieve less-saturated lighter and darker variants. For any given hue, you can visualize a triangle with a fully saturated color in one corner, and then white and black in the other corners. This is how the old Chrome color picker used to show HWB color:

color picker with an outer wheel for selecting hue, and an inner triangle for selecting relative amounts of white and black

The side of the triangle across from our saturated-hue corner is entirely grayscale – the line at which blackness and whiteness together reach 100%, and wash out any contribution from the hue.

I think it’s a pretty good model for adjusting colors. Mixing with white or black is often the best way to ‘lighten’ or ‘darken’ a color. Under the hood, HWB still relies on RGB color math (which is not perceptually uniform) and the sRGB color space (which is fairly limited) – but that’s also true of the more popular HSL format.

HWB in CSS

The hwb() function in CSS expects a hue angle (usually in degrees) and two percentages representing the amount of ‘whiteness’ and ‘blackness’ to mix in:

/* a bright cyan with only slight whiteness & blackness */
html { background: hwb(180deg 5% 10%); }

Because we’re using numbers here instead of a triangle, it’s possible for the combined whiteness and blackness to overshoot 100%. If you lay the results out in a table, the triangle is still visible – where the combined whiteness & blackness are less than or equal to 100% – but we also see a reflected grayscale triangle where the combined values are greater than 100%:

w0% w10% w20% w30% w40% w50% w60% w70% w80% w90% w100% b0% b10% b20% b30% b40% b50% b60% b70% b80% b90% b100%
An HWB table of colors using 180deg hue, incrementing whiteness and blackness from 0 to 100%.

That extended grayscale triangle is useless. There’s nothing meaningful to find out there in the wilderness beyond 100% grayness – so CSS just scales whiteness and blackness down until they fit the triangle again. The rendered color hwb(0deg 50% 50%) is identical to hwb(0deg 60% 60%) and hwb(0deg 100% 100%) – because all of them have the same relative mix of white and black washing out the hue.

‘Powerless’ & ‘missing’ channels

It may be a bit strange that we have access to so many duplicate grays in HWB, but there tends to be some duplication in any color format using hue angles. In HSL colors, for example, a lightness of either 0% or 100% will wash out both hue and saturation. The rendered color hsl(0deg 20% 100%) is identical to hsl(0deg 60% 100%) and hsl(0deg 100% 100%). Still, that’s a much smaller portion of the table:

s0% s10% s20% s30% s40% s50% s60% s70% s80% s90% s100% l0% l10% l20% l30% l40% l50% l60% l70% l80% l90% l100%
An HSL table of colors using 180deg hue, incrementing lightness and saturation from 0 to 100%.

In both HWB & HSL colors, we can describe white and black and a full scale of grays using any hue we want. It doesn’t matter what hue we provide in either table – the grayscale cells will remain the same. In those cells, hue has become ‘powerless’.

CSS Color Module Level 4 defines a number of situations that might result in powerless color components:

  • hsl:
    • If the saturation value is 0%, then the hue channel is powerless.
    • If the lightness value is either 0% or 100%, then both the hue and saturation values are powerless.
  • hwb:
    • If the combined whiteness and blackness values (after normalization) are equal to 100%, then the hue channel is powerless.
  • lab/oklab:
    • If the lightness value is 0%, then both the a and b channels are powerless.
    • The current spec has an open issue to determine if high values of lightness (whites) should make the a and b values powerless, but there is no clear upper boundary to rely on.
  • lch/oklch:
    • If the chroma value is 0%, then the hue channel is powerless.
    • If the lightness value is 0%, then both the hue and chroma channels are powerless.
    • The current spec has an open issue to determine if high values of lightness (whites) should make the hue and chroma values powerless, but there is no clear upper boundary to rely on.

For the most part, powerless channels are a harmless result of representing colors through math. But sometimes, when we want to mix or adjust colors, a powerless component can show up suddenly and cause issues. Imagine a gradient from hsl(0deg 100% 0%) (black, with a powerless red hue) through hsl(180deg 100% 50%) (a bright cyan). If we do naive math to get from one to the other – adjusting each channel along the way – we have to go through a full range of hues just to get from black to cyan:

Lucky for us, browsers don’t generally render gradients using naive HSL math:

Currently browsers convert everything to sRGB before mixing. Gradients in RGB can still get muddy at times, but there are no ‘powerless’ components – gray scales involve an equal mix of all three channels – so that’s a separate issue.

The new spec provides a more explicit way to ‘leave out’ a powerless channel, so that it can’t cause issues like this. We can use the none keyword in place of a value to notate a ‘missing’ channel:

/* full black, the hue and saturation are powerless */
html { background: hsl(none none 0%); }

Browsers are instructed to use none when they are converting from one format to another, if the conversion results in a powerless channel. Rather than converting #000 to hsl(0deg 0% 0%), we convert it to hsl(none none 0%) with a missing hue and saturation.

There’s a lot more we can do with missing channels, even using ‘impossible’ colors in interesting ways, but I’ll leave that for a different article sometime.

Clamping vs scaling

So HWB is doing something familiar, but it’s also doing something unique. I don’t know of any other hue-angle system that generates a table like the HWB example above – where only half the cells contain a useful color.

Other color formats will clamp a channel when it goes outside the meaningful range. These HSL colors are the same, because percentages below 0% or above 100% are clamped to the boundaries of that range:

html {
  --red: hsl(0deg 100% 50%);
  --same-red-clamped: hsl(0deg 2530% 50%);
}

HWB works the same way – whiteness and blackness outside the 0%-100% range are clamped. But HWB has to go one step farther. If the sum of whiteness and blackness is above 100%, they both have to get scaled down while maintaining their relative ratio. That means these colors are also the same:

html {
  --gray: hwb(0deg 80% 20%);
  --same-gray-scaled: hwb(0deg 100% 25%);
  --same-gray-clamped-scaled: hwb(0deg 2530% 25%);
}

Wait, what’s the order of operations?

That final clamped-and-scaled color is what caught my attention. At first I wasn’t sure about the order of those two operations, and we get a different result depending on which happens first. Does 2530% 25% first get clamped to 100% 25% and then further scaled down to 80% 20% – or will it get scaled first to roughly 99% 1%, at which point it no longer needs clamping?

The answer is in the spec, though it’s a bit subtle and I missed it at first (emphasis added):

Values outside of these ranges are not invalid, but are clamped to the ranges defined here at computed-value time. If the sum of these two arguments is greater than 100%, then at computed-value time they are further normalized to add up to 100%, with the same relative ratio.

At first that seemed like a mistake – why not maintain the ratio where we can? – but in practice it makes sense. We have to clamp the lower boundary, since negative values have no meaning, so it’s reasonable to start by treating the upper boundary in the same way.

Out-of-range percentages are meaningless, and corrected like a typo, but redundant grays are a natural outcome of the math – still meaningful, even if they aren’t that useful.

The answer to my question

All of that, just to show you this light gray box. It’s a ‘gradient’ of the three HWB colors above, showing that they are actually the same color – one defined appropriately, one needing to be scaled, and a final version that needs both clamping and scaling:

Sass will soon be adding support for all these new color features, and a few more. If only I would stop documenting the rabbit holes, and get back to work on the proposal.