说明
【跟月影学可视化】学习笔记。
如何用向量描述曲线?
用向量绘制折线的方法来绘制正多边形
<!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, step) { 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>
如何用参数方程描述曲线?
1. 画圆
圆可以用一组参数方程来定义。定义了一个圆心在(x0,y0),半径为 r 的圆。
<!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>
2. 画圆锥曲线
椭圆
a、b 分别是椭圆的长轴和短轴,当 a = b = r 时,这个方程是就圆的方程式。
<!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>
抛物线
抛物线的参数方程。其中 p 是常数,为焦点到准线的距离。
<!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, max) { 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>
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, zFunc) { /** * 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) => l * t * Math.cos(t), (t, l) => l * t * Math.sin(t) ); helical(0, 50, 500, 5).draw(ctx, { strokeStyle: "MediumPurple" }); // 绘制星形线 const star = parametric( (t, l) => l * Math.cos(t) ** 3, (t, l) => l * Math.sin(t) ** 3 ); star(0, Math.PI * 2, 50, 150).draw(ctx, { strokeStyle: "Orange" }); </script> </body> </html>
4. 画贝塞尔曲线
贝塞尔曲线是一种使用数学方法描述的曲线,被广泛用于计算机图形学和动画中。在矢量图中,贝塞尔曲线用于定义可无限放大的光滑曲线。可以用来构建 Catmull–Rom 曲线。
贝塞尔曲线又分为二阶贝塞尔曲线(Quadratic Bezier Curve)和三阶贝塞尔曲线(Qubic Bezier Curve)。
二阶贝塞尔曲线
二阶贝塞尔曲线由三个点确定,P0是起点,P1是控制点,P2是终点
二阶贝塞尔曲线的原理
绘制30条从圆心出发,旋转不同角度的二阶贝塞尔曲线
效果如下
三阶贝塞尔曲线
三阶贝塞尔曲线的参数方程为:
三阶贝塞尔曲线的原理示意图: