Closed rachelnabors closed 2 years ago
Moving this to [css-timing-2] since I believe that is where we will end up addressing this.
previously proposed to www-style by @AmeliaBR.
I'd kind of forgotten about that. I'd definitely forgotten that I'd made nice figures to go with it. It would probably be helpful if they were copied over here. Pulling out the key points from my August 2015 proposal here, with some updated links.
Make the definition of cubic Bézier curves:
A cubic Bézier curve is defined by a series of control points, P0 through Pn (see [the figure] for an example where n=4). P0 and Pn are always set to (0,0) and (1,1), respectively. The parameters to the easing function are used to specify the values for points P1 to Pn-1. These can be set to preset values using the keywords listed below, or can be set to specific values using the cubic-bezier() function. In the cubic-bezier() function, each point Pi is specified by both an X and Y value.
Then farther down, the syntax and definition of the cubic-bezier function is replaced as follows:
cubic-bezier([<number>, <number>]*)
Specifies a cubic-polybezier curve. Each pair of values specifies a point Pi in the form x1,y1; an odd-numbered sequence of values is invalid. All x values must be in the range [0, 1] and each x value must be equal to or greater than the previous; otherwise, the definition is invalid. The y values are unrestricted. The series of points is converted to a series of connected cubic Bézier curve segments by grouping into sets of three: two control points followed by a vertex point. If the number of points (plus the implicit 1,1 end point) is not a multiple of three, the sequence is padded with the point 1,1 to create complete cubic Bézier segments.
For example,
- A cubic-bezier function with no parameters is equal to cubic-bezier(1,1, 1,1), which is essentially a linear timing function.
- The ease-in function could also be written as cubic-bezier(0.42,0).
A multiple bounce transition could be written as follows, resulting in the curve visualized in [Figure]:
cubic-bezier(0.25,0.25, 0.25,0.75, 0.3,1, 0.5,0.5, 0.6,0.5, 0.7,1, 0.85,0.8 0.9,0.8)
Finally, a new function could be added to make it easier to make smooth curves:
smooth-cubic-bezier([
, ]*) Specifies a cubic-polybezier curve with automatically-calculated smooth connections between segments. Each pair of values specifies a point Pi in the form (x1, y1); an odd-numbered sequence of values is invalid. All x values must be in the range [0, 1], each x value must be equal to or greater than the previous; otherwise, the definition is invalid. The y values are unrestricted. If there are 1 or 2 pairs of values provided, the result is the same as for the cubic-bezier function. For 3 or more points, an implicit control point is inserted after every vertex point, that is equal to that vertex point plus the vector from the previous control point to that vector point. The remaining explicit points therefore alternate between control points and vertex points. Again, the sequence of points is padded with 1,1 if necessary to make complete cubic Bézier segments.
For example, the following two functions specify the same curve:
smooth-cubic-bezier(0,0.75, 0,1, 0.5,0.5) cubic-bezier(0,0.75, 0,1, 0.5,0.5, 1,0, 1,1)
Both result in the curve visualized in [Figure]
A first approximation at the figures are attached; if you want to play around quickly, you can also fork from https://jsfiddle.net/L1o71c1c/1/ Or, you know, write a script to generate the SVG markup automatically from a function specification.
Things to discuss:
Given these definitions, should new pre-defined functions be introduced to represent bounce/elastic timing functions in popular JS libraries?
Is the restriction on x values appropriate? We need to ensure that the final curve is a proper function, with each x value having a single corresponding y value. Forcing vertex and control points to be in sorted x order should ensure this, but may be overly restrictive.
It is nonetheless possible to draw a completely vertical arc segment with the current wording. We could address this by adding another restriction (the x values for vertex points must be strictly greater, not just greater or equal to) or we could add an interpretation rule (apply the maximum value, consistent with how the steps function works).
Are the commas between every number in the cubic-bezier function necessary? Do current implementations enforce that syntax? Compare with (a) the Shapes spec which requires commas between points in a polygon, but does not allow them between x y pairs, and (b) SVG syntax, which allows commas and whitespace interchangeably, so many people with mathematical background use commas to join x y pairs and whitespace to separate them, like x1,y1 x2,y2.
[Note that there were comments on the proposal at the time from Brian Birtles, including a link to an even older proposal, that I'll copy into a separate comment to get everything in one place.]
Proposal for "Stacked Timing Functions" from (I think) a face-to-face breakout session in May 2015, as sent to the fx mailing list in June 2015 by @birtles.
We worked on a syntax for chained timing functions and came up with:
easing: [ tf? point? ]?
Concretely, easings look like
easing: cubic-bezier(a,b,c,d) (x, y) cubic-bezier(e,f,g,h) (x2,y2) ...
You can leave out the points (they're evenly distributed between provided points, where easings always start at (0,0) and end at (1,1).
You can leave out the timing functions (in which case they'll default to linear).
cubic-bezier parameters are always in global (0,0,1,1)-space. This means that you can accidentally provide erroneous easings, e.g.:
easing: cubic-bezier(0.2, 0.8, 0.6, 0.6) (0.5, 0.3) cubic-bezier( 0.6, 0.9, 0.7, 1.0)
Here the first point has an x coordinate that is less than the second x control point of the first bezier (0.5 < 0.6). Concretely, an easing is OK as long as all of the x coordinates (specified and inferred) are in nondescending== increasing order.
So this is incorrect too, although it's hard to see why:
easing: cubic-bezier(0.2, 0.8, 0.6, 0.6) cubic-bezier( 0.6, 0.9, 0.7, 1.0)
becomes:
easing: cubic-bezier(0.2, 0.8, 0.6, 0.6) (0.5,0.5) cubic-bezier( 0.6, 0.9, 0.7, 1.0)
(because that's what easing: linear linear would do)
We could instead infer point x coordinates to be evenly spaced between adjacent specified x coordinates, which would mean that the above would be equivalent to
easing: cubic-bezier(0.2, 0.8, 0.6, 0.6) (0.6,0.5) cubic-bezier( 0.6, 0.9, 0.7, 1.0)
which is actually fine. But if we're doing that, should we do the same thing for the y coordinate?
easing: cubic-bezier(0.2, 0.8, 0.6, 0.6) (0.6,0.75) cubic-bezier( 0.6, 0.9, 0.7, 1.0)
Google to polyfill this. If it's good then we'll add something to L2.
moving comments from #3838 to here. looks like what I'd specified and more has been covered in this thread, but I'll share what I'd said anyway.
Physics aren't likely to land any time soon to help folks with spring, bounce or cliff effects, and there's big demand for these engaging and lifelike animation tactics. Due to lacking phsyics, folks bring in javascript animation libraries to do the work, or visit websites that generate 100s of keyframes, or just flat out abuse the cubic-bezier()
function. JS libraries and cubic-bezier()
abuse being the most common, since it's the easiest.
Most of these workarounds result in difficult to manage, rigid or uncanny animations that don't make the web look very great at animation.
cubic-bezier() https://codepen.io/AdrienBachmann/pen/VYVvGo
JS library (34kb) https://codepen.io/rossorthwein/pen/XorVVV
I think we could pacify the need for physics by adding the ability for cubic-bezier()
to take more points. Extend cubic-bezier()
to chain/concat points together.
Current
Proposed
Where we currently have 2 pairs passed into cubic-bezier()
, we allow passing additional pairs so more points could be created.
Explode out: source
.animated-layer {
transition: .2s cubic-bezier(.17,.67,.83,.67,.2,.02,.73,.59, ...);
}
I'm feeling partial to @AmeliaBR's proposal of smooth-cubic-bezier()
as well as adding additional pre-defined functions for things like bounce and elasticity. Cool work! It would be nice to get rid of the commas as well, good call.
@Lange, that is helpful.
What would also be helpful is a write-up of what values / options are supported (or not) in the major animation software that support complex custom easing curves. Anyone have good resources for that?
Looking at the Adobe help pages for After Effects, it looks like the complex easing can be a mix of smooth or sharp changes, or even steps.
So I'm now starting to doubt whether it is enough to swap between smooth and full beziers for the entire sequence. Maybe it make more sense to focus on chaining arbitrary segments. Making a syntax easy to write by hand is good. But it's probably more important to ensure that it can accurately represent easings that are already being used in design tools.
I like @vidhill's suggestion of trying to define the easing using a syntax that defines the progress as a direct function of time, instead of a syntax that defines an arbitrary 2D curve and then tries to constrain it to only having one progress value for each time point. But the question again is: can existing software export to the syntax without loosing information?
As @visiblecode mentioned earlier, we could simply reuse the path()
function currently defined in the Motion Path Module and referring to SVG paths.
That allows combinations of straight lines, quadratic and cubic Beziérs, gaps, and more, in short, should cover all use cases and is consistent to what we already have.
Btw., the timing functions were generalized to also cover color gradients and therefore were renamed to "easing functions". I assume, everything discussed here also applies to gradients, so, maybe someone could rename the summary of this issue accordingly?
Sebastian
@SebastianZ Path syntax is certainly the most expressive, but it requires extra work to calculate a y-value (interpolated value) at a given x value (time progress or position). And there is no guarantee that you'd get a single y value for any x value.
And yes, we are now talking about generic interpolation functions (although the use cases here all seem to be animation), so I've updated the title to refer to the current name of the spec.
... it requires extra work to calculate a y-value (interpolated value) at a given x value (time progress or position).
Can you please elaborate on that? I don't see why that's work than multiple Beziérs.
And there is no guarantee that you'd get a single y value for any x value.
Right, same issue for Beziérs.
I see two solutions for this problem:
Visualisation for the latter:
Input path:
Normalized path:
Sebastian
That sort of normalization is pretty rough to pull off in a robust/understandable/easy to implement way.
The big issue we're up against here is that cubic beziers (and SVG paths) aren't really suited for defining easing functions, because they treat x and y independently.
This is good for drawing 2d shapes that are supposed to be able to overlap themselves in arbitrary ways. But for reasons that are pretty obvious above this is super awkward for applications where y is supposed to depend on x, like for easing functions.
Honestly the ideal tool for this is something like a cubic hermite spline. Apart from being about as simple as you can get from an implementation perspective, they're friendlier to hand authoring than beziers are going to be. You'd just need to supplement them with a way to indicate instantaneous jumps in position/speed.
Let's look at that in practical terms. To define a cubic hermite spline, you just need a list of (x, y, y') triples.
In this context, x would be time, y position, and y' speed. Which should make some intuitive sense from an animation point of view.
Right there, that's enough to do most any kind of motion, including springy animations etc. The missing piece is animations where speed or position change abruptly.
So, for the impact point in bounce animations you have to be able to give the incoming and outgoing speed separately.
Animations with jumps in position are similar-ish, except you also need to say which of the incoming or outgoing position gets used when the time is exactly x.
So, without getting too much into syntax, this means you might have a choice of four keyframe/control point "types" for easing splines:
Seems decent for hand-authoring, and it's also general enough to support export from most animation software, either exactly or as an extremely close approximation.
Also worth adding that segments of cubic splines are easy to convert into Bernstein form, so implementors can re-use code they already have around for evaluating Beziers.
Note that step easings have already made a decision of whether the moment of transition uses prev or next value: they use the next value. So presumably we'd be consistent there, as the use-case for making it controllable seems extremely minimal - you'd only see the difference if you purposely advanced a paused animation to exactly that progress %.
Overall, uh, that sounds like a pretty good idea, and the fact that the curve is automatically c1-continuous is nice (unless you purposely drop to c0- or non-continuous by providing additional arguments). I like the physicality of being able to provide a velocity directly, which enables realistic-looking bounces really easily by just inverting the y velocity, like your example shows. (Versus chained beziers, which require much more guess-and-checking to get a realistic-looking bounce.)
I think I can sweeten the pot a bit more for hand-authoring. Let's say there's also an auto
option for velocity that picks a velocity for you.
For a smooth node with neighbors on both sides, auto
would mean choose the velocity to get C2 continuity. That way, if you wanted you could spell out just the times and positions by hand, and automatically get buttery smooth motion.
In the remaining situations, auto
would pick the velocity assuming no acceleration. That gives you an easy shorthand for linear motion, which would otherwise be a little clunky to do by hand.
I do like @visiblecode's proposal for hand-authoring friendliness (especially with an automatic curve fitting option), and for the fact that it directly defines y as a function of x, so doesn't need arbitrary limits or fix-ups.
But hand-authoring is only half of the argument. The other is compatibility with existing tools.
Also worth adding that segments of cubic splines are easy to convert into Bernstein form, so implementors can re-use code they already have around for evaluating Béziers.
Do you have a link to the relevant formulae? Are these exact conversions or approximations?
Ideally, this would become the new "master" syntax, that could represent all the keyword and function-notation easings defined in the current spec. In particular, I'd be interested to know if your definition of "automatic" velocity calculations matched Blender's automatic handles.
But we'd also want to directly represent curves that can be generated in popular animation software, like AfterEffects and Blender, which seem to use a mix of straight lines and cubic curves (with internally-enforced limits on the curves to keep them always increasing in the x direction).
We'd also want to make it possible to convert from software that currently uses path notation, like Greensock. Which, based on my testing with their visualizer, seems to use the rule proposed by @SebastianZ to convert arbitrary paths into functions.
My take-away from the couple years I spent on this is:
brief overview on the math, I can go into more detail on specific things if you want
Every segment of a cubic spline translates to a cubic polynomial. There are a number of ways to represent a cubic polynomial, which are all exactly convertible (within the limits of floating point precision etc.), including:
The thing is, Bézier curves are based on Bernstein polynomials (the bezier handles map to the control values). The catch is that a 2D Bézier curve segment consists of two polynomials, one for x and one for y. For drawing curves on screen that's perfect, the two polynomials can be evaluated directly, independently, with all the usual advantages of cubics. Mathematically precise, versatile, and efficient to evaluate.
But, for uses like CSS's cubic-bezier(), you have to solve for the unique root of the x polynomial (with things constrained so there is one), and plug the result into the y polynomial. The root finding part isn't possible to do super efficiently, so in practice what most implementations of cubic-bezier() do is precompute a rough approximation and linearly interpolate over that. It works okay, but there's a big speed/accuracy tradeoff. Probably there's more variation between implementations than people realize, especially in extreme cases.
TBH, in general implementors would probably get better results (both in terms of accuracy and also performance) converting each cubic-bezier() segment to a cubic spline (of ~1-3 segments) rather than trying to approximate cubic-bezier() directly in realtime.
There are a bunch of other animation curve representations which get used as well, NURBS and so on. Some of these superficially resemble Béziers (they have similar handles, etc.), but don't allow exact conversion to/from them.
Blender's smoothing is a bit different to the auto
thing I described above (which is based on spline interpolation). Blender's curves can still be approximated by cubic splines but you wouldn't use auto
to do it.
We'd also want to make it possible to convert from software that currently uses path notation, like Greensock. Which, based on my testing with their visualizer, seems to use the rule proposed by @SebastianZ to convert arbitrary paths into functions.
What Greensock does is discard the parts of the path between where x starts decreasing to where it catches up again. I don't know the details of the implementation, but bezier path intersections tend to have rough edge cases to account for.
Obviously any reasonable easing representation should be able to support export of a "baked" representation (after pruning, etc). But should browsers be expected to implement on-the-fly pruning themselves?
But should browsers be expected to implement on-the-fly pruning themselves?
If we adopt a system where you can directly use SVG path notation, then yes, I would expect some sort of reasonable error handling like that. So that comment was more of an aside, going back to the discussion of the alternative.
But if we adopt something like the cubic spline syntax, the main question is can a tool like Greensock implement an algorithm to convert (with a reasonable degree of fidelity) their fixed-up path into the standard notation.
I have on occasion found myself wanting to supply a formula to express timing functions for example when I wanted to perfectly invert another timing function to implement an accelerated expanding reveal animation. We ended up approximating this inverse scale with a generated linear animation with lots of keyframes but it could have been expressed as a single formula. Could supplying a formula solve some of these other express-ability use cases as well?
@AmeliaBR wrote:
If we adopt a system where you can directly use SVG path notation, then yes, I would expect some sort of reasonable error handling like that.
Fair!
But if we adopt something like the cubic spline syntax, the main question is can a tool like Greensock implement an algorithm to convert (with a reasonable degree of fidelity) their fixed-up path into the standard notation.
Yes.
@flackr wrote:
We ended up approximating this inverse scale with a generated linear animation with lots of keyframes but it could have been expressed as a single formula. Could supplying a formula solve some of these other express-ability use cases as well?
Mostly no. (Though having calc()
available as an option for easing functions is kind of tempting anyhow.) The problem is many functions don't have a (practical) formula representation -- including functions that result from cubic-bezier()
's (ab)use of Béziers!
The simplest general-purpose solution for representing arbitrary functions (including ones that can't be expressed as formulas) is to use a piecewise polynomial approximation.
Piecewise linear (degree 1) is one version of that, but as you discovered needs a lot of keyframes.
Piecewise cubic (degree 3), which we're discussing here, is kind of a sweet spot where you don't need that many extra keyframes, but it's still cheap to evaluate and can't go uncontrollably wiggly as can be a problem for degree >= 4.
But if we adopt something like the cubic spline syntax, the main question is can a tool like Greensock implement an algorithm to convert (with a reasonable degree of fidelity) their fixed-up path into the standard notation.
Apparently this isn't a requirement for Greensock. I chatted with the Greensock author about this today, and the way he explained it was instead of generating CSS and delegating to the browser, Greensock always drives animations from JavaScript.
Greensock has its own internal representation for easings that's optimized for efficiency vs perfect accuracy, and it always converts everything (CSS-style easings, SVG path easings, etc) to that internal representation before animating.
Probably exporting animations from Blender is a better model use case.
I had a thought this week -- knots in a spline are a lot like stops in a gradient, just with value+slope instead of colors.
...what if the syntax for splines worked like the syntax for gradients?
animation-timing-function: cubic-spline(<position> <speed> <time%>, ...);
So for example:
animation-timing-function: cubic-spline(0 0 0%, 0.1 auto 25%, 0.5 auto 50%, 0.9 auto 75%, 1 0 100%);
It probably makes sense to support some shorthands.
To start with, using the rules for gradient stops, if you just want equal spacing you can leave off the knot times/percentages:
animation-timing-function: cubic-spline(0 0, 0.1 auto, 0.5 auto, 0.9 auto, 1 0);
It'd probably also make sense to make speed optional (defaulting to auto
):
animation-timing-function: cubic-spline(0 0, 0.1, 0.5, 0.9, 1 0);
(Both of these give the same result as the original above.)
That feels pretty nice.
If we go with the same rules as for gradients, you can create abrupt changes by doubling up knots:
animation-timing-function: cubic-spline(0, 1 50%, 1 50%, 0);
animation-timing-function: cubic-spline(0.5, 1 50%, 0 50%, 0.5);
animation-timing-function: cubic-spline(1 0, 0 -3 50%, 0 3 50%, 1 0);
EBNF syntax for this idea might look something like:
<cubic-spline-easing-function> = cubic-spline( <cubic-spline-knot-list> )
<cubic-spline-knot-list> = <cubic-spline-knot> [, <cubic-spline-knot># ]?
<cubic-spline-knot> = <cubic-spline-knot-position> <cubic-spline-knot-speed>? <cubic-spline-knot-time>?
<cubic-spline-knot-position> = <number>
<cubic-spline-knot-speed> = auto | <number>
<cubic-spline-knot-time> = <percentage>
Also, here's a bounce easing I prepared by hand:
animation-timing-function: cubic-spline(0 0, 1 50%, 1 50%, 0.5, 1 75%, 1 75%, 0.75, 1 87.5%, 1 87.5%, 0.875, 1 93.75%, 1 93.75%, 0.9375, 1 96.8%, 1 96.8%, 0.968, 1 98.4%, 1 98.4%, 1);
Edit: Also, a handmade spring easing.
animation-timing-function: cubic-spline(0 0, 1.5 0 50%, 0.75 0 75%, 1.125 0 87.5%, 0.9375 0 93.75%, 1.031 0 96.8%, 0.984 0 98.4%, 1 0);
Those are neat diagrams and the effects you've produced look great. They seem to cover the different use cases well.
As someone who is not very familiar with gradients (I need to look it up every time) I don't find the parallel with gradient syntax particularly helpful. In particular, putting the "y" value (<cubic-spline-knot-position>
) before the "x" offset (<cubic-spline-knot-time>
) feels back-to-front compared to how I'm used to thinking with regards to keyframe offsets. However, that may be just me, and given that these easing functions may be used with gradients, aligning the syntax probably makes sense.
Currently all easing functions go from (0,0) to (1,1). I'm unsure if we should break that invariant or not (as this syntax currently allows). I seem to recall it had unfortunate implications in the realm of GroupEffect
s (where timing functions are effectively layered on top of one another) but perhaps it's ok.
Since it's already allowed in a more limited way, letting the dependent variable (progress) to go outside the 0..1 range shouldn't be much of a problem.
For the independent variable (time), though -- I'd imagined we'd just use the section of the timing function between 0 and 100%, regardless of where the first/final knots are, extrapolating past the end knots with straight lines if necessary.
animation-timing-function: cubic-spline(0 0 30%, 1 0 66%);
I'm not super attached to the gradient syntax, it just seemed like a nice opportunity for syntactic uniformity with the rest of CSS.
Since it's already allowed in a more limited way, letting the dependent variable (progress) to go outside the 0..1 range shouldn't be much of a problem.
Right, that part is fine.
For the independent variable (time), though -- I'd imagined we'd just use the section of the timing function between 0 and 100%, regardless of where the first/final knots are, extrapolating past the end knots with straight lines if necessary.
What I'm more concerned about is when f(0) != 0 or f(1) != 1. There certainly used to be places where we assumed that. With step-start you can already have f(0) != 0 but only when the "before flag" is false.
I recall that in the past that invariant proved useful but perhaps it's fine now. (It may have been when we were trying to chain timing functions together, or when we were trying to invert them in order to work out when to dispatch events -- but we don't do either of those things anymore).
What I'm more concerned about is when f(0) != 0 or f(1) != 1. There certainly used to be places where we assumed that. With step-start you can already have f(0) != 0 but only when the "before flag" is false.
I recall that in the past that invariant proved useful but perhaps it's fine now. (It may have been when we were trying to chain timing functions together, or when we were trying to invert them in order to work out when to dispatch events -- but we don't do either of those things anymore).
It'd be worth grounding out on whether there are any lingering issues with lifting this restriction, for both Gecko and WebKit. As long as the restriction is in place, there's a whole family of complex animations left that can't be expressed except the "long way", with multiple keyframes.
For example:
Without the requirement that f(0) == 0 and f(1) == 1, this animation could be expressed the same way as other complex animations (using the gradient-style strawman syntax):
@keyframes bouncy {
0% {
transform: translate(0px);
animation-timing-function: cubic-spline(0, 1, 0 33%, 0 33%, 0.33, 0 66%, 0 66%, 0.11, 0);
}
100% {
transform: translate(90px);
}
}
But, if the restriction were in force, for certain animations like this one you'd be forced to fall back to something like:
@keyframes bouncy {
0% {
transform: translate(0px);
animation-timing-function: cubic-spline(0, 1 0);
}
16% {
transform: translate(90px);
animation-timing-function: cubic-spline(0 0, 1);
}
33% {
transform: translate(0px);
animation-timing-function: cubic-spline(0, 1 0);
}
49% {
transform: translate(30px);
animation-timing-function: cubic-spline(0 0, 1);
}
66% {
transform: translate(0px);
animation-timing-function: cubic-spline(0, 1 0);
}
82% {
transform: translate(10px);
animation-timing-function: cubic-spline(0 0, 1);
}
100% {
transform: translate(0px);
}
}
(There's still a somewhat shorter way to write this, which I'll leave as an exercise for the reader because it requires a re-parameterization that's annoying to do by hand.)
@visiblecode How would you handle transitions, or filled animations, if the easing function doesn't reach the the target “end” value at the end time? Would the value jump suddenly to match?
@visiblecode How would you handle transitions, or filled animations, if the easing function doesn't reach the the target “end” value at the end time? Would the value jump suddenly to match?
Yes, similar to the current situation with some step easings.
Edit: For fills, I guess it might make sense to hold the last computed value to make animations like the above possible. Which would be different to the behavior for step.
Edit: For fills, I guess it might make sense to hold the last computed value to make animations like the above possible. Which would be different to the behavior for step.
Right, for fills it is already possible to fill at the mid-point of an interval by using animation-iteration-count: 0.5
for example.
That's an interesting point. I think we can get away without having any special fill behavior.
For use cases involving complex exported animations, multiple intermediate keyframes would be the norm anyhow.
The CSS Working Group just discussed easing timing functions
.
dbaron: nice pictures and few/no equations
I wasn't sure if spelling out the equations would be necessary at this stage. As mentioned upthread, the piecewise curves here are supposed to be Cubic Hermite splines. They are a standard thing and very closely related to Bezier curves.
Hermite splines can be given as a sequence of (t, p, m) triples, corresponding to the knots. In this context those correspond to (time, progress, velocity), with time and progress ranging between 0 and 1, apart from under/overshoot.
(n.b. the strawman CSS syntax from earlier gives t last, and as a percentage, just because it's trying to imitate CSS gradient syntax.)
I'll use t0 to mean the t from the first knot, t1 from the second knot, and so on...
For evaluating the curve in between knots, it's probably easiest to convert the segments to Bernstein form. (Bezier curves are made of polynomials in Bernstein form.)
Converting one Hermite segment, between knots n and n+1, to Bernstein form and re-parameterizing for the unit interval:
Δtn = tn+1 - tn
C0 = pn C1 = pn + mn Δtn / 3 C2 = pn+1 - mn+1 Δtn / 3 C3 = pn+1
Evaluating the converted segment for some t, using De Casteljau's algorithm, as is commonly done for Beziers:
lerp(a, b, x) = (1 - x) a + x b
tunit = (t - tn) / Δtn
B0 = lerp(C0, C1, tunit) B1 = lerp(C1, C2, tunit) B2 = lerp(C2, C3, tunit)
A0 = lerp(B0, B1, tunit) A1 = lerp(B1, B2, tunit)
result = lerp(A0, A1, tunit)
One non-standard thing is that I'm allowing zero length segments (where tn == tn+1) to represent abrupt changes in the curve; these trivial segments shouldn't get evaluated.
When I have a chance, I'll follow up on how to compute unspecified mn values (missing or auto
in the strawman syntax).
Missing mn values (slopes) can be worked out by solving a system of linear equations with an equation per knot.
If a knot's slope is already known, the equation is trivial:
mn = the slope
Otherwise, assuming:
Δtn = tn+1 - tn Δpn = pn+1 - pn
It's a choice between:
A. mn = 0 B. 2 mn / Δtn + mn+1 / Δtn = 3 Δpn / Δtn2 C. mn-1 / Δtn-1 + 2 mn / Δtn-1 = 3 Δpn-1 / Δtn-12 D. mn-1 / Δtn-1 + 2 mn (1 / Δtn-1 + 1 / Δtn) + mn+1 / Δtn = 3 * (Δpn-1 / Δtn-12 + Δpn / Δtn2)
Which equation to use depends on the knot's neighbors:
Previous Knot | Next Knot | Equation |
---|---|---|
none | none | A |
none | same t | A |
same t | none | A |
none | different t | B |
same t | different t | B |
different t | none | C |
different t | same t | C |
different t | different t | D |
This choice of equations amounts to doing standard spline interpolation for each unbroken section of curve with unknown slopes.
The tridiagonal matrix algorithm is a good fit for solving the resulting system of equations and is easy to implement.
Thanks for the equations, @visiblecode !
What did you think about turning this into a WICG proposal? That would have two benefits:
Myself and @argyleink have said we'd be able to help coordinate & deal with the formatting aspects of the proposal, but we'd need the IP contributions sorted out first.
Okay, I'm willing to give it a shot, though it may be a couple weeks before I can get back to you.
wanted to share for potential reference and relevance https://github.com/lunelson/split-ease
I personally prefer Jake Archibalds @jakearchibald easing-worklet proposal, it seems a little more straight forward, what do you guys think?
I wonder if it'd be even simpler to provide multiple points and just linear interpolate between them. You can produce almost any effect with enough points.
The more complex curves can land later, when folks figure them out.
Krill Vastletov discussed something similar in this article https://www.kirillvasiltsov.com/writing/how-to-create-a-spring-animation-with-web-animation-api/, if so we may not need to even finalize the api and can just start with custom made point easing today.
Yeah, you can hack it with keyframes today, but it'd be nice if it could be could be used within keyframes. Something like linear-easing(0, 0.1, 0.3, 0.6, 0.8, 1)
or whatever would be enough.
Yeah, you can hack it with keyframes today, but it'd be nice if it could be could be used within keyframes. Something like
linear-easing(0, 0.1, 0.3, 0.6, 0.8, 1)
or whatever would be enough.
Agreed, having to calculate interpolated keyframe values can be complicated depending on property values being interpolated and is destructive (i.e. your animation's original keyframes are now lost).
@birtles @grorg @smfr how do you feel about going forward with something like linear-easing(0, 0.1, 0.3, 0.6, 0.8, 1)
, which defines a set of easing points with linear interpolation in between?
I imagine the definitions of linear-easing
could get pretty long, with enough points to look smooth, but now we have custom properties they can be turned into a library.
It still leaves the door open for a curve-based solution like cubic-spline
.
@birtles @grorg @smfr how do you feel about going forward with something like
linear-easing(0, 0.1, 0.3, 0.6, 0.8, 1)
, which defines a set of easing points with linear interpolation in between?
I'd be in favour of that. Particularly for a < 1s animation I imagine it wouldn't require too many points to produce a convincing effect.
Here's a little demo to test that https://static-misc-3.glitch.me/linear-easing/.
Cases like bounce are tricky due to sudden changes of direction, but 50 points seems to do well.
I guess people would need to lean on SASS and LESS even harder to generate these series of points. It seems like something that could be evaluated would be better. Something like calc
being the precedent.
In light of the Webkit team's implementation of
spring()
, it's apparent we need to attend to the issue of complex timing functions sooner rather than later.The problem
Designers often need more advanced timing functions than can be described with cubic-beziers. They are not limited to spring functions, either. A common problem is there is no effective way to export a timing graph from Adobe After Effects to a timing function that could be used with CSS or with the Web Animations API. Currently designers have to hack together individual timing functions using CSS animation keyframes, which is impossible to do by hand in all but the most trifling instances.
spring()
is just a bandaid.The solution
We need a format to write functions like
spring()
in, one that we can export to from software like AfterEffects and prototyping tools that have yet to be built.I am not in a position to propose the technical specifications of this solution. But there are people who have that knowledge. I have invited them (@visiblecode) to share their proposals below.