2

When looping a zoom animation a linear timing will not feel linear but rather slow at the start and fast at the end.

How would you go about calculating the correct curve? If you need more info ask in the comments.

In this pen I use a hand drawn curve to try to fix the issue, since I am clearly too stupid to figure out the logic.

http://codepen.io/pixelass/pen/qZjBQy

// mathematically correct curve ?
@function linear-zoom() {
  // add your code here
  $sx: 0;
  $sy: 0;
  $ex: 1;
  $ey: 1;
  $curve: ($sx, $sy, $ex, $ey);
  @return $curve;
}

zoom hack

  • what's wrong with hand-tweaking the parameters and eyeballing whether that looks good? That's literally the only way to get something that "feels right" - maths can't tell your what humans in general experience as "the" smoothest looking traversal (in fact, different people'll probably give different answers if you were to A/B test this) – Mike 'Pomax' Kamermans Mar 25 '16 at 16:54
  • @Mike'Pomax'Kamermans I think I am mostly interested in the logic behind it. –  Mar 25 '16 at 17:34
  • Seen http://cubic-bezier.com? Or, if you want a *lot* of logic, https://pomax.github.io/bezierinfo? – Mike 'Pomax' Kamermans Mar 25 '16 at 19:27
  • @Mike'Pomax'Kamermans We know how bezier curves work. The question (I think, and what I want to know) is why does it seem to speed up with a linear zoom when thats a constant alteration. And can the bezier curve that fixs this be calculated by math instead of hand/eye....you try doing it by hand, spent over an hour and I still didnt get it (well done pixelass ;) – PAEz Mar 26 '16 at 05:03

1 Answers1

2

The scale parameters you're using are multiples of two, so when you use linear animation speed the scaling increases linearly, but distances increase logarithmically.

Let's look at some numbers. First, a circle that we scale from 0.5 to 2, so two log2 steps:

t=0    => scale(0.5)
t=0.25 => scale((0.75 * 0.5 + 0.25 * 2)) = scale(0.875)
t=0.5  => scale((0.5 * 0.5 + 0.5 * 2)) = scale(1.25)
t=0.75 => scale((0.25 * 0.5 + 0.75 * 2)) = scale(1.625)
t=1    => scale(2)

So that looks great, linear transition between start and end. We might think that means we can add more circles to do the same, but if we do we end up with logarithmic behaviour. Let's look at why: let's say we add one that scales 0.25 to 1.

t=0    => scale(0.25)
t=0.25 => scale((0.75 * 0.25 + 0.25)) = scale(0.4375)
t=0.5  => scale((0.5 * 0.25 + 0.5)) = scale(0.625)
t=0.75 => scale((0.25 * 0.25 + 0.75)) = scale(0.8125)
t=1    => scale(1)

That seems good: at every time interval, the big circle's radius is twice the smaller circle... but that's not what you want, because that also means that a point on the smaller circle only moves 0.75 units in the time it takes a point on the larger circle to move twice that distance (1.5 units). We see that the larger a circle starts at, the faster it moves across the screen, following a simple 2^t formula.

To counteract this you want to compensate for the specific logarithmic growth, independent for each animation due to the different parameters. Getting a single Bezier curve to do that for all curves is mathematically impossible, but since humans aren't perfect maths machines, you can get away with it on a small bit of screen like your codepen - to do it "properly" for any start/end set of parameters a and b you need to set up a Bezier curve that approximates the function 1/(2^t) over the interval t=[a,b], which isn't much fun: we need to approximate the series for 1/2^x up to order 2 on specific intervals with a Bezier curve, and then use the coordinates that gives us as cubic easing parameters.

There's two ways you can do this - either find the one Bezier curve that fits the function best from your globally smallest to largest value, and then use curve splitting to find the parameters for each subinterval, or find Bezier fits for each separate interval. The first is (marginally) easier but error prone due to the low order series approximation of 1/2^x, the second is better, but quite a lot of work, so if you want to find out how to do this math.stackexchange.com is a better place for getting to the true mathematical bottom here.

The "Stackoverflow answer" that most programmers will be able to work with is to build yourself a Lookup Table of "some curves for some intervals" and then use interpolations between those to deal with intervals that fall somewhere in between the intervals you used to build the LUT: plot the intervals with whichever language you have lying around, and then just use something like cubic-bezier.com with that plot overlaid and simply find "a good fit" for each of those plots by eyeballing the control points. Do that for, say, ten of the intervals you use, and you're good to go.

This is less maths for sure, and you can't "program" that solution as a generic solution, but it is efficient: from a user perception perspective, this solution should already be more than good enough to feel smooth.

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
  • 1
    Or simply multiply ( or divide ) by 0.97 repeatedly. – Arif Burhan Mar 26 '16 at 18:25
  • 1
    @ArifBurhan while correct, not super useful in finding the Bezier fit for each interval, since you're still going to have to approximate *that* function (CSS, for reasons that I don't really understand, has no custom easing functions. If it did, this would be *a lot* easier. All you'd need is an `(t,a,b) => 1/(2^lerp(t,a,b))` number generator) – Mike 'Pomax' Kamermans Mar 26 '16 at 18:30
  • individual lerps will let you come up with a decent Bezier, but you might want to do a plot of control points needed to approximate specific intervals (giving you two lines through which they need to pass) and then seeing if you can easily express the functions for them in terms of the interval start value. If so, that's a pretty solid way to always get the right parameters, too. – Mike 'Pomax' Kamermans Mar 27 '16 at 17:38