贝塞尔曲线
贝塞尔曲线用于计算机图形绘制形状,CSS 动画和许多其他地方。 它们其实非常简单,值得学习一次并且在矢量图形和高级动画的世界里非常受用。
- 控制点
- 数学 贝塞尔曲线可以使用数学方程式来描述。 很快我们就能看到 —— 没必要知道它。但是为了完整性 —— 请看这里。 给定控制点
Pi
的坐标:第一个控制点的坐标为P1 = (x1, y1)
,第二个控制点的坐标为P2 = (x2, y2)
,以此类推,曲线坐标由方程式描述,这个方程式依赖属于区间[0,1]
的参数t
。- 有两个控制点的曲线方程:
P = (1-t)P1 + tP2
- 有三个控制点的曲线方程:
P = (1−t)2P1 + 2(1−t)tP2 + t2P3
- 有四个控制点的曲线方程:
P = (1−t)3P1 + 3(1−t)2tP2 +3(1−t)t2P3 + t3P4
这些是矢量方程。 我们可以逐坐标重写它们,例如 3 点曲线: x = (1−t)2x1 + 2(1−t)tx2 + t2x3
y = (1−t)2y1 + 2(1−t)ty2 + t2y3
我们应该放置 3 个控制点的坐标,而不是x1、y1、x2、y2、x3 和 y3
。 例如,如果控制点是(0,0)
、(0.5, 1)
和(1, 0)
,则方程式为:x = (1−t)2 * 0 + 2(1−t)t * 0.5 + t2 * 1 = (1-t)t + t2 = t
y = (1−t)2 * 0 + 2(1−t)t * 1 + t2 * 0 = 2(1-t)t = –t2 + 2t
现在随着t
从0
到1
变化,每个t
对应的(x,y)
集合可以构成曲线。
- 有两个控制点的曲线方程:
- 德卡斯特里奥算法
- 德卡斯特里奥算法与曲线的数学定义相同,但直观地显示了曲线是如何被建立的。
- 如何通过给定点绘制曲线? 我们使用控制点制作贝塞尔曲线。正如我们所见,它们并不在曲线上。或者更准确地说,第一个和最后一个在曲线上,但其它的不在。 有时我们有另一种任务:绘制一条曲线通过几个点,让它们都在一条平滑曲线上。这种任务叫插值,这里我们不覆盖讲解它。 这些曲线有数学方程式,例如拉格朗日多项式。 在计算机图形中样条插值通常用于构建连接多个点的平滑曲线。
CSS 动画
- CSS 过渡(transition)[# css-transition]
- CSS 过渡的理念非常简单,我们只需要定义某一个属性以及如何动态地表现其变化。当属性变化时,浏览器将会绘制出相应的过渡动画。
- 也就是说:我们只需要改变某个属性,然后所有流畅的动画都由浏览器生成。
JavaScript.animated { transition-property: background-color; transition-duration: 3s; }
- 现在,只要一个元素拥有名为
.animated
的类,那么任何背景颜色的变化都会被渲染为 3 秒钟的动画。
html
<button id="color">Click me</button>
<style>
#color {
transition-property: background-color;
transition-duration: 3s;
}
</style>
<script>
color.onclick = function() {
this.style.backgroundColor = 'red';
};
</script>
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-timing-function
- 时间函数描述了动画进程在时间上的分布。它是先慢后快还是先快后慢?
- 这个属性接受两种值:一个贝塞尔曲线(Bezier curve)或者阶跃函数(steps)。我们先从贝塞尔曲线开始,这也是较为常用的。
- 贝塞尔曲线(Bezier curve)
- 时间函数可以用贝塞尔曲线描述,通过设置四个满足以下条件的控制点:
- CSS 中设置一贝塞尔曲线的语法为:
cubic-bezier(x2, y2, x3, y3)
。这里我们只需要设置第二个和第三个值,因为第一个点固定为(0,0)
,第四个点固定为(1,1)
。
- transition-property
- 其他
- CSS 提供几条内建的曲线:
linear
、ease
、ease-in
、ease-out
和ease-in-out
。 linear
其实就是cubic-bezier(0, 0, 1, 1)
的简写 —— 一条直线- 贝塞尔曲线可以使动画『超出』其原本的范围。
- CSS 提供几条内建的曲线:
- 阶跃函数(Steps)
- 时间函数
steps(number of steps[, start/end])
允许你让动画分段进行,number of steps
表示需要拆分为多少段。
- 时间函数
- transitionend 事件
- CSS 动画完成后,会触发
transitionend
事件。 - 这被广泛用于在动画结束后执行某种操作。我们也可以用它来串联动画。
transitionend
的事件对象有几个特定的属性:event.propertyName
:当前完成动画的属性,这在我们同时为多个属性加上动画时会很有用。event.elapsedTime
:动画完成的时间(按秒计算),不包括transition-delay
。
- 关键帧动画(Keyframes)
- 我们可以通过 CSS 提供的
@keyframes
规则整合多个简单的动画。 - 它会指定某个动画的名称以及相应的规则:哪个属性,何时以及何地渲染动画。然后使用
animation
属性把动画绑定到相应的元素上,并为其添加额外的参数。
- 我们可以通过 CSS 提供的
- CSS 动画完成后,会触发
html
<div class="progress"></div>
<style>
@keyframes go-left-right { /* 指定一个名字:"go-left-right" */
from { left: 0px; } /* 从 left: 0px 开始 */
to { left: calc(100% - 50px); } /* 移动至 left: 100%-50px */
}
.progress {
animation: go-left-right 3s infinite alternate;
/* 把动画 "go-left-right" 应用到元素上
持续 3 秒
持续次数:infinite
每次都改变方向
*/
position: relative;
border: 2px solid green;
width: 50px;
height: 20px;
background: lime;
}
</style>
JavaScript 动画
JavaScript 动画可以处理 CSS 无法处理的事情。例如,沿着具有与 Bezier 曲线不同的时序函数的复杂路径移动,或者实现画布上的动画。
- 使用 setInterval
- 从 HTML/CSS 的角度来看,动画是 style 属性的逐渐变化。例如,将
style.left
从0px
变化到100px
可以移动元素。 - 如果我们用
setInterval
每秒做 50 次小变化,看起来会更流畅。电影也是这样的原理:每秒 24 帧或更多帧足以使其看起来流畅。
- 从 HTML/CSS 的角度来看,动画是 style 属性的逐渐变化。例如,将
JavaScript
let start = Date.now(); // 保存开始时间
let timer = setInterval(function() {
// 距开始过了多长时间
let timePassed = Date.now() - start;
if (timePassed >= 2000) {
clearInterval(timer); // 2 秒后结束动画
return;
}
// 在 timePassed 时刻绘制动画
draw(timePassed);
}, 20);
// 随着 timePassed 从 0 增加到 2000
// 将 left 的值从 0px 增加到 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
- 使用 requestAnimationFrame
- 假设我们有几个同时运行的动画。如果我们单独运行它们,每个都有自己的
setInterval(..., 20)
,那么浏览器必须以比20ms
更频繁的速度重绘。 - 每个
setInterval
每20ms
触发一次,但它们相互独立,因此20ms
内将有多个独立运行的重绘。 - 这几个独立的重绘应该组合在一起,以使浏览器更加容易处理。
- 假设我们有几个同时运行的动画。如果我们单独运行它们,每个都有自己的
- 结构化动画
JavaScript
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);
}
});
}
- 时序函数
- n 次幂
JavaScriptfunction quad(timeFraction) { return Math.pow(timeFraction, 2) }
- 圆弧
JavaScriptfunction circ(timeFraction) { return 1 - Math.sin(Math.acos(timeFraction)); }
- 反弹:弓箭射击
- 此函数执行“弓箭射击”。首先,我们“拉弓弦”,然后“射击”。
- 与以前的函数不同,它取决于附加参数
x
,即“弹性系数”。“拉弓弦”的距离由它定义。
JavaScriptfunction back(x, timeFraction) { return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x); }
- 弹跳
JavaScriptfunction 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) } } }
- 伸缩动画
JavaScriptfunction 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
中:
- 在“easeOut”模式中,我们将
JavaScripttimingEaseOut(timeFraction) = 1 - timing(1 - timeFraction); // 接受时序函数,返回变换后的变体 function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } }
- easeInOut
JavaScriptif (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
。
- 除了移动元素,我们还可以做其他事情。我们所需要的只是写出合适的