Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-values-5] Accept more than 2 values in *-mix(<progress>, ...) and progress() notations #11530

Open
LeaVerou opened this issue Jan 18, 2025 · 4 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Jan 18, 2025

A common need in design systems is to get a value based on its position in a list. E.g. get the lightest color tint, or the smallest font-size etc.

In css-values-5 we define *-mix() functions (calc-mix(), color-mix() etc) that interpolate rather than mix, i.e. 0% gets you the first color and 100% the second one instead of the way around which is how the mixing version works.

This means they do not actually need to be limited to two arguments!
All we need to solve a lot of the use cases that motivated #10034 and a host more is to convert the grammars of these mix functions from:

<calc-mix()> = calc-mix( <progress>, <calc-sum>, <calc-sum> )
<color-mix()> =
  color-mix( [ <progress> && <color-interpolation-method>? ] , <color>, <color> ) |
  color-mix( <color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2} )

to

<calc-mix()> = calc-mix( <progress>, <calc-sum># )
<color-mix()> =
  color-mix( [ <progress> && <color-interpolation-method>? ] , <color># ) |
  color-mix( <color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2} )

And similarly for cross-fade() and transform-mix() though I haven't found use cases for these.

Yes, it can be emulated with the 2-value *-mix(), but the code to do it is abysmal and its complexity increases quadratically with the number of values.

The same argument applies to progress(), being the inverse of these. Currently it is defined as:

<progress()> = progress(<calc-sum>, <calc-sum>, <calc-sum>)

With this change, it would become:

<progress()> = progress(<calc-sum>, <calc-sum>#)

(or just progress( <calc-sum>#) but separating the first argument may be better for clarity)

A nice-to-have could also be to support percentages as well with the values, which would work in the same way as gradient color stops, i.e. affect how the interpolation works.

This is a change that only very slightly increases implementation effort for these functions, and would save authors a ton of pain, so it seems like a no-brainer to me.

@LeaVerou LeaVerou changed the title [css-values-5] *-mix(<progress>, ...) notation with more than 2 values [css-values-5] Accept more than 2 values in *-mix(<progress>, ...) and progress() notations Jan 18, 2025
@Loirooriol
Copy link
Contributor

interpolate rather than mix

I don't understand what difference you are implying

This means they do not actually need to be limited to two arguments!

I don't understand how this is supposed to behave

@LeaVerou
Copy link
Member Author

interpolate rather than mix

I don't understand what difference you are implying

This should be better explained in the spec, which just mentions the two concepts :/
Take a look at the following:

  1. color-mix(100% color1, color2) will produce color1
  2. color-mix(100%, color1, color2) will produce color2

Basically mixing has the opposite behavior of interpolation: 100% of color1 implies 0% of color2 and gives you color1. Whereas a progress of 100% from color1 to color2 (interpolation) gives you color2 because it represents progress alongside the continuum of color1 to color2 (think of how gradients behave).

While it's unclear what something like color-mix(color1 50%, color2 30%, color3) would do, so we decided to stick to 2 colors for mixing, when it's interpolation, something like color-mix(80%, color1, color2, color3) can be well defined: color2 is at 50% so 80% represents 30%/50% = 60% from color2 to color3, i.e. color-mix(60%, color2, color3).

I don't understand how this is supposed to behave

I think understanding the difference of mixing vs interpolation is kinda a prerequisite for this proposal.

@Loirooriol
Copy link
Contributor

Loirooriol commented Jan 20, 2025

Basically mixing has the opposite behavior of interpolation

I disagree that they are opposites. Whether you call color-mix(100% color1, color2) and color-mix(color1, 100% color2) mixing or interpolation, it's clear that they are the same kind of thing, just with different weights. And color-mix(100%, color1, color2) is equivalent to the latter, not the former.

While it's unclear what something like color-mix(color1 50%, color2 30%, color3)

Well, I think https://drafts.csswg.org/css-color-5/#color-mix-percent-norm could be generalized in a quite natural way:

  1. Let S1 be the sum of the non-omitted percentages: 50% + 30% = 80%
  2. Distribute (in equal parts) 100% - S1 among the values that omitted their percentage. In this case there is a single omitted percentage, so the 3rd color takes all of the remaining 100%-80% = 20%
  3. Let S be the sum of all percentages (note this is 100% if there was some omitted percentage, otherwise it's S1). So 100% in this case.
  4. If S is 0%, the function is invalid
  5. If S > 100%, scale all percentages accordingly so that they add up to 100%.
  6. If S < 100%, save S as an alpha multiplier. They scale percentages accordingly so that they add up to 100%.

So in this case it would be color-mix(color1 50%, color2 30%, color3 20%), equivalent to

color-mix(
  color1 50%,
  color-mix(color2 60%, color3 40%) 50%
)

something like color-mix(80%, color1, color2, color3) can be well defined: color2 is at 50% so 80% represents 30%/50% = 60% from color2 to color3, i.e. color-mix(60%, color2, color3)

Yes that can work, though I would maybe call this color picking rather than mixing/interpolating. Because it's placing colors at equidistant positions between 0% and 100%, and then picking the color at the provided percentage, falling back to interpolating the nearest pair of colors if there is no color at that exact position.

However, I think forcing equidistant positions may be a bit inflexible. Just like gradients, I suspect that authors will ask to be able to assign colors at a custom percentage position, or even a range. But these positions could be confused with mixing weights, so I would prefer a different function name.

@LeaVerou
Copy link
Member Author

LeaVerou commented Jan 20, 2025

Happy to generalize it to the version with weights too, though I do think it's somewhat less intuitive.

Yes that can work, though I would maybe call this color picking rather than mixing/interpolating.

Note that this doesn't just affect color-mix(). What would you call it for calc-mix()? Length picking?

However, I think forcing equidistant positions may be a bit inflexible. Just like gradients, I suspect that authors will ask to be able to assign colors at a custom percentage position, or even a range.

I completely agree, but equidistant is fine for an MVP — we can add positions later. Far, far easier to add intermediate color stops to fake the lack of positions than to deal with a quadratic number of nested calls.

But these positions could be confused with mixing weights, so I would prefer a different function name.

I agree *-mix() is unfortunately named, but I don't feel as strongly about it, and would definitely not want to hold up the feature because of that. However, whatever name we go with should be the one for the 2 argument case as well.
Perhaps *-scale()? color-scale(), calc-scale() etc.

@astearns astearns moved this to FTF agenda items in CSSWG January 2025 meeting Jan 22, 2025
@LeaVerou LeaVerou moved this from FTF agenda items to Friday morning in CSSWG January 2025 meeting Jan 24, 2025
@astearns astearns moved this from Friday morning to Friday afternoon in CSSWG January 2025 meeting Jan 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Friday afternoon
Development

No branches or pull requests

2 participants