天天看點

【數學篇】07 # 如何用向量和參數方程描述曲線?

說明

【跟月影學可視化】學習筆記。

如何用向量描述曲線?

用向量繪制折線的方法來繪制正多邊形

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>如何用向量描述曲線</title>
    <style>canvas {
            border: 1px dashed salmon;
        }</style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
    <script type="module">import { Vector2D } from './common/lib/vector2d.js';

        const canvas = document.querySelector('canvas');
        const ctx = canvas.getContext('2d');
        const {width, height} = canvas;
        ctx.translate(0.5 * width, 0.5 * height);
        ctx.scale(1, -1);
        /**
         * 邊數 edges
         * 起點 x, y
         * 一條邊的長度 step
         * */
        function regularShape(edges = 3, x, y,) {
            const ret = [];
            const delta = Math.PI * (1 - (edges - 2) / edges);
            let p = new Vector2D(x, y);
            const dir = new Vector2D(step, 0);
            ret.push(p);
            for(let i = 0; i < edges; i++) {
                p = p.copy().add(dir.rotate(delta));
                ret.push(p);
            }
            return ret;
        }
        function draw(points, strokeStyle = 'salmon', fillStyle = null) {
            ctx.strokeStyle = strokeStyle;
            ctx.beginPath();
            ctx.moveTo(...points[0]);
            for(let i = 1; i < points.length; i++) {
                ctx.lineTo(...points[i]);
            }
            ctx.closePath();
            if(fillStyle) {
                ctx.fillStyle = fillStyle;
                ctx.fill();
            }
            ctx.stroke();
        }

        draw(regularShape(3, 128, 128, 100));  // 繪制三角形
        draw(regularShape(6, -64, 128, 50));  // 繪制六邊形
        draw(regularShape(11, -64, -64, 30));  // 繪制十一邊形
        draw(regularShape(60, 128, -64, 6));  // 繪制六十邊形</script>
</body>
</html>      
【數學篇】07 # 如何用向量和參數方程描述曲線?

如何用參數方程描述曲線?

1. 畫圓

圓可以用一組參數方程來定義。定義了一個圓心在(x0,y0),半徑為 r 的圓。

【數學篇】07 # 如何用向量和參數方程描述曲線?
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>畫圓</title>
    <style>canvas {
            border: 1px dashed salmon;
        }</style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
    <script>const canvas = document.querySelector('canvas');
        const ctx = canvas.getContext('2d');
        const {width, height} = canvas;
        ctx.translate(0.5 * width, 0.5 * height);
        ctx.scale(1, -1);

        
        const TAU_SEGMENTS = 60;
        const TAU = Math.PI * 2;
        function arc(x0, y0, radius, startAng = 0, endAng = Math.PI * 2) {
            const ang = Math.min(TAU, endAng - startAng);
            const ret = ang === TAU ? [] : [[x0, y0]];
            const segments = Math.round(TAU_SEGMENTS * ang / TAU);
            for(let i = 0; i <= segments; i++) {
                const x = x0 + radius * Math.cos(startAng + ang * i / segments);
                const y = y0 + radius * Math.sin(startAng + ang * i / segments);
                ret.push([x, y]);
            }
            return ret;
        }

        function draw(points, strokeStyle = 'salmon', fillStyle = null) {
            ctx.strokeStyle = strokeStyle;
            ctx.beginPath();
            ctx.moveTo(...points[0]);
            for(let i = 1; i < points.length; i++) {
                ctx.lineTo(...points[i]);
            }
            ctx.closePath();
            if(fillStyle) {
                ctx.fillStyle = fillStyle;
                ctx.fill();
            }
            ctx.stroke();
        }

        draw(arc(0, 0, 100));</script>
</body>
</html>      
【數學篇】07 # 如何用向量和參數方程描述曲線?

2. 畫圓錐曲線

橢圓

a、b 分别是橢圓的長軸和短軸,當 a = b = r 時,這個方程是就圓的方程式。

【數學篇】07 # 如何用向量和參數方程描述曲線?
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>橢圓</title>
    <style>canvas {
            border: 1px dashed salmon;
        }</style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
    <script>const canvas = document.querySelector('canvas');
        const ctx = canvas.getContext('2d');
        const {width, height} = canvas;
        ctx.translate(0.5 * width, 0.5 * height);
        ctx.scale(1, -1);

        
        const TAU_SEGMENTS = 60;
        const TAU = Math.PI * 2;
        
        function ellipse(x0, y0, radiusX, radiusY, startAng = 0, endAng = Math.PI * 2) {
            const ang = Math.min(TAU, endAng - startAng);
            const ret = ang === TAU ? [] : [[x0, y0]];
            const segments = Math.round(TAU_SEGMENTS * ang / TAU);
            for(let i = 0; i <= segments; i++) {
                const x = x0 + radiusX * Math.cos(startAng + ang * i / segments);
                const y = y0 + radiusY * Math.sin(startAng + ang * i / segments);
                ret.push([x, y]);
            }
            return ret;
        }

        function draw(points, strokeStyle = 'salmon', fillStyle = null) {
            ctx.strokeStyle = strokeStyle;
            ctx.beginPath();
            ctx.moveTo(...points[0]);
            for(let i = 1; i < points.length; i++) {
                ctx.lineTo(...points[i]);
            }
            ctx.closePath();
            if(fillStyle) {
                ctx.fillStyle = fillStyle;
                ctx.fill();
            }
            ctx.stroke();
        }

        draw(ellipse(0, 0, 100, 50));</script>
</body>
</html>      
【數學篇】07 # 如何用向量和參數方程描述曲線?

抛物線

抛物線的參數方程。其中 p 是常數,為焦點到準線的距離。

【數學篇】07 # 如何用向量和參數方程描述曲線?
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>抛物線</title>
    <style>canvas {
            border: 1px dashed salmon;
        }</style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
    <script>const canvas = document.querySelector('canvas');
        const ctx = canvas.getContext('2d');
        const {width, height} = canvas;
        ctx.translate(0.5 * width, 0.5 * height);
        ctx.scale(1, -1);
        
        const LINE_SEGMENTS = 60;
        function parabola(x0, y0, p, min,) {
            const ret = [];
            for(let i = 0; i <= LINE_SEGMENTS; i++) {
                const s = i / 60;
                const t = min * (1 - s) + max * s;
                const x = x0 + 2 * p * t ** 2;
                const y = y0 + 2 * p * t;
                ret.push([x, y]);
            }
            return ret;
        }

        function draw(points, strokeStyle = 'salmon', fillStyle = null) {
            ctx.strokeStyle = strokeStyle;
            ctx.beginPath();
            ctx.moveTo(...points[0]);
            for(let i = 1; i < points.length; i++) {
                ctx.lineTo(...points[i]);
            }
            ctx.closePath();
            if(fillStyle) {
                ctx.fillStyle = fillStyle;
                ctx.fill();
            }
            ctx.stroke();
        }

        draw(parabola(0, 0, 5.5, -10, 10));</script>
</body>
</html>      
【數學篇】07 # 如何用向量和參數方程描述曲線?

3. 畫其他常見曲線

在 lib 下面建立一個 ​

​parametric.js​

​ 檔案,封裝一個更簡單的 JavaScript 參數方程繪圖子產品。

// 根據點來繪制圖形
function draw(
    points,
    context,
    { strokeStyle = "salmon", fillStyle = null, close = false } = {}
) {
    context.strokeStyle = strokeStyle;
    context.beginPath();
    context.moveTo(...points[0]);
    for (let i = 1; i < points.length; i++) {
        context.lineTo(...points[i]);
    }
    if (close) context.closePath();
    if (fillStyle) {
        context.fillStyle = fillStyle;
        context.fill();
    }
    context.stroke();
}

// 導出高階函數繪圖子產品
export function parametric(xFunc, yFunc,) {
    /**
     * start、end 表示參數方程中關鍵參數範圍的參數
     * seg 表示采樣點個數的參數,當 seg 預設 100 時,就表示在 start、end 範圍内采樣 101(seg+1)個點
     * ...args 後續其他參數是作為常數傳給參數方程的資料。
     * */ 
    return function (start, end, seg = 100, ...args) {
        const points = [];
        for (let i = 0; i <= seg; i++) {
            const p = i / seg;
            const t = start * (1 - p) + end * p;
            const x = xFunc(t, ...args); // 計算參數方程組的x
            const y = yFunc(t, ...args); // 計算參數方程組的y
            if (zFunc) {
                points.push(zFunc(x, y));
            } else {
                points.push([x, y]);
            }
        }
        return {
            draw: draw.bind(null, points),
            points, // 生成的頂點資料
        };
    };
}      

下面使用上面封裝的實作一下抛物線,阿基米德螺旋線,星形線。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>畫其他常見曲線</title>
        <style>canvas {
                border: 1px dashed salmon;
            }</style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script type="module">import { parametric } from "./common/lib/parametric.js";

            const canvas = document.querySelector("canvas");
            const ctx = canvas.getContext("2d");
            const { width, height } = canvas;
            const w = 0.5 * width,
                h = 0.5 * height;
            ctx.translate(w, h);
            ctx.scale(1, -1);

            // 繪制坐标軸
            function drawAxis() {
                ctx.save();
                ctx.strokeStyle = "#ccc";
                ctx.beginPath();
                ctx.moveTo(-w, 0);
                ctx.lineTo(w, 0);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(0, -h);
                ctx.lineTo(0, h);
                ctx.stroke();
                ctx.restore();
            }
            drawAxis();

            // 繪制抛物線
            const para = parametric(
                (t) => 25 * t,
                (t) => 25 * t ** 2
            );
            para(-5.5, 5.5).draw(ctx);

            // 繪制阿基米德螺旋線
            const helical = parametric(
                (t,) => l * t * Math.cos(t),
                (t,) => l * t * Math.sin(t)
            );
            helical(0, 50, 500, 5).draw(ctx, { strokeStyle: "MediumPurple" });

            // 繪制星形線
            const star = parametric(
                (t,) => l * Math.cos(t) ** 3,
                (t,) => l * Math.sin(t) ** 3
            );
            star(0, Math.PI * 2, 50, 150).draw(ctx, { strokeStyle: "Orange" });</script>
    </body>
</html>      
【數學篇】07 # 如何用向量和參數方程描述曲線?

4. 畫貝塞爾曲線

貝塞爾曲線是一種使用數學方法描述的曲線,被廣泛用于計算機圖形學和動畫中。在矢量圖中,貝塞爾曲線用于定義可無限放大的光滑曲線。可以用來建構 Catmull–Rom 曲線。

貝塞爾曲線又分為二階貝塞爾曲線(Quadratic Bezier Curve)和三階貝塞爾曲線(Qubic Bezier Curve)。

二階貝塞爾曲線

二階貝塞爾曲線由三個點确定,P0是起點,P1是控制點,P2是終點

【數學篇】07 # 如何用向量和參數方程描述曲線?

二階貝塞爾曲線的原理示意圖

【數學篇】07 # 如何用向量和參數方程描述曲線?

繪制30條從圓心出發,旋轉不同角度的二階貝塞爾曲線

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>二階貝塞爾曲線</title>
        <style>canvas {
                border: 1px dashed salmon;
            }</style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script type="module">import { parametric } from "./common/lib/parametric.js";
            import { Vector2D } from "./common/lib/vector2d.js";

            const canvas = document.querySelector("canvas");
            const ctx = canvas.getContext("2d");
            const { width, height } = canvas;
            const w = 0.5 * width,
                h = 0.5 * height;
            ctx.translate(w, h);
            ctx.scale(1, -1);

            // 繪制坐标軸
            function drawAxis() {
                ctx.save();
                ctx.strokeStyle = "#ccc";
                ctx.beginPath();
                ctx.moveTo(-w, 0);
                ctx.lineTo(w, 0);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(0, -h);
                ctx.lineTo(0, h);
                ctx.stroke();
                ctx.restore();
            }
            drawAxis();

            const quadricBezier = parametric(
                (t, [{ x: x0 }, { x: x1 }, { x: x2 }]) => (1 - t) ** 2 * x0 + 2 * t * (1 - t) * x1 + t ** 2 * x2,
                (t, [{ y: y0 }, { y: y1 }, { y: y2 }]) => (1 - t) ** 2 * y0 + 2 * t * (1 - t) * y1 + t ** 2 * y2
            );

            const p0 = new Vector2D(0, 0);
            const p1 = new Vector2D(100, 0);
            p1.rotate(0.75);
            const p2 = new Vector2D(200, 0);
            const count = 30;
            for (let i = 0; i < count; i++) {
                // 繪制30條從圓心出發,旋轉不同角度的二階貝塞爾曲線
                p1.rotate((2 / count) * Math.PI);
                p2.rotate((2 / count) * Math.PI);
                quadricBezier(0, 1, 100, [p0, p1, p2]).draw(ctx);
            }</script>
    </body>
</html>      

效果如下

【數學篇】07 # 如何用向量和參數方程描述曲線?

三階貝塞爾曲線

三階貝塞爾曲線的參數方程為:

【數學篇】07 # 如何用向量和參數方程描述曲線?

三階貝塞爾曲線的原理示意圖:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>三階貝塞爾曲線</title>
        <style>canvas {
                border: 1px dashed salmon;
            }</style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script type="module">import { parametric } from "./common/lib/parametric.js";
            import { Vector2D } from "./common/lib/vector2d.js";

            const canvas = document.querySelector("canvas");
            const ctx = canvas.getContext("2d");
            const { width, height } = canvas;
            const w = 0.5 * width,
                h = 0.5 * height;
            ctx.translate(w, h);
            ctx.scale(1, -1);

            // 繪制坐标軸
            function drawAxis() {
                ctx.save();
                ctx.strokeStyle = "#ccc";
                ctx.beginPath();
                ctx.moveTo(-w, 0);
                ctx.lineTo(w, 0);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(0, -h);
                ctx.lineTo(0, h);
                ctx.stroke();
                ctx.restore();
            }
            drawAxis();

            const cubicBezier = parametric(
                (t, [{ x: x0 }, { x: x1 }, { x: x2 }, { x: x3 }]) =>
                    (1 - t) ** 3 * x0 +
                    3 * t * (1 - t) ** 2 * x1 +
                    3 * (1 - t) * t ** 2 * x2 +
                    t ** 3 * x3,
                (t, [{ y: y0 }, { y: y1 }, { y: y2 }, { y: y3 }]) =>
                    (1 - t) ** 3 * y0 +
                    3 * t * (1 - t) ** 2 * y1 +
                    3 * (1 - t) * t ** 2 * y2 +
                    t ** 3 * y3
            );

            const p0 = new Vector2D(0, 0);
            const p1 = new Vector2D(100, 0);
            p1.rotate(0.75);
            const p2 = new Vector2D(150, 0);
            p2.rotate(-0.75);
            const p3 = new Vector2D(200, 0);
            const count = 30;
            for (let i = 0; i < count; i++) {
                p1.rotate((2 / count) * Math.PI);
                p2.rotate((2 / count) * Math.PI);
                p3.rotate((2 / count) * Math.PI);
                cubicBezier(0, 1, 100, [p0, p1, p2, p3]).draw(ctx);
            }</script>
    </body>
</html>