canvas德卡斯特里奥算法构造贝塞尔曲线可视化实现
前言
在现代js教程中看到通过德卡斯特里奥算法构造贝塞尔曲线的demo,觉得很有意思,尝试自己写一下
目标效果如下
吐槽一下,我看到文章底部才发现原来这里的demo是svg做的
开始制作
多层画布
因为这个动画效果有变化的部分和不变的部分,所以将它们区分开,减少重复绘制。方格坐标是固定不变的,作为最底层;控制点线段也只需绘制一次,作为第二层;绘制过程的多个变化线段,作为第三层。
<div id="bezierCurve">
<canvas id="coordinateSystem" width="420" height="420"
>此环境不支持canvas</canvas
>
<canvas id="controlLine" width="420" height="420"
>此环境不支持canvas</canvas
>
<canvas id="animation" width="420" height="420"
>此环境不支持canvas</canvas
>
<button id="opAnimation" onclick="opAnim()">开始</button>
<span id="showTime">t:0</span>
</div>
定义全局常量变量
// 定义常量
const whMultiple = 20; //格子所占大小
const translation = 10; //坐标系离canvas原点的偏移量,因为紧挨着线段宽度会减少一半
const self = this; //获取全局对象,方面后面调用
const pointsControl = [
{ x: 0, y: 20 },
{ x: 10, y: 20 },
{ x: 10, y: 0 },
{ x: 20, y: 0 },
{ x: 20, y: 10 },
{ x: 15, y: 10 },
{ x: 15, y: 15 },
];
// 定义全局变量
var timer; //定时器
var distance = 0; // 在A->B上按比例取点,0为A点,1为B点
var pointsBezier = []; //存储贝塞尔曲线上的点
var coordinateSystem = document
.getElementById('coordinateSystem')
.getContext('2d');
var controlLine = document.getElementById('controlLine').getContext('2d');
var animation = document.getElementById('animation').getContext('2d');
const opAnimation = document.getElementById('opAnimation');
const showTime = document.getElementById('showTime');
方格画板
首先需要一个方格画板,写个方法,以左上角为原点做一个坐标系
// 定义常量
const whMultiple = 10; //格子所占大小
// 绘制坐标系
function drawCoordinateSystem(ctx, x, y) {
if (x < 1 || y < 1) return;
ctx.strokeStyle = '#e2e2e2';
ctx.lineWidth = 0.5;
ctx.beginPath();
for (let i = 0; i <= x; i++) {
ctx.moveTo(i * whMultiple, 0);
ctx.lineTo(i * whMultiple, y * whMultiple);
}
for (let i = 0; i <= y; i++) {
ctx.moveTo(0, i * whMultiple);
ctx.lineTo(y * whMultiple, i * whMultiple);
}
ctx.stroke();
}
控制点连线
德卡斯特里奥算法是通过递归不断降阶,最终得到一阶曲线的算法。除了最开始所给定的控制点,每次降阶产生的点也可以看作控制点
// 绘制控制点连线线段,参数形如[{x,y}]
function drawControlLine(ctx, coordinates, color) {
if (coordinates.length <= 1) return;
ctx.strokeStyle = color;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(coordinates[0].x * whMultiple, coordinates[0].y * whMultiple);
for (let i = 1; i < coordinates.length; i++) {
ctx.lineTo(
coordinates[i].x * whMultiple,
coordinates[i].y * whMultiple
);
}
ctx.stroke();
}
重头戏之德卡斯特里奥算法
给不同阶数的控制线不同的颜色,目前颜色是写死了七种,最高就支持七阶,更高也能画线,但是颜色就不对了,这个有需要的可以自己写个阶梯颜色生成函数,就能支持任意阶数了。
draw函数是通过递归,不断降阶,最终得出贝塞尔曲线上的一个点,也绘画出这一帧的变化。然后是setInterval执行动画,原本想window.requestAnimationFrame()的,不过这样动画太快了,不好清晰的看到曲线生成的过程,如读者有更好的写法,欢迎指正。
// 德卡斯特里奥算法
function deCasteljau(ctx, pointsControl) {
if (pointsControl.length <= 1) return;
const colors = [
'#15209b',
'#15719b',
'#157e9b',
'#159b9b',
'#159b89',
'#159b4b',
'#759b15',
];
// 这里做绘制一帧的操作
function draw(pointsControl, distance) {
if (pointsControl.length <= 2) {
pointsBezier.push(
getMidPoint(distance, pointsControl[0], pointsControl[1])
);
drawControlLine(ctx, pointsBezier, 'red');
return;
}
const newPointsControl = [];
for (let i = 1; i < pointsControl.length; i++) {
newPointsControl.push(
getMidPoint(distance, pointsControl[i - 1], pointsControl[i])
);
}
drawControlLine(ctx, newPointsControl, colors[newPointsControl.length]);
draw(newPointsControl, distance);
}
timer = setInterval(() => {
if (self.distance >= 1.01) {
clearInterval(timer);
self.distance = 0;
self.pointsBezier = [];
self.opAnimation.innerText = '开始';
self.showTime.innerText = 't:0';
return;
}
self.showTime.innerText = 't:' + self.distance;
ctx.clearRect(0, 0, 800, 800);
draw(pointsControl, self.distance);
self.distance = parseFloat((self.distance + 0.01).toFixed(2));
}, 100);
}
这里是获取有向线段A->B上按比例取的点坐标
function getMidPoint(distance, pointStart, pointEnd) {
const x = pointStart.x + distance * (pointEnd.x - pointStart.x);
const y = pointStart.y + distance * (pointEnd.y - pointStart.y);
return { x, y };
}
控制按钮
function opAnim() {
switch (opAnimation.innerText) {
case '开始':
console.log('开始', this.distance);
opAnimation.innerText = '暂停';
deCasteljau(animation, pointsControl, distance);
break;
case '继续':
console.log('继续', distance);
opAnimation.innerText = '暂停';
deCasteljau(animation, pointsControl, distance);
break;
case '暂停':
opAnimation.innerText = '继续';
clearInterval(timer);
break;
default:
break;
}
}
完成效果
不算高度还原,只能说实现了可视化吧,现代JS教程的那个demo还可以拖拽控制点,这个后面我有时间再开一个贴。