Skip to content

[css-easing-2] Complex easing/timing functions #229

Description

@nearestnabors

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.

Activity

  1. birtles commented on Jun 24, 2016

    @birtles
    Contributor

    There is some previous discussion of this topic, thread starting here: https://lists.w3.org/Archives/Public/public-fx/2015JulSep/0034.html

  2. visiblecode commented on Jun 24, 2016

    @visiblecode

    Cubic beziers aren't a bad choice for timing functions. They're very easy to evaluate (just a few additions and multiplications). Also, most animation tools use some form of cubic spline to specify animation paths, which can be converted into cubic bezier easings with good fidelity.

    There are two problems with cubic-bezier() as it is currently specced, however:

    1. You can't specify a spline of multiple segments
    2. cubic-bezier() is abusing 2d parametric curves to specify 1d functions

    The first issue can be addressed by, essentially, just adding more points to the cubic-bezier() syntax. I believe this has already been proposed in various forms.

    The second issue is pretty significant, though, and deserves some unpacking. cubic-bezier() is specified as a 2d bezier, a 2d parametric curve. This has a lot of implications, including:

    1. Without additional constraints, the resulting curve isn't guaranteed to have a singular y value for a given x and can't be used to define a mathematical function. This has been dealt with by restricting x to the range [0, 1], but that doesn't actually address the root (no pun) problem -- because a cube root is involved there are still two extra solutions in the complex plane.
    2. As mentioned, implementors must solve cube roots to work backwards from x to the implicit t parameter, before they get to the normal (and much simpler/exact!) bezier calculation to evaluate the easing function.
    3. Tool authors who want to do a direct conversion of cubic spline segments to cubic-bezier()s have no use at all for the extra dimension, but must clutter the output with decimal approximations of 1/3 and 2/3.

    The extra dimension isn't useful and makes life harder for implementors and tool authors alike. Ideally, instead of having to specify a two-dimensional curve like cubic-bezier(0.33333, 0.4, 0.66666, 0.6), we ought to have gotten one-dimensional curves, e.g. cubic-bezier(0.4, 0.6).

    With one dimension, instead of having to work backwards from (normalized) time to an implicit t, the normalized time could be used directly as the input without having to solve cube roots first.

    I guess if I were going to make a concrete proposal, I'd like to see syntax for specifying 1d bezier splines, in a format something like cubic-spline(c, [c,t,c,]* c) (where the cs and ts are numbers).

    The first and last values are exactly like the control points in cubic-bezier(), except since we're only dealing with one dimension, the "control points" are specified by scalar values, rather than pairs of them.

    Between those end "control values", you could have zero or more (control value, time, control value) triples representing intermediate knots with their adjacent control "points". The knot times (the middle value of the triples) would have to be monotonically increasing, in the range (0, 1).

    As a concrete example, let's take the following complex easing:

     @keyframes example {
         from {
             some-property: 100px;
             animation-timing-function: cubic-bezier(0.33333, 0.25, 0.66666, 0.8);
         }
         50% {
             some-property: 75px;
             animation-timing-function: cubic-bezier(0.33333, 0.5, 0.66666, 0.6);
         }
         to {
             some-property: 50px;
         }
     }
    

    Given the aforementioned cubic-spline() function, you could define the same animation more simply as:

     @keyframes example {
         from {
             some-property: 100px;
             animation-timing-function: cubic-spline(0.125, 0.4,0.5,0.75, 0.8);
         }
         to {
             some-property: 50px;
         }
     }
    

    IMO this seems better for both export tooling and hand authoring.

    One thing this proposal doesn't address are use cases where you want easings/animations with sudden jumps in them. (i.e. not C0 continuous) Having multiple intermediate CSS keyframes may still be a good answer for animations with discontinuities (rather than trying to define timing functions with discontinuities), though in order to do that there would need to be a mechanism to specify different incoming and outgoing property values for a CSS keyframe.

  3. visiblecode commented on Jun 24, 2016

    @visiblecode

    One of the things that bothers me about spring() specifically is that it requires finding an approximate solution to a differential equation, just like cubic-bezier() requires approximate solving of cube roots (only moreso). Browsers' approximations will vary, and there are performance/quality tradeoffs.

    Using (1d) bezier splines for defining easing functions is far simpler than something like spring() computationally, and is easy to pull off with good precision. It's also relatively easy for tools to "bake" a physically-based animation (including ones much more complex than spring simulations) into a spline, which animation tools do anyway for better reproduceability and performance.

  4. visiblecode commented on Jun 24, 2016

    @visiblecode

    Thinking about it overnight, cubic-polybezier() might be a more accurate/less misleading name than cubic-spline(). I'll let other people bikeshed about naming though.

  5. visiblecode commented on Jun 24, 2016

    @visiblecode

    I'm also an idiot; those triples need to be four-tuples, there's an extra position parameter which I left out. So that example/proposal is wrong.

  6. visiblecode commented on Jun 24, 2016

    @visiblecode

    Adding the missing position parameter to the two control values, the syntax would be something like cubic-spline(c, [c, t, p, c,]* c). And that example becomes:

    @keyframes {
        from {
            some-property: 100px;
            animation-timing-function: cubic-spline(0.125, 0.4, 0.5,0.5, 0.75, 0.8);
        }
        to {
            some-property: 50px;
        }
    }
    
  7. visiblecode commented on Jul 1, 2016

    @visiblecode

    Having thought about this for a while and chatted with others, I think the minimum required features for representing complex easing functions boil down to:

    Piecewise cubics are pretty much the common denominator for animation software, which either uses some form of piecewise cubic spline directly, or else curves which are reasonably easy to convert to piecewise cubics.

    The main exception are animation tools which are doing physical simulations. Even then, some simple physical systems can be directly represented. For example, piecewise cubics are more than enough to precisely represent physically-accurate "bouncing ball" easings, since ballistic trajectories are simple parabolic arcs.

    Oscillating spring (or pendulum) easings are sinusoidal and not exactly representable using simple polynomials, but you can still do a good job of approximating them by gluing multiple cubic segments together. If spring() became part of the standard, it might make sense to define it in terms of an appropriate piecewise cubic approximation.

    I don't know how often jump continuities are really needed in easing functions, but they're certainly required if you want to be able to define a single easing function to recreate existing complex @keyframes animations which use timing functions like step-start() or step-end(). So it seems worth including them.

  8. visiblecode commented on Jul 1, 2016

    @visiblecode

    It's probably worth observing that motion curves (as opposed to timing/easing curves) in animation software are a little bit of a different story. There you're more likely to find NURBS or other more sophisticated types of curves which require a bit more work to approximate with cubic segments.

    On the 1d versus 2d issue -- From what I can see so far, After Effects uses one-dimensional piecewise functions for timing curves, although I don't have a copy to play with directly. However, poking at the implementation of Blender, it appears Blender f-curve segments do work similarly to cubic-bezier(), using (suitably constrained) 2d bezier curves to define the 1d function.

  9. visiblecode commented on Jul 1, 2016

    @visiblecode

    Additional observation: while you can muddle through with multiple keyframes and cubic-bezier() easings for most purposes, if you need smooth curves joined by a jump discontinuity, @keyframes as currently specified can't be made to work. Best you can currently do to simulate such a jump discontinuity is to define two keyframes very close together and use a step easing between them:

    @keyframes example {
        from {
            blah: 10px;
            animation-timing-function: cubic-bezier( ... stuff ... );
        }
        49.99999% {
            blah: 40px;
            animation-timing-function: step-end;
        }
        50% {
            blah: 100px;
            animation-timing-function: cubic-bezier( ... stuff ...);
        }
        to {
            blah: 150px;
        }
    }
    
  10. birtles commented on Jul 1, 2016

    @birtles
    Contributor

    Best you can currently do to simulate such a jump discontinuity is to define two keyframes very close together and use a step easing between them

    Yes, this is actually quite common to see. The Web Animations API allows you to overlap keyframe offsets so that you can add discontinuities and I believe there has been discussion in the past of adding syntax to allow you to do this in CSS keyframes (e.g. 50%+).

  11. visiblecode commented on Jul 1, 2016

    @visiblecode

    One suggestion made in the Slack discussion, which I had promised to document here, was to use an SVG path to specify an easing function.

    In that case, any SVG path could be allowed, at least provided it ranged between 0 and 1 in the x dimension, and the curve's x component was continuous and monotonic over that range as well. In that case, the M operator could used to indicate jump discontinuities.

    So, for two cubic segments joined by a jump discontinuity 50% of the way through, you might have something like: animation-timing-function: path("M 0,0 C 0.17,0.1, 0.33,0.2 0.5,0.4 M 0.5,0.8 C 0.67,0.8 0.83,0.9, 1,1")

    I have mixed feelings about this, but it would work, aside from the issue of needing to indicate whether you wanted left- or right- continuity at any jumps.

  12. visiblecode commented on Jul 1, 2016

    @visiblecode

    The idea I had floated prior to that was something like: animation-timing-function: complex-easing(cubic(0, 0.1, 0.2, 0.4), 0.5, cubic(0.8, 0.8, 0.9, 1))

    (In spite of cubic() having a superficial resemblance to cubic-bezier(), the parameters are all y values.)

    i.e. two one-dimensional cubic bezier segments which meet at x = 0.5, but the first ends at y = 0.4, and the second begins at y = 0.8. Still doesn't address the issue of directional continuity though.

    It does also require you to repeat the y/output value even when you want the two segments to join with C0 continuity. For example if the first segment ended at y = 0.4, and the second began there:

    animation-timing-function: complex-easing(cubic(0, 0.1, 0.2, 0.4), 0.5, cubic(0.4, 0.8, 0.9, 1))

    This also wouldn't allow for a direct translation of blender f-curves, while I think SVG paths would.

  13. 90 remaining items

  14. birtles commented on Jun 15, 2021

    @birtles
    Contributor

    @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.

  15. jakearchibald commented on Jun 15, 2021

    @jakearchibald
    Contributor

    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.

  16. benlesh commented on Jun 15, 2021

    @benlesh

    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.

  17. jakearchibald commented on Jun 15, 2021

    @jakearchibald
    Contributor

    @benlesh I disagree for the reasons I gave above. Eg this as a library:

    :root {
      --ease-bounce: linear-easing(0, 0.004, 0.01, 0.03, 0.06, 0.09, 0.13, 0.18, 0.24, 0.3, 0.37, 0.45, 0.54, 0.63, 0.7, 0.8, 1, 0.963, 0.91, 0.86, 0.83, 0.8, 0.77, 0.76, 0.751, 0.7508, 0.758, 0.77, 0.79, 0.82, 0.86, 0.91, 0.96, 0.99, 0.97, 0.95, 0.94, 0.938, 0.943, 0.96, 0.98, 1, 0.99, 0.984, 0.988, 1);
    }

    …could be used like this:

    .whatever {
      transition: transform 1s var(--ease-bounce);
    }

    Even with cubic-bezier, most folks are copy-pasting something from https://easings.net/ rather than authoring their own.

    Trying to add unnecessary complexity will stall this again. The more complex curve-based solutions can come later.

  18. benlesh commented on Jun 15, 2021

    @benlesh

    As long as this solution isn't in lieu of pursuing a more flexible – if more complex – solution, I guess I don't see any problem with it. (Not that you needed my permission, haha. I just mean my concern above is invalid)

  19. chriskirknielsen commented on Jun 15, 2021

    @chriskirknielsen

    @jakearchibald I definitely like the proposal. Have you given thought to a different syntax, closer to clip-path's polygon() with a comma separating each XY pair? I don't know of the implications but I know that the syntax has made it easier for me to debug paths (well, that and/or line breaks I suppose).

    I know Amelia mentioned how folks with math backgrounds might prefer x1,y1 x2,y2 but since we have the polygon() precedent, I'd imagine it'd be more familiar. Or maybe make commas optional though I think it's nice to, at a glance, know if you're missing a coordinate in a pair.

  20. jakearchibald commented on Jun 15, 2021

    @jakearchibald
    Contributor

    Have you given thought to a different syntax, closer to clip-path's polygon() with a comma separating each XY pair?

    The issues with this are pretty well documented in this thread. Starting #229 (comment)

  21. jakearchibald commented on Aug 20, 2021

    @jakearchibald
    Contributor

    I tried to start a PR for this, but I'm not entirely sure what I'm doing when it comes to CSS specs #6533.

  22. lukejagodzinski commented on Dec 1, 2021

    @lukejagodzinski

    What about taking inspiration from video editors https://youtu.be/xM9w0SSpgtc?t=289? If it worked for them it would work for CSS as well.

  23. jakearchibald commented on Dec 1, 2021

    @jakearchibald
    Contributor

    The door remains open for other easing syntax in future, but I don't think we should avoid shipping a solution for another 5 years in search of some holy grail solution.

  24. okikio commented on Jan 21, 2022

    @okikio

    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.

    @jakearchibald Inspired by your demo I created a spring-easing generator, it supports multiple variants of spring, and multiple libraries, e.g. gsap, animejs, WAAPI, linear-easing(...) (or whatever it's called once it's finalized), and more... (you can check out a demo on Codepen and Stackblitz).

    IMO, we should call the easing function either discrete, linear, or smooth, I really don't think we're going to find a name that perfectly matches, so, as long as the core details (as well as any related issues) are worked out, it should be good.

  25. jakearchibald commented on Jul 17, 2022

    @jakearchibald
    Contributor

    I've opened #7508 to look into curve-based custom easings.

  26. tomasdev commented on Feb 20, 2024

    @tomasdev

    @jakearchibald reviving this old thread (thanks @argyleink for pointing me here) -- there's a question I have around linear() being "enough"

    Suppose you want a smooth animation at 60fps, a non-approximation would mean having 60 points inside linear. That is only if you want a one second animation. Factor in higher FPS displays and/or longer animations and you end up bloating the CSS pretty fast.

    Would it be possible to re-open this issue, in favor of discussing the "longer term" (#229 (comment)) solution that solves for interpolation / proper bezier curves?

  27. jakearchibald commented on Feb 20, 2024

    @jakearchibald
    Contributor

    @tomasdev I don't think your assessment is correct. Check out this example. According to your maths, this would need 480 points to be smooth, but 76 here seems more than enough. For a 1.5s animation, 35 points seems more than enough.

    That said, as per my previous comment, I opened #7508 for exploring curves.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions