贝塞尔曲线
贝塞尔曲线用于计算机图形绘制形状,CSS 动画和许多其他地方。
控制点
贝塞尔曲线由控制点定义。
- 控制点不总是在曲线上这是非常正常的,稍后我们将看到曲线是如何构建的。
- 曲线的阶次等于控制点的数量减一。 对于两个点我们能得到一条线性曲线(直线),三个点 — 一条二阶曲线,四个点 — 一条三阶曲线。
- 曲线总是在控制点的凸包内部:
由于最后一个属性,在计算机图形学中,可以优化相交测试。如果凸包不相交,则曲线也不相交。因此,首先检查凸包的交叉点可以非常快地给出“无交叉”结果。检查交叉区域或凸包更容易,因为它们是矩形,三角形等(见上图),比曲线简单的多
数学
贝塞尔曲线可以使用数学方程式来描述。
...
德卡斯特里奥算法
德卡斯特里奥算法与曲线的数学定义相同,但直观地显示了曲线是如何被建立的。
德卡斯特里奥算法构造三点贝塞尔曲线:
- 绘制控制点。在上面的演示中,它们标有:
1、2和3。 - 创建控制点 1 → 2 → 3 间的线段. 在上面的演示中它们是棕色的。
- 参数
t从0to1变化。 在上面的演示中取值0.05:循环遍历0, 0.05, 0.1, 0.15, ... 0.95, 1。
对于每一个t的取值:- 在每一个棕色线段上我们取一个点,这个点距起点的距离按比例
t取值。由于有两条线段,我们能得到两个点
例如,当t=0— 所有点都在线段起点处,当t=0.25— 点到起点的距离为线段长度的 25%,当t=0.5— 50%(中间),当t=1— 线段终点。 - 连接这些点,下面这张图中连好的线被绘制成蓝色。

- 在每一个棕色线段上我们取一个点,这个点距起点的距离按比例
- 现在在蓝色线段上取一个点,距离比例取相同数值的
t。也就是说,当t=0.25(左图)时,我们取到的点位于线段的左 1/4 终点处,当t=0.5(右图)时 — 线段中间。在上图中这一点是红色的。 - 随着
t从0to1变化,每一个t的值都会添加一个点到曲线上。这些点的集合就形成的贝塞尔曲线。它在上面的图中是红色的,并且是抛物线状的。
对于 4 个点
算法:
- 控制点通过线段连接:1 → 2、2 → 3 和 3 → 4。 我们能得到 3 条棕色的线段。
- 对于
0to1之间的每一个t:- 我们在这些线段上距起点距离比例为
t的位置取点。把这些点连接起来,然后得到两条绿色线段。 - 在这些线段上同样按比例
t取点,得到一条蓝色线段。 - 在蓝色线段按比例
t取点。在上面的例子中是红色的。
- 我们在这些线段上距起点距离比例为
- 这些点在一起组成了曲线。
该算法是递归的,并且可以适应于任意数量的控制点。
给定 N 个控制点,我们将它们连接起来以获得初始的 N-1 个线段。
然后对从0到1的每一个t: - 在每条线段上按
t比例距离取一个点并且连接 —— 会得到 N-2 个线段。 - 在上面得到的每条线段上按
t比例距离取一个点并且连接 —— 会得到 N-3 个线段,以此类推…… - 直到我们得到一个点。得到的这些点就形成了曲线。
CSS 动画
CSS 动画可以在不借助 Javascript 的情况下做出一些简单的动画效果。
transition
CSS 提供了四个属性来描述一个过渡:
transition-propertytransition-durationtransition-timing-functiontransition-delay
transition-property
在 transition-property 中我们可以列举要设置动画的所有属性,如:left、margin-left、height 和 color。
不是所有的 CSS 属性都可以使用过渡动画,但是它们中的大多数都是可以的。all 表示应用在所有属性上。
transition-duration
transition-duration 允许我们指定动画持续的时间。时间的格式参照 CSS 时间格式:单位为秒 s 或者毫秒 ms。
transition-delay
transition-delay 允许我们设定动画开始前的延迟时间。例如,对于 transition-delay: 1s,动画将会在属性变化发生 1 秒后开始渲染。
你也可以提供一个负值。那么动画将会从整个过渡的中间时刻开始渲染。例如,对于 transition-duration: 2s,同时把 delay 设置为 -1s,那么这个动画将会持续 1 秒钟,并且从正中间开始渲染。
负值有时可以简化js动画代码
transition-timing-function
时间函数描述了动画进程在时间上的分布。它是先慢后快还是先快后慢?
这个属性接受两种值:一个贝塞尔曲线(Bezier curve)或者阶跃函数(steps)。我们先从贝塞尔曲线开始,这也是较为常用的。
贝塞尔曲线
时间函数可以用贝塞尔曲线描述,通过设置四个满足以下条件的控制点:
- 第一个应为:
(0,0)。 - 最后一个应为:
(1,1)。 - 对于中间值,
x必须位于0..1之间,y可以为任意值。
CSS 中设置一贝塞尔曲线的语法为:cubic-bezier(x2, y2, x3, y3)。这里我们只需要设置第二个和第三个值,因为第一个点固定为(0,0),第四个点固定为(1,1)。
时间函数描述了动画进行的快慢。
x轴表示时间:0—— 开始时刻,1——transition-duration的结束时刻。y轴表示过程的完成度:0—— 属性的起始值,1—— 属性的最终值。
CSS 提供几条内建的曲线:linear、ease、ease-in、ease-out和ease-in-out。
贝塞尔曲线可以使动画『超出』其原本的范围。
曲线上的控制点的y值可以使任意的:不管是负值还是一个很大的值。如此,贝塞尔曲线就会变得很低或者很高,让动画超出其正常的范围。
但是,如何针对特定的任务寻找到合适的贝塞尔曲线呢?事实上,有很多工具可以帮到你。比方说,我们可以利用这个网站:http://cubic-bezier.com/。
阶跃函数
时间函数 steps(number of steps[, start/end]) 允许你让动画分段进行,number of steps 表示需要拆分为多少段。
steps 的第一个参数表示段数。时间间隔也会以同样的方式被拆分
第二个参数可以取 start 或 end 两者其一。
start 表示在动画开始时,我们需要立即开始第一段的动画。
另一个值 end 表示:改变不应该在最开始的时候发生,而是发生在每一段的最后时刻。
另外还有一些简写值:
step-start—— 等同于steps(1, start)。即:动画立刻开始,并且只有一段。也就是说,会立刻开始,紧接着就结束了,宛如没有动画一样。step-end—— 等同于steps(1, end)。即:在transition-duration结束时生成一段动画。
transitionend 事件
CSS 动画完成后,会触发 transitionend 事件。
这被广泛用于在动画结束后执行某种操作。我们也可以用它来串联动画。
动画通过 go 函数初始化,并且在每次动画完成后都会重复执行,并转变方向:
boat.onclick = function() {
//...
let times = 1;
function go() {
if (times % 2) {
// 向右移动
boat.classList.remove('back');
boat.style.marginLeft = 100 * times + 200 + 'px';
} else {
// 向左移动
boat.classList.add('back');
boat.style.marginLeft = 100 * times - 200 + 'px';
}
}
go();
boat.addEventListener('transitionend', function() {
times++;
go();
});
};
transitionend 的事件对象有几个特定的属性:
event.propertyName :当前完成动画的属性,这在我们同时为多个属性加上动画时会很有用。
event.elapsedTime :动画完成的时间(按秒计算),不包括 transition-delay。
关键帧动画
我们可以通过 CSS 提供的 @keyframes 规则整合多个简单的动画。
它会指定某个动画的名称以及相应的规则:哪个属性,何时以及何地渲染动画。然后使用 animation 属性把动画绑定到相应的元素上,并为其添加额外的参数。
JavaScript 动画
JavaScript 动画可以处理 CSS 无法处理的事情。
使用 setInterval
从 HTML/CSS 的角度来看,动画是 style 属性的逐渐变化。例如,将 style.left 从 0px 变化到 100px 可以移动元素。
如果我们用 setInterval 每秒做 50 次小变化,看起来会更流畅。电影也是这样的原理:每秒 24 帧或更多帧足以使其看起来流畅。
使用 requestAnimationFrame
假设我们有几个同时运行的动画。
如果我们单独运行它们,每个都有自己的 setInterval(..., 20),那么浏览器必须以比 20ms 更频繁的速度重绘。
每个 setInterval 每 20ms 触发一次,但它们相互独立,因此 20ms 内将有多个独立运行的重绘。
这几个独立的重绘应该组合在一起,以使浏览器更加容易处理。
换句话说,像下面这样:
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
……比这样更好:
setInterval(animate1, 20);
setInterval(animate2, 20);
setInterval(animate3, 20);
还有一件事需要记住。有时当 CPU 过载时,或者有其他原因需要降低重绘频率。例如,如果浏览器选项卡被隐藏,那么绘图完全没有意义。
有一个标准动画时序提供了 requestAnimationFrame 函数。
它解决了所有这些问题,甚至更多其它的问题。
语法:
let requestId = requestAnimationFrame(callback);
这会让 callback 函数在浏览器每次重绘的最近时间运行。
如果我们对 callback 中的元素进行变化,这些变化将与其他 requestAnimationFrame 回调和 CSS 动画组合在一起。因此,只会有一次几何重新计算和重绘,而不是多次。
返回值 requestId 可用来取消回调:
// 取消回调的周期执行
cancelAnimationFrame(requestId);
callback 得到一个参数 —— 从页面加载开始经过的毫秒数。这个时间也可通过调用 performance.now() 得到。
通常 callback 很快就会运行,除非 CPU 过载或笔记本电量消耗殆尽,或者其他原因。
下面的代码显示了 requestAnimationFrame 的前 10 次运行之间的时间间隔。通常是 10-20ms:
<script>
let prev = performance.now();
let times = 0;
requestAnimationFrame(function measure(time) {
document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
prev = time;
if (times++ < 10) requestAnimationFrame(measure);
});
</script>
结构化动画
现在我们可以在 requestAnimationFrame 基础上创建一个更通用的动画函数:
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction 从 0 增加到 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// 计算当前动画状态
let progress = timing(timeFraction);
draw(progress); // 绘制
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
animate 函数接受 3 个描述动画的基本参数:
duration
动画总时间,比如 1000。
timing(timeFraction)
时序函数,类似 CSS 属性 transition-timing-function,传入一个已过去的时间与总时间之比的小数(0 代表开始,1 代表结束),返回动画完成度(类似 Bezier 曲线中的 y)。
draw(progress)
获取动画完成状态并绘制的函数。值 progress = 0 表示开始动画状态,progress = 1 表示结束状态。
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
时序函数
上文我们看到了最简单的线性时序函数。
n 次幂
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
增大幂会让动画加速得更快。
圆弧
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
反弹:弓箭射击
与以前的函数不同,它取决于附加参数 x,即“弹性系数”。“拉弓弦”的距离由它定义。
function back(x, timeFraction) {
return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x);
}
弹跳
想象一下,我们正在抛球。球落下之后,弹跳几次然后停下来。
function bounce(timeFraction) {
for (let a = 0, b = 1, result; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
伸缩动画
另一个“伸缩”函数接受附加参数 x 作为“初始范围”。
function elastic(x, timeFraction) {
return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}
逆转 ease*
我们有一组时序函数。它们的直接应用称为“easeIn”。
有时我们需要以相反的顺序显示动画。这是通过“easeOut”变换完成的。
easeOut
在“easeOut”模式中,我们将 timing 函数封装到 timingEaseOut中:
timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction);
换句话说,我们有一个“变换”函数 makeEaseOut,它接受一个“常规”时序函数 timing 并返回一个封装器,里面封装了 timing 函数:
// 接受时序函数,返回变换后的变体
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
easeInOut
我们还可以在动画的开头和结尾都显示效果。该变换称为“easeInOut”。
给定时序函数,我们按下面的方式计算动画状态:
if (timeFraction <= 0.5) { // 动画前半部分
return timing(2 * timeFraction) / 2;
} else { // 动画后半部分
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
封装器代码:
function makeEaseInOut(timing) {
return function(timeFraction) {
if (timeFraction < .5)
return timing(2 * timeFraction) / 2;
else
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
}
bounceEaseInOut = makeEaseInOut(bounce);
更有趣的 "draw"
除了移动元素,我们还可以做其他事情。我们所需要的只是写出合适的 draw。
