贝塞尔曲线
贝塞尔曲线用于计算机图形绘制形状,CSS 动画和许多其他地方。
控制点
贝塞尔曲线由控制点定义。
- 控制点不总是在曲线上这是非常正常的,稍后我们将看到曲线是如何构建的。
- 曲线的阶次等于控制点的数量减一。 对于两个点我们能得到一条线性曲线(直线),三个点 — 一条二阶曲线,四个点 — 一条三阶曲线。
- 曲线总是在控制点的凸包内部:
由于最后一个属性,在计算机图形学中,可以优化相交测试。如果凸包不相交,则曲线也不相交。因此,首先检查凸包的交叉点可以非常快地给出“无交叉”结果。检查交叉区域或凸包更容易,因为它们是矩形,三角形等(见上图),比曲线简单的多
数学
贝塞尔曲线可以使用数学方程式来描述。
...
德卡斯特里奥算法
德卡斯特里奥算法与曲线的数学定义相同,但直观地显示了曲线是如何被建立的。
德卡斯特里奥算法构造三点贝塞尔曲线:
- 绘制控制点。在上面的演示中,它们标有:
1
、2
和3
。 - 创建控制点 1 → 2 → 3 间的线段. 在上面的演示中它们是棕色的。
- 参数
t
从0
to1
变化。 在上面的演示中取值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
从0
to1
变化,每一个t
的值都会添加一个点到曲线上。这些点的集合就形成的贝塞尔曲线。它在上面的图中是红色的,并且是抛物线状的。
对于 4 个点
算法:
- 控制点通过线段连接:1 → 2、2 → 3 和 3 → 4。 我们能得到 3 条棕色的线段。
- 对于
0
to1
之间的每一个t
:- 我们在这些线段上距起点距离比例为
t
的位置取点。把这些点连接起来,然后得到两条绿色线段。 - 在这些线段上同样按比例
t
取点,得到一条蓝色线段。 - 在蓝色线段按比例
t
取点。在上面的例子中是红色的。
- 我们在这些线段上距起点距离比例为
- 这些点在一起组成了曲线。
该算法是递归的,并且可以适应于任意数量的控制点。
给定 N 个控制点,我们将它们连接起来以获得初始的 N-1 个线段。
然后对从0
到1
的每一个t
: - 在每条线段上按
t
比例距离取一个点并且连接 —— 会得到 N-2 个线段。 - 在上面得到的每条线段上按
t
比例距离取一个点并且连接 —— 会得到 N-3 个线段,以此类推…… - 直到我们得到一个点。得到的这些点就形成了曲线。
CSS 动画
CSS 动画可以在不借助 Javascript 的情况下做出一些简单的动画效果。
transition
CSS 提供了四个属性来描述一个过渡:
transition-property
transition-duration
transition-timing-function
transition-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
。