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-4] How to interpolate min/max/clamp? #4082

Closed
xiaochengh opened this issue Jul 3, 2019 · 16 comments
Closed

[css-values-4] How to interpolate min/max/clamp? #4082

xiaochengh opened this issue Jul 3, 2019 · 16 comments
Labels
css-values-4 Current Work

Comments

@xiaochengh
Copy link
Contributor

For interpolation, https://drafts.csswg.org/css-values-4/#combining-values says:

These operations are only defined on computed values.

While https://drafts.csswg.org/css-values-4/#calc-computed-value says:

The computed value of a min(), max(), or clamp() function is the comma-separated list of expressions, with each expression having all its component computed.

So their computed values are not numbers, and there's no straightforward way to interpolate them. Should this be explicitly specified?

@ewilligers
Copy link
Contributor

Suppose we have a start keyframe
width: min(10%, 1em)
and an end keyframe
width: max(30%, 2em)

Suppose we initially have container width 100px and font-size 20px. We pause the animation at 50% progress, change the container width to 250px and change the font-size to 15px.

If we read the computed style at 50% progress, the result changes from
calc((0.5 * min(10%, 20px)) + (0.5 * max(30%, 40px))) to calc((0.5 * min(10%, 15px)) + (0.5 * max(30%, 30px)))

Informally, we might think of the start frame "used value" changing from min(10px, 20px) = 10px to min(25px, 15px) = 15px, and the end frame "used value" changing from max(30px, 40px) = 40px to max(75px, 30px) = 75px. The changes to container width and font-size cause the "used value" result at 50% progress to change from 25px to 45px.

@alancutter
Copy link
Contributor

One option is to introduce an interpolate() function for dimension values.

Note that this was decided for transform list animation as well for similar reasons, there's no way to describe an interpolation for all possible computed values using the same value syntax due to the implicit branching on layout output (after resolving percentages).
#2854

@AmeliaBR
Copy link
Contributor

AmeliaBR commented Jul 4, 2019

My preference would be to interpolate the individual parts of the function.

So, if you are transitioning from width: min(5em, 0) to width: min(5em, 100%), the mid-point of the transition would be width: min(5em, 50%), regardless of how 5em and 50% compare in the current layout.

This would be consistent to what happens if you have a fixed width (or height) and transition max-width or min-width, or vice versa: transition the width value but keep the max/min constant. There are authoring use cases for this behavior, too: if you have multiple elements that you are animating between zero and different target values (e.g., widths of progress bars or bar chart data), you can create an effect of them all transitioning at the same rate, but stopping when they each meet their target, by animating the max clamp from 0 to 100%.

For the functions, this method of interpolation could be extending to work when transitioning from a simple dimension to a min/max/clamp function. The simple dimension could be replaced by a matching function with all the parameters set to the original value, and transitioned from there.

E.g., when transitioning from font-size: 16px to font-size: max(18px, 20vh), the initial value for interpolation would be treated like font-size: max(16px, 16px). The mid-point would therefore be font-size: max(17px, calc(8px + 10vh)).

For transitioning from one function type to another (e.g., from a min() function to a max() function), they would need to be treated as separate terms combined in a calc expression. So, the mid-point between min(5px, 1em) and max(10px, 1em) would be calc( 0.5*min(5px, 1em) + 0.5*max(10px, 1em) ).

@AmeliaBR AmeliaBR added the css-values-4 Current Work label Jul 4, 2019
@Loirooriol
Copy link
Contributor

@AmeliaBR So if you want to interpolate function parameters instead of function results, does it meant that the half point between pow(2, 2) = 4 and pow(2, 4) = 16 would be pow(2, 3) = 8 and not (4+16)/2 = 10? And the half point between tan(0turn) = 0 and tan(0.5turn) = 0 would be tan(0.25turn) = infinity? Or would min/max/clamp be special in this regard?

IMO this kind of things seem nice for simple cases but can get complicated and very inconsistent. So while I do think it's a valid usecase, maybe it would be better to do it using custom props. I believe this is supposed to work if --param is registered as a <length-percentage>:

#el {
  transition: --param 1s;
  --param: 0%;
  width: min(5em, var(-param));
}
#el:hover {
  --param: 100%;
}

@AmeliaBR
Copy link
Contributor

AmeliaBR commented Jul 4, 2019

@Loirooriol My suggestion was specifically for min/max/clamp. I hadn't thought about extending the same logic to the trigonometric or power functions. For the examples you gave, there's no reason not to simplify the math functions at computed time, prior to interpolation. That said, there are trig functions that couldn't be simplified at computed value time, like asin(100%/4em), so we do need to consider that case as well.

@alancutter
Copy link
Contributor

Using registered custom properties for animating individual expression parameters sounds like an appropriate solution to me. I think it's a bit too expensive to have the browser do it by default.

@AmeliaBR
Copy link
Contributor

AmeliaBR commented Jul 5, 2019

I think it's a bit too expensive to have the browser do it by default.

I'm not sure it's any more expensive than the alternative. Compared to calculating layout, interpolating numbers is easy, even if you have a list of them that need to be compared or combined at the end. If we interpolate used values, that means that you can't do the interpolation until after layout. If anything that affects layout is also changing (as in Eric's example), then you can't compute the interpolated value at a given time without doing layout for that point in time.

@tabatkins
Copy link
Member

The problem isn't interpolating numbers, it's matching up numbers across two expression trees so that you know what should interpolate with what. That can get expensive.

@alancutter
Copy link
Contributor

IMO simply allocating and updating such a data structure is the expensive part. The actual interpolation/evaluation math is probably minuscule.

@AmeliaBR
Copy link
Contributor

AmeliaBR commented Jul 9, 2019

We already have rules for upgrading simple expressions to more complex ones in the procedure for interpolating percentages and dimensions, so that shouldn't be an issue.

Currently, if you interpolate between a percentage and a length, there is an initial step where both are converted into calc expressions: 10px100% is treated as calc(10px + 0%)calc(0px + 100%), and then the two quantities inside that calculation are interpolated separately.

Yes, for interpolating more complex calc expressions, the initial setup (expanding out the two values so they have parallel structure) gets more complicated. But that setup only happens once at the start of the transition. For each animation frame, you're only updating individual numbers & then calculating out the result.

The interpolation doesn't apply to used values and it doesn't need layout.

All that said, after spending a lot of time today trying to sketch out how a similar approach could work with comparator functions, I am now convinced that interpolating values inside the functions wouldn't work.

The first issue is that the order of values in a max or min function is arbitrary. Pairing up gives you different results depending on whether you sort them by unit type or not:

  • min(0px, 100%)min(0%, 100px); interpolating term by term, the midpoint would be min(0, calc(50px + 50%)) aka 0
  • min(0px, 100%)min(100px, 0%); interpolating term by term, the midpoint would be min(50px, 50%) aka probably more than 0

Similarly, you can't simplify additions of different comparator functions on a term-by-term basis:

  • max(100px, 100%) + max(20%, 50px) is neither equal to max(150px, 120%) nor to
    max(100px + 20%, 50px + 100%); the first expression has three different calc expansions, depending on the relationship between % and px.

So, my revised suggestion is that any math function needs to be treated as an opaque algebraic term, that can't be interpolated inside of itself.

In other words, when interpolating from min(50px, 100%)min(100px, 50%), the expanded expression would be

  • calc(1*min(50px, 100%) + 0*min(100px, 50%))calc(0*min(50px, 100%) + 1*min(100px, 50%));
    the midpoint would be calc(0.5*min(50px, 100%) + 0.5*min(100px, 50%))

More generally, you replace each unique function with a variable. Transitioning between two non-identical functions results in an expression of the form 1a + 0b0a + 1b, with the interpolated value at progress point t defined as calc( (1-t)*a + t*b).

The same approach would apply to the trig and power functions.

This approach ensures that the interpolation is always linear, and gives the same result regardless of whether you are able to simplify to a number before or after the linear interpolation:

  • pow(2,3)pow(2,4) is the same transition as 8 → 16; the midpoint for a linear interpolation is 12, which is the same as calc(0.5*pow(2,3) + 0.5*pow(2,4))

  • max(10px, 16px)max(30px, 24px) (e.g., after subbing in px for em at computed time) is the same transition as 16px → 30px.

@tabatkins
Copy link
Member

Yes, I agree with this approach (interpolation is basically just treating each side as opaque); I spent some time thinking on it yesterday and came to the same conclusion, but didn't have the time to write it down yet. ^_^

I don't think the caveat about "non-identical functions" needs to be there. All math interpolation would just be scaling and summing the two endpoints, it's just that algebraic simplification is allowed to occur. The old "line up the components and interpolate each" is identical in result to this, since the components all combine linearly as well.

@alancutter
Copy link
Contributor

alancutter commented Jul 10, 2019

Sticking with linear interpolations only for dimension animations keeps things simple for web/browser developers. Registered custom properties enables non-linear interpolation via function parameter interpolation. I'm in support of this.

@AmeliaBR
Copy link
Contributor

I don't think the caveat about "non-identical functions" needs to be there.

Yeah, that's just another optional simplification. It would only be relevant if you're talking about comparing top level expressions, where you need to know if you're actually transitioning the value at all, or if before and after have identical computed values.

Registered custom properties enables non-linear interpolation via function parameter interpolation.

Yes, that is definitely another argument for keeping the default behavior simple: the author can always force a different interpolation behavior by interpolating a variable instead of the complete expression, as Oriol showed above.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed How to interpolate min/max/clamp?, and agreed to the following:

  • RESOLVED: For interpolation math functions are treated as atomic algebraic terms. You interpolate a calc expression that is the weighted sum of all the terms
The full IRC log of that discussion <dael> Topic: How to interpolate min/max/clamp?
<dael> github: https://github.com//issues/4082
<dael> AmeliaBR: I agenda+ it, though thinking of it for next week.
<dael> AmeliaBR: We're starting to get consensus, at least TabAtkins and I and Alan C agree
<dael> AmeliaBR: We have new mask functions including these functions. We don't have anything that defines how work in animations and transitions. They generally work on computed values and these functions exist at computed time so need to define how to interpolate
<smfr> s/mask/math/
<dael> AmeliaBR: Proposal is that interpolation would be a linear interpolation of the terms where the functions on the initial side of interpolation are scaled to to multiplied by 0 and on the result side the scaled up from 0 to 1. Intermediary result is the sum of those terms
<dael> AmeliaBR: Important benefits of the approach is you can define the intermediary value as a computed value without needing to sub in values from layout. And you can do mathematical simplification and it won't change result
<dael> AmeliaBR: If author wants different interpolation like changing power and want power interpolated instead of results they can do by custom properties and interpolate the custom prop rather then final mathematical expression
<dael> Rossen_: Other comments or thoughts?
<dael> AmeliaBR: Prop: For interpolation math functions are treated as atomic algebraic terms. You interpolate a calc expression that is the sum of all the terms
<dbaron> weighted sum, I hope :-)
<dael> Rossen_: Okay. Any objections?
<fremy> LGTM
<dael> AmeliaBR: Yes, weighted sub by the interpolation factor
<dael> s/sub.sum
<dael> s/sub/sum
<TabAtkins> Aka, if interpolating between any two expressions A and B, the interpolation is *defined as* calc(.X*A + (1-.X)*B) (plus any algebraic simplifications)
<svoisen> jensimmons: There are links for both calendars at the bottom of this message https://lists.w3.org/Archives/Member/w3c-css-wg/2017OctDec/0076.html
<dael> RESOLVED: For interpolation math functions are treated as atomic algebraic terms. You interpolate a calc expression that is the weighted sum of all the terms

@alancutter
Copy link
Contributor

Thanks for bringing that to resolution so quickly!

@xiaochengh
Copy link
Contributor Author

Thank you all so much for the quick resolution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-values-4 Current Work
Projects
None yet
Development

No branches or pull requests

7 participants