Repository navigation
[css-values] Abandon mix()? #9343
Description
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:
- There are no issues with "must be sole value" or other weirdness.
- They can support detailed interpolation behavior (e.g. interpolation a given color space). Only a matter of time until we run into "interpolate HOW" if everything uses
mix(). - We only grant mixing capabilities where it actually matters and has meaning.
- It's possible to implement and ship gradually.
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.
Activity
mirisuzanne commentedon Sep 14, 2023
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 commentedon Sep 15, 2023
Abandon in its current form. :-)
If we make
mix()more eager to resolve itself by computed-value time, then it could be workable.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 withfont-palette-mix()). But specifications allow this on a case-by-case basis, not as a blanket "anything can mix".tabatkins commentedon Sep 15, 2023
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 writeprop: 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
transformthat requires layout info (such astranslate(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.Good news: the spec already says exactly this:
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 commentedon Sep 15, 2023
Are you sure? Wouldn't the current spec cause e.g. the computed value of
float: mix(left; right; 10%)to literally bemix(left; right; 10%)?leftandrightare not interpolable, so this does not apply, which means that we fall into: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 letmix()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.The good thing about
color-mix()is that it isn't that special. It's just another thing allowed by<color>. This meanscolor-mix()can be used wherever<color>goes, be it top-level or other places. So I do thinktransform-mix()and generallyspecific-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 commentedon Sep 15, 2023
leftandrightare definitely interpolable, they're just discretely interpolable; you can write keyframes that go fromlefttorightand 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.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 needtransform-mix().)Loirooriol commentedon Sep 15, 2023
I guess discrete interpolation is still an interpolation, so
float: mix(left; right; 10%)would compute tofloat: left?And can the broken "<s" in the spec be fixed?
Loirooriol commentedon Sep 15, 2023
@tabatkins I think #6429 made
displayanimatable. Examples of non-animatable properties would be the animation properties: https://drafts.csswg.org/css-animations/#animation-definitiontabatkins commentedon Sep 15, 2023
Ah and also I already do specify that it's invalid if the values aren't interpolable.
andruud commentedon Sep 15, 2023
Ah, OK. I think that was it. See also the IRC discussion in the first post. These comments reinforced (or caused?) my misunderstanding:
[css-values-4][editorial] Added more examples, slightly rephrased som…
tabatkins commentedon Sep 15, 2023
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.
[css-values-4] Punt mix() to Values 5 #9343
tabatkins commentedon Oct 23, 2023
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 commentedon Oct 23, 2023
I suggested moving it to L5 in #6753 (comment), but @fantasai said
tabatkins commentedon Oct 23, 2023
Yeah, but that's been an issue for years, so we're not making things worse in the interim. ^_^
tabatkins commentedon Nov 3, 2023
We've added
transform-mix()to Values 5, so I think all the values that can't be represented now have a representation.[-][css-values-4] Abandon mix()?[/-][+][css-values] Abandon mix()?[/+]LeaVerou commentedon Nov 29, 2023
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…
mix()value always has an equivalent CSS value withoutmix(), essentially a higher level abstraction over existing syntax.*-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: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: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:
tabatkins commentedon Nov 29, 2023
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 commentedon Nov 29, 2023
In
aspect-ratio,mix()allows the nice logarithmic interpolation of ratios, e.g.If I use
aspect-ratio: calc-mix(50%, 1, 9)I will get5instead. So I need a more cumbersomeexp(calc-mix(50%, log(1), log(9)))orcalc(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 commentedon Nov 29, 2023
Isn’t this particular use case of
ratio-mix()simply the geometric mean? #4700PS: Nevermind, you actually mentioned that yourself in #4953.
Loirooriol commentedon Nov 30, 2023
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 commentedon Nov 30, 2023
Sure, but (a) the interpolated result is completely expressible as a number, and (b) you can reproduce it by hand with
log()andexp()functions:aspect-ratio: exp(log(1) * p + log(9) * (1 - p))(orexp(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 commentedon Dec 7, 2023
The CSS Working Group just discussed
[css-values] Abandon mix()?, and agreed to the following:RESOLVED: Drop mix() from Values 4RESOLVED: we will not rely on mix() to represent a single interpolated value but may have something like it for expressing the whole interpolationThe 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