贝塞尔曲线

489 阅读1分钟

通过很少的控制点,去生成复杂的平滑曲线,也就是贝塞尔曲线。

学习CSS3中的 transition 及 animation 时遇到了 *-timing-function 属性,一直不甚了解,今天专门看了下,大概了解了其原理,只是归纳后的数学公式还不能理解(遗忘高中及大学里的相关知识)。

1 资源放送

transition-timing-function

animation-timing-function

cubic-bezier.com

BezierCurves

贝塞尔曲线-简书文章

2 历史-关键词

爸爸:法国工程师皮埃尔·贝济埃(Pierre Bézier)

正式生日:1962年

应用:图形设计、路径规划

原理:等比,递归

关联:伯恩斯坦多项式、德卡斯特里奥(de Casteljau)算法

3 推导

3.1 二阶贝塞尔

在平面内任选 3 个不共线的点,依次用线段连接。在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。

比值t,(0 <= t <= 1)

t = AD/AB

t = BE/BC DE上存在一点F,使得 DF/DE = t 将F点的轨迹描绘下来,即为下图红线

公式推导

D = t*A + (1-t)*B

E = t*B + (1-t)*C

F = t*D + (1-t)*E

= t*(t*A + (1-t)*B) + (1-t)*(t*B + (1-t)*C)

= t^2*A + 2*t*(1-t)*B + (1-t)^2*C

3.2 三阶贝塞尔

二阶的贝塞尔通过在控制点之间再采点的方式实现降阶, 每一次选点都是一次的降阶。

四个点对应是三次的贝塞尔曲线. 分别在 AB BC CD 之间采EFG点, EFG三个点对应着二阶贝塞尔, 在EF FG之间采集HI点来降阶为一阶贝塞尔曲线。

高阶的贝塞尔可以通过不停的递归直到一阶。

找到所有的J点,依次将J点连接起来

公式推导

E = t*A + (1-t)*B
F = t*B + (1-t)*C
G = t*C + (1-t)*D

H = t*E + (1-t)*F
I = t*F + (1-t)*G

J = t*H + (1-t)*I

= t*(t*E + (1-t)*F) + (1-t)*(t*F + (1-t)*G)

= t^2*(t*A + (1-t)*B) + t*(1-t)*(t*B + (1-t)*C) + t*(1-t)*(t*B + (1-t)*C) 
+ (1-t)^2*(t*C + (1-t)*D)

= t^3*A + 3*t^2*(1-t)*B + 3*t*(1-t)^2*C + (1-t)^3*D

3.3 三阶、四阶gif

4 贝塞尔曲线的性质

  • 各项系数之和为1

    系数是二项式的展开(t+(1-t))^n = (1)^n非负性

  • 对称性

    第i项系数和倒数第i项系数相同

  • 递归性

  • 凸包性质

    贝塞尔曲线始终会在包含了所有控制点的最小凸多边形中, 不是按照控制点的顺序围成的最小多边形

  • 端点性质

    第一个控制点和最后一个控制点,恰好是曲线的起始点和终点

  • 一阶导数性质

    这一点的性质可以用在贝塞尔曲线的拼接,只要保证三点一线中的中间点是两段贝塞尔曲线的连接点,就可以保证两端贝塞尔曲线的导数连续连续。

