说明
【跟月影学可视化】学习笔记。
如何用向量描述曲线?
用向量绘制折线的方法来绘制正多边形
<!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条从圆心出发,旋转不同角度的二阶贝塞尔曲线
<!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>
效果如下
三阶贝塞尔曲线
三阶贝塞尔曲线的参数方程为:
三阶贝塞尔曲线的原理示意图:
<!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>