【瞭解】貝塞爾曲線

曲線美

原理

命名:貝塞爾曲線(Bézier curve)

組成:由起點、終點、控制點組成。

說明:其中控制點的個數可以是0-n, 0個控制點的時候爲一階貝塞爾曲線(一條直線),1個控制點的時候爲二階貝塞爾曲線,以此類推。

重要性:是計算機圖形學中相當重要的參數曲線。

前身:伯恩斯坦多項式,德卡斯特里奧算法

由來:由法國工程師(數學家)皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來爲汽車的主體進行設計。

出發點:只需要很少的控制點,就可以繪製出一條平滑複雜的曲線。

曲線繪製過程

  • 一階貝塞爾曲線

  • 二階貝塞爾曲線

  • 三階貝塞爾曲線

  • 四階貝塞爾曲線

  • 高階階貝塞爾曲線

說明:在p0p1、p1p2、p2p3等等的起點到控制點再到終點的連線中,每段連線都被分割成了兩部分(仔細看動圖中的黑色、綠色、藍色圓點),各段連線中兩部分的比值都是相同的,比值範圍是0到1,而這個比值就是t

在線繪製,來源於 h5 canvas n 階貝塞爾曲線

數學知識(二階貝塞爾曲線爲例)

  • 步驟一:在平面內選3個不同線的點並且依次用線段連接

  • 步驟二:在AB和BC線段上找出點D和點E,使得 AD/AB = BE/BC

  • 步驟三:連接DE,在DE上尋找點F,F點需要滿足:DF/DE = AD/AB = BE/BC

  • 步驟四:最最重要的!

    • 上面三步是在講如何確定F點,DF/DE = AD/AB = BE/BC = t
    • 當 t 從 0-1 變化時,逆推出的所有 F 點連接起來,就繪製出了一條曲線

    P0 == A;P1 == B;P2 == C

  • 公式推導

    P點爲已知點,B點爲最終所求的點(上面圖所示的F點)。

    • 一階貝塞爾:B(t) = P0(1-t) + p1t

    • 二階貝塞爾:B(t) = P0(1-t)² + 2P1t(1-t) + P2t²

    • 三階貝塞爾:B(t) = P0(1-t)³ + 3P1t(1-t)² + 3P2t²(1-t) + P3t³

    • n階貝塞爾

個人理解

  • 一階貝塞爾曲線:一根直線
  • 二階至n階貝塞爾曲線:曲線
  • n 階貝塞爾曲線由 n+1 個點控制
  • 三階貝塞爾曲線應用最廣
  • 任何高階貝塞爾曲線,都可通過多個低階貝塞爾曲線組合而成
  • 二階只能繪製出一個彎曲的弧度,若要再加一個彎曲的弧度,方案有2:
    • 增加一階,使用高階
    • 兩個二階重複

瀏覽器中如何繪製

css

transition-timing-function:立方貝塞爾曲線(三階貝塞爾曲線)

cubic-bezier(x1, y1, x2, y2)
  • x1,y1 第一個控制點
  • x2,y2 第二個控制點
  • 默認起點 0,0 終點 1,1
transition: all 1s cubic-bezier(.25,.1,.25,1)

canvas

二階貝塞爾曲線:quadraticCurveTo

說明:quadratic: 二次方

語法:

// cpx,cpy 控制點
// x,y 結束點
context.quadraticCurveTo(cpx,cpy,x,y)

示例:

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(20,20);
ctx.quadraticCurveTo(20,100, 200,20);
ctx.stroke();

示例說明:

三階貝塞爾曲線:bezierCurveTo

語法:

// cp1x,cp1y 控制點1
// cp2x,cp2y 控制點
// x,y 結束點
// x,y 結束點
context.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y);

示例:

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(20,20);
ctx.bezierCurveTo(20,100, 200,100, 200,20);
ctx.stroke();

svg

利用 svg 的 path 標籤繪製。

path 標籤的 d 屬性中的 M 表示:moveTo

大寫表示絕對定位,小寫表示相對定位。

二階貝塞爾曲線:Q/q = quadratic Bézier curve

示例:M:起點,Q:兩個點

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="190px" height="160px">
  <path d="M20 20 Q 20,100, 200,20" stroke="orange" stroke-width="3" fill="none"/>
</svg>

三階貝塞爾曲線:C/c = curveto

示例:M:起點,C:三個點

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="190px" height="160px">
  <path d="M20 20 C20 100, 200 100, 200 20" stroke="orange" stroke-width="3" fill="none"/>
</svg>

組合:

  • Q(quadratic Bézier curve) + T(smooth quadratic Bézier curveto)
  • C(smooth curveto) + S(curveto)

說明:T,S 是在 Q、C 的基礎上,快速生成平滑曲線,且點的數量會減少一個

  • Q+T 示例:M:起點,Q:兩個點,T:一個點
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="300" height="100">
    <desc>二次貝塞爾平滑曲線</desc><defs></defs>
    <path d="M20 10 Q140 40 180 20 T280 30" stroke="orange" stroke-width="3" fill="none"></path>
</svg>
  • C+S 示例:M:起點,C:三個點,S:兩個點
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="300" height="100">
  <desc>三次貝塞爾平滑曲線</desc><defs></defs>
  <path d="M20 20 C90 40 130 40 180 20 S250 60 280 20" stroke="yellowgreen" stroke-width="3" fill="none"></path>
</svg>

webGl

容器是 canvas, 省略,感興趣的可自行查閱

css + js

background-image: paint(worklet-name);

<!-- 1: 容器 -->
<div class="bg"></div>

<!-- 2: css -->
<style>
  .bg {
    background: paint(workletBezier); 
    width: 100px;
    height: 100px;
  }
</style>

<!-- 3: -->
<script>
  if ('paintWorklet' in CSS) {
    // 必須是單獨的js
    CSS.paintWorklet.addModule('workletBezier.js');
  }
</script>

<!-- workletBezier.js -->
<script>
class WorkletBezier {
  paint(context, canvas, properties) {
    context.beginPath();
    context.moveTo(20,20);
    context.bezierCurveTo(20,100,200,100,200,20);
    context.strokeStyle = 'dodgerblue';
    context.lineWidth = 3;
    context.stroke();
  }
}

registerPaint('workletBezier', WorkletBezier);
</script>

css, canvas, svg 三階貝塞爾總結

  • css 起點、終點固定,只需兩個控制點
  • canvas、svg, 一個M(moveto,起點),加三個點(兩控制點,一結束點)

高階

高階利用上面的公式,求出一個個點,再把點連接起來(需要考慮性能、精度問題)。

優化,可利用低價繪製高階。

擴展

應用

1. 小球拋物線運動

        <style>
          .ball-wrap {
            position: relative;
          }
          .ball-outer {
            position: absolute;
            top: 30px;
            left: 27%;
            animation: parabola-x 1s linear infinite;
          }
          .ball {
            width: 15px;
            height: 15px;
            background: orange;
            border-radius: 50%;
            box-shadow: 0 0 2px 0 #000;
            animation: parabola-y 1s cubic-bezier(.55,0,.85,.36) infinite;
          }
          @keyframes parabola-x {
            0% {
              transform: translateX(0);
            }
            100% {
              transform: translateX(200px);
            }
          }
          @keyframes parabola-y {
            0% {
              transform: translateY(15px);
            }
            100% {
              transform: translateY(200px);
            }
          }
        </style>
        <div class="ball-outer">
          <div class="ball"></div>
        </div>

2. 水波圖

示例

3. 如何根據已知的點數據繪製出一條平滑的曲線?

    let data = [
      { "date": "2020-04-24", "value": 84 },
      { "date": "2020-04-25", "value": 150 },
      { "date": "2020-04-26", "value": 94 },
      { "date": "2020-04-27", "value": 40 },
      { "date": "2020-04-28", "value": 77 },
      { "date": "2020-04-29", "value": 99 },
      { "date": "2020-04-30", "value": 95 },
      { "date": "2020-05-01", "value": 72 },
      { "date": "2020-05-02", "value": 61 },
      { "date": "2020-05-03", "value": 125 },
      { "date": "2020-05-04", "value": 59 },
      { "date": "2020-05-05", "value": 200 },
      { "date": "2020-05-06", "value": 74 },
      { "date": "2020-05-07", "value": 76 },
      { "date": "2020-05-08", "value": 83 }
    ]

    const canvas = document.querySelector('#canvas');
    const ctx = canvas.getContext('2d');

    const w = canvas.width;
    const h = canvas.height;

    let pos = [];
    function createPos() {
      data.forEach((item, i) => {
        pos.push({
          x: (i + 1) * (w / (data.length + 1)),
          y: item.value
        })
      })
    }
    createPos();

    // 折線
    function drawLine() {
      pos.forEach((item, i) => {
        if (i < pos.length - 1) {
          const start = item;
          const end = pos[i + 1];

          // 線段
          ctx.beginPath();
          ctx.moveTo(start.x, start.y);
          ctx.lineTo(end.x, end.y);
          ctx.lineWidth = 1;
          ctx.lineJoin = 'round';
          ctx.strokeStyle = 'yellowgreen';
          ctx.stroke();
        }

        // 點
        ctx.beginPath();
        ctx.fillRect(item.x - 2, item.y - 2, 4, 4);
        ctx.fillStyle = 'black';
        ctx.fill();
        ctx.closePath();

        // 文本
        ctx.fillText(i, item.x - 2, item.y + 12);
      })
    }
    drawLine();
 
   function getMiddlePos(a, b) {
      return (a + b) / 2;
    }     
 
   function drawCurve() {
      ctx.moveTo((pos[0].x), pos[0].y);

      pos.forEach((item, i) => {
        if (i < pos.length - 1) {
          const a = pos[i];
          const b = pos[i + 1];
          const m = {
            x: getMiddlePos(a.x, b.x),
            y: getMiddlePos(a.y, b.y)
          }
          const ammx = getMiddlePos(a.x, m.x);
          const mbmx = getMiddlePos(m.x, b.x);

          ctx.quadraticCurveTo(ammx, a.y, m.x, m.y);
          ctx.quadraticCurveTo(mbmx, b.y, b.x, b.y);

          ctx.lineWidth = 1;
          ctx.strokeStyle = 'red';
          ctx.stroke();
        }
      })
    }
    drawCurve();

效果圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章