Bezier Curves Demo

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BezierCurves</title>
    <script src="https://www.w3cplus.com/sites/default/files/blogs/2017/1703/wind-all-0.7.3.js"></script>
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
        height: 100vh;
        width: 100vw;
      }
      body {
        display: flex;
        justify-content: center;
      }
      .wrapper {
        text-align: center;
      }
      small {
        font-size: 16px;
        margin-left: 20px;
      }
      p * {
        vertical-align: middle;
        margin-left: 5px;
      }

      .wrapper div {
        position: relative;
        box-shadow: 0 0 0 1px hsl(0, 0%, 80%);
        background: hsl(0, 0%, 95%);
        width: 800px;
        height: 600px;
      }
      canvas {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
      }
    </style>
  </head>
  <body>
    <div class="wrapper">
      <h1>
        Bézier Curve<small>(click the canvas to set the control points)</small>
      </h1>
      <p>
        Number of control points:
        <input type="range" min="2" max="30" value="3" disabled />
        <span></span>
      </p>
      <div id="canvasBox">
        <canvas
          id="canvasOne"
          width="800"
          height="600"
          style="z-index: 1"
        ></canvas>
        <canvas
          id="canvasTwo"
          width="800"
          height="600"
          style="z-index: 2"
        ></canvas>
        <canvas
          id="canvasThree"
          width="800"
          height="600"
          style="z-index: 3"
        ></canvas>
      </div>
    </div>
    <script>
      window.addEventListener("load", eventWindowLoaded, false);

      var Debugger = function () {};

      Debugger.log = function (message) {
        try {
          console.log(message);
        } catch (exception) {
          return;
        }
      };

      function eventWindowLoaded() {
        canvasApp();
      }

      function canvasApp() {
        // var theCanvas = document.getElementById('canvasOne');
        // var context = theCanvas.getContext('2d');

        Debugger.log("Drawing Canvas");

        var input = document.getElementsByTagName("input")[0],
          span = document.getElementsByTagName("span")[0],
          div = document.getElementById("canvasBox"),
          ctx1 = document.getElementById("canvasOne").getContext("2d"),
          ctx2 = document.getElementById("canvasTwo").getContext("2d"),
          ctx3 = document.getElementById("canvasThree").getContext("2d"),
          points = [],
          colors = [],
          running = true,
          steps = 200,
          interval = 16,
          num;

        ctx1.font = "16px consolas";
        ctx1.fillStyle = ctx1.strokeStyle = "hsl(0, 0%, 50%)";
        ctx1.lineWidth = ctx2.lineWidth = 2;
        ctx3.strokeStyle = "hsl(0, 90%, 70%)";

        function count() {
          num = parseInt(input.value);
          span.innerHTML = num;
        }

        function toggle() {
          input.disabled = running = !running;
        }

        function draw(per, arr, color) {
          var ary = [],
            node;

          ctx2.strokeStyle = ctx2.fillStyle = colors[color];
          node = arr.reduce(function (previous, current, index) {
            var p = {
              x: arr[index - 1].x + (arr[index].x - arr[index - 1].x) * per,
              y: arr[index - 1].y + (arr[index].y - arr[index - 1].y) * per,
            };

            if (index > 1) {
              ctx2.beginPath();
              ctx2.moveTo(previous.x, previous.y);
              ctx2.lineTo(p.x, p.y);
              ctx2.stroke();
              ctx2.closePath();
            }

            ctx2.beginPath();
            ctx2.arc(p.x, p.y, 3, 0, Math.PI * 2, true);
            ctx2.fill();
            ctx2.closePath();

            ary.push(p);
            return p;
          });

          if (ary.length > 1) {
            draw(per, ary, color + 1);
          } else {
            ctx3.lineTo(node.x, node.y);
            ctx3.stroke();
          }
        }

        var drawAsync = eval(
          Wind.compile("async", function () {
            toggle();
            ctx3.beginPath();
            ctx3.moveTo(points[0].x, points[0].y);
            for (var i = 0; i <= steps; i++) {
              draw(i / steps, points, 0);
              $await(Wind.Async.sleep(interval));
              ctx2.clearRect(0, 0, 800, 600);
            }
            ctx3.closePath();
            points = [];
            toggle();
          })
        );

        div.addEventListener(
          "click",
          function (e) {
            if (running) {
              return;
            }
            var point = {
              x: e.pageX - div.offsetLeft,
              y: e.pageY - div.offsetTop,
            };

            if (points.length == 0) {
              ctx1.clearRect(0, 0, 800, 600);
              ctx2.clearRect(0, 0, 800, 600);
              ctx3.clearRect(0, 0, 800, 600);
            } else {
              ctx1.beginPath();
              ctx1.moveTo(point.x, point.y);
              ctx1.lineTo(
                points[points.length - 1].x,
                points[points.length - 1].y
              );
              ctx1.stroke();
              ctx1.closePath();
            }

            ctx1.beginPath();
            ctx1.fillText(
              "[" + point.x + "," + point.y + "]",
              15,
              25 * (points.length + 1)
            );
            ctx1.arc(point.x, point.y, 4, 0, Math.PI * 2, true);
            ctx1.fill();
            ctx1.closePath();

            points.push(point);

            if (points.length == num) {
              drawAsync().start();
            }
          },
          false
        );

        input.addEventListener("change", count, false);

        for (var i = 0; i < parseInt(input.max); i++) {
          colors[i] = "hsl(" + 60 * (i + 1) + ", 60%, 60%)";
        }

        count();

        toggle();
      }
    </script>
  </body>
</html>