canvas德卡斯特里奥算法构造贝塞尔曲线可视化实现

前言

现代js教程中看到通过德卡斯特里奥算法构造贝塞尔曲线的demo,觉得很有意思,尝试自己写一下
目标效果如下
image
吐槽一下,我看到文章底部才发现原来这里的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还可以拖拽控制点,这个后面我有时间再开一个贴。
image

参考

现代JavaScript教程
德卡斯特里奥算法——找到Bezier曲线上的一个点
canvas中的拖拽、缩放、旋转

posted @ 2023-01-05 10:15  初学者-xjr  阅读(142)  评论(0编辑  收藏  举报