如何更好地近似粗贝塞尔曲线?

How to get a better approximation of a thick bezier curve?

假设我已经有一条由许多直线(代码中的 bezier 数组)近似的贝塞尔曲线,我想用一系列矩形绘制它。我有下面的代码正是这样做的:

// don't change this array
const bezier = [{x:167.00,y:40.00},{x:154.37,y:42.09},{x:143.09,y:44.48},{x:133.08,y:47.15},{x:124.26,y:50.09},{x:116.55,y:53.27},{x:109.87,y:56.68},{x:104.15,y:60.31},{x:99.32,y:64.14},{x:95.28,y:68.15},{x:91.97,y:72.34},{x:89.31,y:76.67},{x:87.22,y:81.14},{x:85.63,y:85.74},{x:84.44,y:90.43},{x:83.60,y:95.22},{x:83.02,y:100.08},{x:82.63,y:105.00},{x:82.33,y:109.96},{x:82.07,y:114.94},{x:81.76,y:119.94},{x:81.33,y:124.93},{x:80.69,y:129.89},{x:79.77,y:134.82},{x:78.49,y:139.70},{x:76.78,y:144.50},{x:74.55,y:149.22},{x:71.74,y:153.84},{x:68.25,y:158.34},{x:64.03,y:162.71},{x:58.97,y:166.93},{x:53.02,y:170.98},{x:46.10,y:174.86},{x:38.11,y:178.54},{x:29.00,y:182.00}];

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const thickness = 35;

function rotateCanvas(x, y, a) {
  ctx.translate(x, y);
  ctx.rotate(a);
  ctx.translate(-x, -y);
}

function drawRectangle(rX, rY, rW, rH, rA, color) {
  ctx.beginPath();
  rotateCanvas(rX + rW / 2, rY + rH / 2, rA);
  ctx.rect(rX, rY, rW, rH);
  rotateCanvas(rX + rW / 2, rY + rH / 2, -rA);
  ctx.fill();
}

function calcRectFromLine(x1, y1, x2, y2) {
  const dx = x2 - x1;
  const dy = y2 - y1;
  const mag = Math.sqrt(dx * dx + dy * dy);
  const angle = Math.atan2(dy, dx);

  return { 
    x: (x1 + x2) / 2 - mag / 2,
    y: (y1 + y2) / 2 - thickness / 2,
    w: mag,
    h: thickness,
    a: angle
  };
}

function calculateRectangles() {
  const result = [];

  for (let i = 1; i < bezier.length; i++) {
    const prev = bezier[i - 1];
    const curr = bezier[i];

    result.push(calcRectFromLine(prev.x, prev.y, curr.x, curr.y));
  }

  return result;
}

const rectangles = calculateRectangles();

for (let r of rectangles) {
  drawRectangle(r.x, r.y, r.w, r.h, r.a);
}
<canvas width="400" height="400"></canvas>

如果您 运行 代码片段,您会看到曲线并不完全粗,而且它是一系列矩形的事实非常明显。

如果您将厚度参数从 35 更改为较小的数字并重新设置 运行,它看起来不错。只有当它很厚的时候才会出现这种情况。

代码当前采用贝塞尔数组,创建一系列旋转矩形,然后渲染它们。

有什么方法可以修改 calculateRectangles 函数以 return 更好地近似曲线吗?理想情况下,它仍然是 return 围绕其中心旋转的矩形列表,但渲染时它看起来更像曲线,而不像矩形列表。

我能想到的唯一想法是以某种方式 return 两倍于 calculateRectangles 的矩形,其中每个矩形都与前一个矩形相反,这样直线的两边都被填充在,虽然我认为 可能 有效,但不幸的是,它具有 return 两倍多的矩形的副作用,这是不可取的,如果可能的话我会避免它.

你不能通过绘制矩形来真正制作“厚贝塞尔曲线”,你最终只会在一个尺寸上在它们之间有很多间隙,而在另一边看起来很奇怪。如果你想坚持多边形近似,你需要在你的每个点使用法线,然后画线连接它们。这会导致梯形截面,因此如果我们想要好看的结果,我们不能使用普通矩形。

但是,偏移量越大,在曲率半径很小的区域中遇到的问题就越多:您已经可以通过查看在波峰下方相互交叉的法线来看到这样的问题左上角,我们实际上不想连接所有 normal-offset 个顶点,因为其中一个位于我们要追踪的形状内。

或者,您可以用“更多贝塞尔曲线”来抵消贝塞尔曲线本身,例如https://pomax.github.io/bezierinfo/#offsetting,但即便如此,您仍然需要解决偏移形状中的重叠问题。

相反,您可以近似曲线 using circular arcs,这使得“加厚曲线”更容易一些,因为您只需使用相同的弧角和圆心,使用两个不同的半径值来获得您的两个偏移段。

您应该绘制的形状不是矩形,而是四边形,通过将连续法线的端点连接到曲线获得。据推测,您可以通过 Path 对象来实现。

在高曲率区域,您可能还需要减小步长,因为外部曲线可能不平滑。


事实上,您可以通过选择步长来“拉平”贝塞尔曲线,使连续线段之间的偏差保持在固定公差以下。

在曲线较粗的情况下,您可以保留这个想法,但要确保曲线两侧的边界偏差都成立。

第一次尝试还不错,但我会继续尝试。只需将其添加到 getRectangles 函数的末尾即可添加更多近似矩形。对于我的目的来说似乎已经足够好了(而且很简单!),但我会继续调查一下。我知道它不能很好地工作,但没关系,而且我真的不需要比还好更好的东西了:

let len = result.length;

for (let i = 1; i < len; i++) {
  const prevR = result[i - 1];
  const currR = result[i - 0];

  result.push({
    x: (prevR.x + currR.x) / 2,
    y: (prevR.y + currR.y) / 2,
    w: (prevR.w + currR.w) / 2,
    h: (prevR.h + currR.h) / 2,
    a: (prevR.a + currR.a) / 2
  });
}

其实我越玩越觉得这比ok好一点。我认为这可能是一个足够好的解决方案。除非有人能想出更好的办法。

这是区别的 GIF: