Skip to content

贝塞尔曲线

贝塞尔曲线用于计算机图形绘制形状,CSS 动画和许多其他地方。 它们其实非常简单,值得学习一次并且在矢量图形和高级动画的世界里非常受用。

  1. 控制点
    1. 贝塞尔曲线由控制点定义。这些点可能有 2、3、4 个或更多。
    2. 控制点不总是在曲线上这是非常正常的。
      1. 曲线的阶次等于控制点的数量减一。 对于两个点我们能得到一条线性曲线(直线),三个点 — 一条二阶曲线,四个点 — 一条三阶曲线。
    3. 曲线总是在控制点的凸包内部:
    4. 贝塞尔曲线绘制的主要重点 —— 通过移动曲线,曲线以直观明显的方式变化。
  2. 数学 贝塞尔曲线可以使用数学方程式来描述。 很快我们就能看到 —— 没必要知道它。但是为了完整性 —— 请看这里。 给定控制点 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) 集合可以构成曲线。
  3. 德卡斯特里奥算法
    1. 德卡斯特里奥算法与曲线的数学定义相同,但直观地显示了曲线是如何被建立的。
  4. 如何通过给定点绘制曲线? 我们使用控制点制作贝塞尔曲线。正如我们所见,它们并不在曲线上。或者更准确地说,第一个和最后一个在曲线上,但其它的不在。 有时我们有另一种任务:绘制一条曲线通过几个点,让它们都在一条平滑曲线上。这种任务叫插值,这里我们不覆盖讲解它。 这些曲线有数学方程式,例如拉格朗日多项式。 在计算机图形中样条插值通常用于构建连接多个点的平滑曲线。

CSS 动画

  1. CSS 过渡(transition)[# css-transition]
    1. CSS 过渡的理念非常简单,我们只需要定义某一个属性以及如何动态地表现其变化。当属性变化时,浏览器将会绘制出相应的过渡动画。
    2. 也就是说:我们只需要改变某个属性,然后所有流畅的动画都由浏览器生成。
    JavaScript
    .animated {
      transition-property: background-color;
      transition-duration: 3s;
    }
  2. 现在,只要一个元素拥有名为 .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
  1. 一些属性
    1. transition-property
      1. 在 transition-property 中我们可以列举要设置动画的所有属性,如:left、margin-left、height 和 color
      2. 不是所有的 CSS 属性都可以使用过渡动画,但是它们中的大多数都是可以的。all 表示应用在所有属性上。
    2. transition-duration
      1. transition-duration 允许我们指定动画持续的时间。时间的格式参照 CSS 时间格式:单位为秒 s 或者毫秒 ms
    3. transition-delay
      1. transition-delay 允许我们设定动画开始前的延迟时间。例如,对于 transition-delay: 1s,动画将会在属性变化发生 1 秒后开始渲染。
    4. transition-timing-function
      1. 时间函数描述了动画进程在时间上的分布。它是先慢后快还是先快后慢?
      2. 这个属性接受两种值:一个贝塞尔曲线(Bezier curve)或者阶跃函数(steps)。我们先从贝塞尔曲线开始,这也是较为常用的。
    5. 贝塞尔曲线(Bezier curve)
      1. 时间函数可以用贝塞尔曲线描述,通过设置四个满足以下条件的控制点:
      2. CSS 中设置一贝塞尔曲线的语法为:cubic-bezier(x2, y2, x3, y3)。这里我们只需要设置第二个和第三个值,因为第一个点固定为 (0,0),第四个点固定为 (1,1)
  2. 其他
    1. CSS 提供几条内建的曲线:lineareaseease-inease-out 和 ease-in-out
    2. linear 其实就是 cubic-bezier(0, 0, 1, 1) 的简写 —— 一条直线
    3. 贝塞尔曲线可以使动画『超出』其原本的范围。
  3. 阶跃函数(Steps)
    1. 时间函数 steps(number of steps[, start/end]) 允许你让动画分段进行,number of steps 表示需要拆分为多少段。
  4. transitionend 事件
    1. CSS 动画完成后,会触发 transitionend 事件。
    2. 这被广泛用于在动画结束后执行某种操作。我们也可以用它来串联动画。
    3. transitionend 的事件对象有几个特定的属性:
      1. event.propertyName :当前完成动画的属性,这在我们同时为多个属性加上动画时会很有用。
      2. event.elapsedTime :动画完成的时间(按秒计算),不包括 transition-delay
    4. 关键帧动画(Keyframes)
      1. 我们可以通过 CSS 提供的 @keyframes 规则整合多个简单的动画。
      2. 它会指定某个动画的名称以及相应的规则:哪个属性,何时以及何地渲染动画。然后使用 animation 属性把动画绑定到相应的元素上,并为其添加额外的参数。
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 曲线不同的时序函数的复杂路径移动,或者实现画布上的动画。

  1. 使用 setInterval
    1. 从 HTML/CSS 的角度来看,动画是 style 属性的逐渐变化。例如,将 style.left 从 0px 变化到 100px 可以移动元素。
    2. 如果我们用 setInterval 每秒做 50 次小变化,看起来会更流畅。电影也是这样的原理:每秒 24 帧或更多帧足以使其看起来流畅。
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';
	}
  1. 使用 requestAnimationFrame
    1. 假设我们有几个同时运行的动画。如果我们单独运行它们,每个都有自己的 setInterval(..., 20),那么浏览器必须以比 20ms 更频繁的速度重绘。
    2. 每个 setInterval 每 20ms 触发一次,但它们相互独立,因此 20ms 内将有多个独立运行的重绘。
    3. 这几个独立的重绘应该组合在一起,以使浏览器更加容易处理。
  2. 结构化动画
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);
    }

  });
}
  1. 时序函数
    1. n 次幂
    JavaScript
    function quad(timeFraction) {
      return Math.pow(timeFraction, 2)
    }
    1. 圆弧
    JavaScript
    	function circ(timeFraction) {
    	  return 1 - Math.sin(Math.acos(timeFraction));
    	}
    1. 反弹:弓箭射击
      1. 此函数执行“弓箭射击”。首先,我们“拉弓弦”,然后“射击”。
      2. 与以前的函数不同,它取决于附加参数 x,即“弹性系数”。“拉弓弦”的距离由它定义。
    JavaScript
    	function back(x, timeFraction) {
    	  return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x);
    	}
    1. 弹跳
    JavaScript
    	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)
    	    }
    	  }
    	}
    1. 伸缩动画
    JavaScript
    	function elastic(x, timeFraction) {
    	  return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
    	}
    1. 逆转:ease*
      1. 我们有一组时序函数。它们的直接应用称为“easeIn”。
      2. 有时我们需要以相反的顺序显示动画。这是通过“easeOut”变换完成的。
    2. easeOut
      1. 在“easeOut”模式中,我们将 timing 函数封装到 timingEaseOut中:
    JavaScript
    	timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction);
    	// 接受时序函数,返回变换后的变体
    	function makeEaseOut(timing) {
    	  return function(timeFraction) {
    	    return 1 - timing(1 - timeFraction);
    	  }
    	}
    1. easeInOut
    JavaScript
    	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);
  2. 更有趣的 “draw”
    1. 除了移动元素,我们还可以做其他事情。我们所需要的只是写出合适的 draw