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] Abandon mix()? #9343

Open
andruud opened this issue Sep 12, 2023 · 21 comments
Open

[css-values] Abandon mix()? #9343

andruud opened this issue Sep 12, 2023 · 21 comments

Comments

@andruud
Copy link
Member

andruud commented Sep 12, 2023

From #6245 (comment):

<TabAtkins> smfr: Would this be like a calc()?
<TabAtkins> fantasai: Like, but wider.
<fantasai> fantasai: It has to be able to interpolate every possible computed value in the entire space of CSS
<TabAtkins> smfr: It requires UAs to have a parallel version of calc trees, for every possible value
<TabAtkins> fantasai: You kinda already have that since everything can interp
<TabAtkins> fantasai: Like, how do you interp between currentcolor and blue? No way to represent that right now. (color-mix() is coming, but this is a wider issue)
<TabAtkins> fantasai: So we have lots of places where we want to interp things that don't have intermediate values
<TabAtkins> smfr: That makes sense, we also invented cross-fade() to hit the image case
<TabAtkins> smfr: I'd like to hear from other impls about their thoughts on impl complexity, and whether it makes sense to think of it in terms of calc()

(cc @smfr)

If my understanding is correct, mix() as currently defined is asking for way too much for not nearly enough benefit.

It has to be able to interpolate every possible computed value in the entire space of CSS

This would indeed require that any computed value that we currently store in a space-efficient manner now needs to deal with the possibility of being a function instead, which then needs to be interpreted further used-value time (@bfgeek). This is probably a non-starter, but it also doesn't seem that necessary. E.g. do we really need to be able to represent display: mix(inline; block; 50%) as a computed value when no meaningful mix between inline/block exists (I hope)?

The current path we're on elsewhere in CSS, with type-specific mix functions (color-mix, font-palette-mix) seems way more sensible:

So ideally we should just abandon mix(). We can probably find other ways of doing everything we want in #6245, e.g. extend all typed *-mix() functions with the capability of pulling progress from a named timeline, and things like that.

@mirisuzanne
Copy link
Contributor

Abandon, or narrow in scope? What happens if this is limited to properties that can have clearly defined interpolations for transitioning/animations/etc? Are there other issues we would need to address?

@andruud
Copy link
Member Author

andruud commented Sep 15, 2023

Abandon, or narrow in scope?

Abandon in its current form. :-)

If we make mix() more eager to resolve itself by computed-value time, then it could be workable.

What happens if this is limited to properties that can have clearly defined interpolations for transitioning/animations/etc?

Yes. We could allow it to be specified for anything, but resolve it by computed-value time if there's no interpolation behavior. Examples:

  • display: mix(inline; block; 10%) => computed value: inline. (Has no interpolation behavior).
  • opacity: mix(0; 1; 10%) => computed value: 0.1. (Has interpolation behavior, but still resolves computed-value time). (EDIT: Actually, the spec already covers this).

And then we allow mix() to actually be the computed value in a select few cases, like <transform-list> edge cases, or animating font-palettes (if we don't go with font-palette-mix()). But specifications allow this on a case-by-case basis, not as a blanket "anything can mix".

@tabatkins
Copy link
Member

The intention of mix() is that it represented exactly the behavior that you get from animating between the two input values; that is, if you could write @keyframes foo { from { prop: start; } to { prop: end; }}, then you could write prop: mix(start; end; X%) and get the exact same value.

This had exactly one purpose: to allow representing the computed value of an animated property that cannot be fully resolved at computed-value time, like a transform that requires layout info (such as translate(50%)) but also triggers matrix interpolation (which can only express fixed pixel values). Anything else it did was secondary; there's some minor usefulness in being able to get an interpolated value without having to interpolate manually, but that wasn't the point.

We could allow it to be specified for anything, but resolve it by computed-value time if there's no interpolation behavior.

Good news: the spec already says exactly this:

If the two <declaration-value>s in mix() are interpolable (without using mix() itself) as values for the property in which it is specified, the computed value of mix() is the result of interpolating these two values to the progress given by the <percentage>. Otherwise, the computed value of mix() is the mix() functional notation itself with its <percentage> computed and both <declaration-value>s computed as values for this property.

So outside of the very few cases that can't be represented at computed-value time, mix() already disappears at computed-value time. This shouldn't require any changes to computed values for any properties that wouldn't already require this kind of thing anyway.

That said, we do have several functions which are essentially specialized mix() versions: color-mix(), palette-mix(), cross-fade(). It is possible for us to just add more specialized functions as needed (a transform-mix()?), if we'd prefer to do that instead of having a generic facility. As you point out, a specialized function allows for more specific control when necessary (like specifying color space in the color mixing functions). I don't have a strong opinion either way, actually.

@andruud
Copy link
Member Author

andruud commented Sep 15, 2023

Good news: the spec already says exactly this: [...]

Are you sure? Wouldn't the current spec cause e.g. the computed value of float: mix(left; right; 10%) to literally be mix(left; right; 10%)?

If the two <s in mix() are interpolable (without using mix() itself) as values for the property in which it is specified, the computed value of mix() is the result of interpolating these two values to the progress given by the <percentage>.

left and right are not interpolable, so this does not apply, which means that we fall into:

Otherwise, the computed value of mix() is the mix() functional notation itself with its <percentage> computed and both <declaration-value>s computed as values for this property.

So we end up with mix(left; right; 10%). This is not a situation that can happen with with regular animations. Yes, allowing this in a few problematic cases was the point of adding this in the first place (again: w3c/css-houdini-drafts#425), so we'd of course have to let mix() be the computed value in those cases. But I allowing it genreally for everything is going to far, as we'd need to be able to represent lots of expensive computed values internally (mix(left; right; 10%)) for no practical benefit.

That said, we do have several functions which are essentially specialized mix() versions: color-mix(), palette-mix(), cross-fade(). It is possible for us to just add more specialized functions as needed (a transform-mix()?)

The good thing about color-mix() is that it isn't that special. It's just another thing allowed by <color>. This means color-mix() can be used wherever <color> goes, be it top-level or other places. So I do think transform-mix() and generally specific-thing-mix() is a better approach, even if they don't offer any special interpolation behavior.

That said, if we still really want mix(), we can make it more approachable by resolving it computed-value time by default (also for discrete animations).

EDIT: Formatting.

@tabatkins
Copy link
Member

left and right are not interpolable

left and right are definitely interpolable, they're just discretely interpolable; you can write keyframes that go from left to right and get a value that flips between them. mix() would return that value. If this was the major misunderstanding, that makes this issue make a lot more sense to me.

I suppose I just didn't properly handle the case where values genuinely aren't interpolable, like for display. That wasn't an intentional omission; I guess it makes the most sense to make that invalid.

[specialized functions represent a type]

Yeah that's valid. Let's ask the larger WG about this; I'm happy to go in either direction.

(But if we drop mix() we do need transform-mix().)

@Loirooriol
Copy link
Contributor

I guess discrete interpolation is still an interpolation, so float: mix(left; right; 10%) would compute to float: left?

And can the broken "<s" in the spec be fixed?

@Loirooriol
Copy link
Contributor

@tabatkins I think #6429 made display animatable. Examples of non-animatable properties would be the animation properties: https://drafts.csswg.org/css-animations/#animation-definition

@tabatkins
Copy link
Member

Ah and also I already do specify that it's invalid if the values aren't interpolable.

@andruud
Copy link
Member Author

andruud commented Sep 15, 2023

If this was the major misunderstanding, that makes this issue make a lot more sense to me.

Ah, OK. I think that was it. See also the IRC discussion in the first post. These comments reinforced (or caused?) my misunderstanding:

fantasai: It has to be able to interpolate every possible computed value in the entire space of CSS

smfr: It requires UAs to have a parallel version of calc trees, for every possible value

tabatkins added a commit that referenced this issue Sep 15, 2023
@tabatkins
Copy link
Member

I added some more examples calling out these cases.

But your point about the specialized functions being a useful type is still valid, so let's still talk about this in the group.

@tabatkins tabatkins added css-values-5 and removed css-values-4 Current Work labels Oct 23, 2023
@tabatkins
Copy link
Member

For now, we've kicked mix() out of Values 4 and put it into the Values 5 draft (alongside a more general treatment of mixing stuff, as discussed in #6245).

@Loirooriol
Copy link
Contributor

I suggested moving it to L5 in #6753 (comment), but @fantasai said

No, we need that one [mix()] to handle serialization of interpolation of transforms.

@tabatkins
Copy link
Member

Yeah, but that's been an issue for years, so we're not making things worse in the interim. ^_^

@tabatkins
Copy link
Member

We've added transform-mix() to Values 5, so I think all the values that can't be represented now have a representation.

@fantasai fantasai changed the title [css-values-4] Abandon mix()? [css-values] Abandon mix()? Nov 21, 2023
@LeaVerou
Copy link
Member

I think the goal of mix() is a little murky and if we're keeping it we need to reach consensus on that:
Is it intended to…

  1. Allow authors to tap into the default interpolation algorithm without having to resort to hacks like paused animations with negative delays or complex logic, i.e. any mix() value always has an equivalent CSS value without mix(), essentially a higher level abstraction over existing syntax.
  2. Or to also serve as the (potentially sole) representation of the interpolated value for certain types, essentially reducing the need to add more specific *-mix() functions to CSS?

These involve different tradeoffs and different design decisions. I suspect 1 is easier to implement (?) and from the author's perspective, 1 covers their use cases just fine. 2 does make for a smaller API surface, but its benefits are largely internal.

From an author perspective, it’s entirely unclear why mix() needs to be a whole value, or how exactly does this work. E.g. I imagine most authors would not be able to tell with reasonable confidence whether this would be allowed:

--untyped-custom-property: mix(...);
property: something var(--untyped-custom-property), something else;

The current path we're on elsewhere in CSS, with type-specific mix functions (color-mix, font-palette-mix) seems way more sensible

mix() covers distinct use cases over these.


I wonder if a reasonable MVP would be to only ship the mix(<progress> of <animation-name>) syntax. It is unclear from the current spec what property that returns, but it seems like that should be a parameter that would default to the current property:

<mix()> = mix( [ <custom-ident> at ]? <progress> of <animation-name> )

This already makes the current workarounds a lot easier, doesn’t have warts like having to be the whole value of a property, and can even be generalized in a limited way like so:

@keyframes mix-helper {
	from { background: var(--from);
	to { background: var(--to);
}

.foo {
	--from: yellow;
	--to: linear-gradient(in oklch, yellow, oklch(.5 .2 180));
	other-property: mix(background at 30% of mix-helper);
}

@tabatkins
Copy link
Member

The "extract a value from particular progress along a keyframe'd animation" idea is completely unrelated to mix(); it was discussed around the same time, but it's not in any way a "mixing" function. I'm annoyed it got folded into the spec; it needs to be a completely different function. I suggest ignoring it; I'm pinging Elika right now about killing it (and potentially reviving it in a dedicated function instead).

Ignoring that usage, what use-cases do you think mix() covers that aren't covered by existing values and the proposed *-mix() family?

@Loirooriol
Copy link
Contributor

Loirooriol commented Nov 29, 2023

In aspect-ratio, mix() allows the nice logarithmic interpolation of ratios, e.g.

aspect-ratio: mix(50%, 1, 9); /* 3 */

If I use aspect-ratio: calc-mix(50%, 1, 9) I will get 5 instead. So I need a more cumbersome exp(calc-mix(50%, log(1), log(9))) or calc(pow(1, 1 - 0.5) * pow(9, 0.5)), and these don't handle degenerate ratios properly.

I'm not sure whether a ratio-mix() is worth it, but this is the use-case that comes to mind.

@Crissov
Copy link
Contributor

Crissov commented Nov 29, 2023

Isn’t this particular use case of ratio-mix() simply the geometric mean? #4700

PS: Nevermind, you actually mentioned that yourself in #4953.

@Loirooriol
Copy link
Contributor

Yes, specifically at 50% it can be simplified as a basic geometric mean, but otherwise it's a weighted geometric mean and you need something like the formulas above.

@tabatkins
Copy link
Member

In aspect-ratio, mix() allows the nice logarithmic interpolation of ratios

Sure, but (a) the interpolated result is completely expressible as a number, and (b) you can reproduce it by hand with log() and exp() functions: aspect-ratio: exp(log(1) * p + log(9) * (1 - p)) (or exp(calc-mix(p, log(1), log(9))) if you want).

If we think that exposing a convenient shorthand for logarithmic interpolation would be useful, we can do so, but there's nothing requiring us to do so like the other cases we've added a *-mix() for. (Well, calc-mix() is just a convenience shorthand too, but it justified itself well.)

@css-meeting-bot
Copy link
Member

css-meeting-bot commented Dec 7, 2023

The CSS Working Group just discussed [css-values] Abandon mix()?, and agreed to the following:

  • RESOLVED: Drop mix() from Values 4
  • RESOLVED: we will not rely on mix() to represent a single interpolated value but may have something like it for expressing the whole interpolation
The full IRC log of that discussion <dael> TabAtkins: First off, to clarify. Since issue was filled mix() gained a new syntax. We're not talking about that. This is about dropping original feature for mix(). It was intended to let us represent values that couldn't be written as computed but do interpolate
<dael> TabAtkins: Since then we added sufficient specialized functions to values 5 to handle all cases we know where this happens. Only a handful of cases that needed the generic interpolation.
<dael> TabAtkins: So, I believe we can drop the generic interpolator and stick with the specific ones that handle each thing that needs special behavior
<dael> florian: And it did not represent things that are not interpolable?
<dael> TabAtkins: Correct, it was invalid syntax
<dael> fantasai: Two reasons mix was added. One was for these interp that you can't show intermediary value. Other was create syntax to allow authors to represent an interpolation of a value between two breakpoints in a MQ. It's not that the intermediary can't be represented but it's because you want a range depending on another calc
<miriam> q+
<dael> fantasai: That's what mix was originally for. Since then, TabAtkins and I decided it should be kicked from level 4. But the other function of mix where you can take a whole property value and interpolate between it and another value based on how far you are in a 800px window
<dael> florian: And you could represented it but not well?
<astearns> ack miriam
<dael> fantasai: You could, but now that there's calc-mix you can represent. But if you have something like descrete keywords you can't unless there's an existing mix value
<dael> miriam: You've basically answered my question.
<dael> TabAtkins: They cover the case for the types they're meant for. There's a bunch of types like keywords that don't have a mix function
<dael> miriam: That use case feels useful to me
<dael> fantasai: I think there are reasons to want something like mix. I don't think we want to impl as is. Reasonable to resolve not in L4 but seems reasonable to explore
<dael> TabAtkins: Also resolve that we're not intending mix to represent intermediate values would be good. But intent that it's useful for authors to say somewhere on this path is good. Good to resolve that we're not keeping mix to rep intermediate values
<dael> astearns: Prop: We will drop mix from L4. Specifically, we don't want to use it for the generic representation of interpolated values but we may in a future level come up with something like mix to express the interpolation itself
<dael> fantasai: 2 resolutions. Drop mix from Values L4. Second is: Keep exploring mix for interpolation use cases but not for representing a single interpolated value
<TabAtkins> proposed: we're not keeping mix() *for the purpose of representing intermediate values*. keeping it for *authors wanting to interpolate arbitrary things* is still on the table
<dael> astearns: First part, drop mix from Values 4. Obj?
<dael> RESOLVED: Drop mix() from Values 4
<dael> astearns: Second is we will never use mix to represent a single interpolated value but may have something like it for expressing the whole interpolation
<dael> fantasai: It shouldn't be the only way to represent the value
<dael> RESOLVED: We will not rely on mix() to represent a single interpolated value but may have something like it for expressing the whole interpolation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants