Skip to main content

Principles of Curved Path Animation

Free2017-01-07#JS#CSS曲线动画#CSS轨迹动画#CSS沿曲线运动#JS曲线动画#JS抛物线动画

Animation libraries generally provide various easing effects, but achieving curved motion requires a little trick.

1. Animation Functions

Animation is displacement as a function of time: s = f(t)

The independent variable is t, and the dependent variable is s. The displacement of the object changes over time, which appears as animation. For example:

// 已知
var property = 'marginLeft';
var s0 = 100;   // 起点
var s1 = 200;   // 终点
var duration = 1000;

// 由题意得
var S = s1 - s0;    // 总位移
var T = duration;   // 总时间

// 求任意时刻t对应的位移
var t0 = +new Date();
var tick, interval = 1000 / 60;
setTimeout(tick = function() {
    var t = +new Date() - t0;
    // 完成度
    var p = Math.min(t / T, 1);
    // t时刻相对起点的位移s
    var s = S * p;
    document.body.style[property] = s0 + s + 'px';

    if (p !== 1) setTimeout(tick, interval);
}, interval);

marginLeft changes uniformly from 100px to 200px. The body first jumps 100px to the right, and then moves 100px to the right at a constant speed within 1 second.

To implement such an animation, the only problem is: given the total displacement S and total time T, find the displacement s at any time t relative to the starting point.

We have implemented uniform linear motion. It might seem like s = vt wasn't used, but it actually was:

s = v * t
  = (S / T) * t
  = S * (t / T)
  = S * p

Since the animation function is s = f(t) and doesn't contain v, we need to replace v with known quantities. Because the completion percentage is p = t / T, animation is also a function of displacement relative to completion percentage.

2. Uniformly Accelerated/Decelerated Motion

Similarly, by replacing v and a in the displacement formulas for uniformly accelerated/decelerated motion, we get the function for displacement s relative to time t.

Uniform Acceleration

Displacement formula:

// v0 = 0时,只有一个未知量a
s = 1/2at^2

Given total time T, total displacement S, and completion percentage p = t / T, find displacement s at any time t relative to the starting point:

// 终点处有
S = 1/2 * a * T^2
// 得
a = 2S / T^2
// 任意时刻
s = 1/2 * a * t^2
  = 1/2 * (2S / T^2) * t^2
  = 1/2 * 2S * (t^2 / T^2)
  = S * p^2

Uniform Deceleration

Displacement formula:

// 含有2个未知量v0和a
s = v0t - 1/2at^2

Given total time T, total displacement S, and completion percentage p = t / T, find displacement s at any time t relative to the starting point:

// 1.逆向匀加速求v0
// 起点处有
S = 1/2 * a * T^2
// 得
a = 2S / T^2
v0 = aT = 2S / T^2 * T = 2S / T
// 2.任意时刻
s = v0 * t - 1/2 * a * t^2
  = (2S / T) * t - 1/2 * (2S / T^2) * t^2
  = 2S * (t / T) - S * (t^2 / T^2)
  = 2S * p - S * p^2
  = S * p * (2 - p)

3. Curved Motion

Simple curved motion can be decomposed into linear motions. For example, the sine function y = sinx can be decomposed into:

// x轴匀速直线
x = S * p = 2PI * p
// y轴sinx
y = sinx = sin(2PI * p)

Horizontal projectile motion can be decomposed into:

// x轴匀速直线
x = S * p = X * p
// y轴匀加速
y = S * p^2 = Y * p^2

The left half of a parabola can be seen as the reverse motion of a leftward horizontal projectile:

// x轴匀速直线
x = S * p = X * p
// y轴匀减速
y = S * p * (2 - p) = Y * p * (2 - p)

Circular motion is slightly more unique. With the algebraic equation (x - a)^2 + (y - b)^2 = r^2, calculating x and y involves handling positive and negative signs (which is cumbersome but feasible). Therefore, consider using parametric equations:

// 对于圆上任意一点,有
sinθ = y / r, cosθ = x / r
// 得参数方程
x = a + r * cosθ, y = b + r * sinθ

// 圆心为(0, 0)时
x = r * cosθ = r * cos(θ * p), y = r * sinθ = r * sin(θ * p)

The angle [0, 2PI] changes uniformly, and x and y change with the angle.

P.S. Explaining circular motion using polar coordinates is somewhat far-fetched (r(θ) = r, setting a center, and then rotating uniformly through [0, 360]. How would positions be calculated in an era without transform?)

4. Easing Functions

Comparing the formulas obtained above:

s = S * p               // 匀速
s = S * p^2             // 匀加速
s = S * p * (2 - p)     // 匀减速
s = S * cos(2PI * p)    // cos
s = S * sin(2PI * p)    // sin

Notice that the total displacement S remains constant while the subsequent part differs. Therefore:

var easings = {
    linear: function(p) { return p; },
    acceleration: function(p) { return p * p; },
    deceleration: function(p) { return p * (2 - p); },
    sin: function(p) { return Math.sin(2 * Math.PI * p); },
    cos: function(p) { return Math.cos(2 * Math.PI * p); }
}

These easing functions are used to adjust p, so the animation should be:

// 任意时刻t对应的位移
st = s0 + S * easing(p)
// 即
// 当前值 = 初始值 + totalDelta * easing函数修正后的完成度

Since p = t / T, easing actually acts on t, and is also called a timing function (timingFunction).

Animation libraries all work this way, for example, jQuery:

// from https://github.com/jquery/jquery/blob/2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/effects/Tween.js
jQuery.easing = {
    linear: function( p ) {
        return p;
    },
    swing: function( p ) {
        return 0.5 - Math.cos( p * Math.PI ) / 2;
    },
    _default: "swing"
};

velocity:

// from https://github.com/ayqy/velocity-1.4.1/blob/master/velocity.js
Velocity.Easings = {
    linear: function(p) {
    // 线性,直接返回完成度
        return p;
    },
    swing: function(p) {
    // 两头慢中间快,cos从+1到-1变化,中间斜率最大变化最快
        return 0.5 - Math.cos(p * Math.PI) / 2;
    },
    /* Bonus "spring" easing, which is a less exaggerated version of easeInOutElastic. */
    spring: function(p) {
    // easeInOutElastic的温和版
        return 1 - (Math.cos(p * 4.5 * Math.PI) * Math.exp(-p * 6));
    }
};

Other complex easing effects, such as friction (spring), gravity (bounce), and other physical effects, common timing easing series (easing functions corresponding to various Bezier curves), and step effects follow the same principle: adjusting the completion percentage, which is essentially speed control.

5. Online Demo

Implementing these curved paths by customizing easing and Redirects in velocity, for example:

// 自定义缓动函数
// 匀加速
Velocity.Easings.acceleration = function (p, opts, tweenDelta) {
    return p * p;
};
// 匀减速
Velocity.Easings.deceleration = function (p, opts, tweenDelta) {
    return p * (2 - p);
};

// 自定义动画效果
Velocity.Redirects['throw-h'] = function(element, options, elementsIndex, elementsSize, elements, promiseData) {
    Velocity(this, {
        translateX: [300, 'linear', 0],
        translateY: [300, 'acceleration', 0]
    }, options);
};

// run
Velocity(document.body, 'throw-h', 3000);

See the Demo for details: http://ayqy.net/temp/curve-path-animation.html

During the demo, I discovered that velocity forcibly assigns the end point value when the completion percentage is 1. This is problematic in scenarios like sin. The source code is as follows:

else if (percentComplete === 1) {
// 已完成,手动赋值,确保终点准确(不受计算精度影响)
//!!! 不应该手动赋值终点了,因为sin之类的,终点是0
//!!! 强制赋值就错了
    /* If this is the last tick pass (if we've reached 100% completion for this tween),
     ensure that currentValue is explicitly set to its target endValue so that it's not subjected to any rounding. */
    // currentValue = tween.endValue;
    currentValue = tween.currentValue;
}

The fix is to trust the easing function (simply remove the part above) and calculate the current value at the end point through the easing function as well. The downside is potential calculation precision issues; for example, sin at 2PI is 0, but the calculation might yield a very small value instead of 0. However, this has no practical impact.

References

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment