如何生成“粗”贝塞尔曲线?

And*_*ner 5 geometry bezier

我正在寻找一种通过“加厚”贝塞尔曲线以编程方式生成多边形的方法。像这样的东西:

粗贝塞尔曲线

我最初的想法是找到线中的法线,并从中生成多边形:

贝塞尔法线

但问题是法线可以在陡峭的曲线中相互交叉,如下所示:

贝塞尔法线相互交叉

是否有任何公式或算法可以从贝塞尔曲线生成多边形?我在互联网上找不到任何信息,但也许我正在使用错误的词进行搜索......

Yve*_*ust 5

如果你想要一个恒定的厚度,这被称为偏移曲线,你使用法线的想法是正确的。

这确实带来了两个困难:

  1. 偏移曲线不能完全表示为贝塞尔曲线;您可以改用折线,或将 Beziers 改造为折线;

  2. 当曲率半径变得小于偏移宽度时,确实会出现尖点。您必须检测多段线的自交点。

据我所知,没有简单的解决方案。

有关更多信息,请查看38. Curve offsetting


imb*_*izi 5

此处详细介绍了分步过程:如何绘制偏移曲线

\n

解决方案基于 Gabriel Suchowolski 的论文 \xe2\x80\x98 Quadratic bezier offsetting with selected subdivision \xe2\x80\x98。作者的更多内容:数学+代码

\n

交互示例:CodePen

\n

\r\n
\r\n
var canvas, ctx;\nvar drags;\nvar thickness = 30;\nvar drawControlPoints = true;\nvar useSplitCurve = true;\n\nfunction init() {\n  canvas = document.createElement(\'canvas\');\n  ctx = canvas.getContext(\'2d\');\n  document.body.appendChild(canvas);\n\n  drags = [];\n\n  window.addEventListener(\'resize\', resize);\n  window.addEventListener(\'mousedown\', mousedown);\n  window.addEventListener(\'mouseup\', mouseup);\n  window.addEventListener(\'mousemove\', mousemove);\n\n  document.getElementById(\'btnControl\').addEventListener(\'click\', function(e) {\n    drawControlPoints = !drawControlPoints\n  });\n  document.getElementById(\'btnSplit\').addEventListener(\'click\', function(e) {\n    useSplitCurve = !useSplitCurve\n  });\n\n  resize();\n  draw();\n\n  var positions = [{\n    x: canvas.width * 0.3,\n    y: canvas.height * 0.4\n  }, {\n    x: canvas.width * 0.35,\n    y: canvas.height * 0.85\n  }, {\n    x: canvas.width * 0.7,\n    y: canvas.height * 0.25\n  }];\n  for (var i = 0; i < positions.length; i++) {\n    drags.push(new Drag(ctx, new Vec2D(positions[i].x, positions[i].y)));\n  }\n}\n\nfunction draw() {\n  requestAnimationFrame(draw);\n\n  ctx.fillStyle = \'#FFFFFF\';\n  ctx.fillRect(0, 0, canvas.width, canvas.height);\n  ctx.lineWidth = 1;\n\n  for (var i = 0; i < drags.length; i++) {\n    d = drags[i];\n    d.draw();\n  }\n\n  for (var i = 1; i < drags.length - 1; i++) {\n    /*\n    var d1 = (i == 0) ? drags[i].pos : drags[i - 1].pos;\n    var d2 = drags[i].pos;\n    var d3 = (i == drags.length - 1) ? drags[drags.length - 1].pos : drags[i + 1].pos;\n\n    var v1 = d2.sub(d1);\n    var v2 = d3.sub(d2);\n    var p1 = d2.sub(v1.scale(0.5));\n    var p2 = d3.sub(v2.scale(0.5));\n    var c = d2;\n    */\n    var p1 = drags[i - 1].pos;\n    var p2 = drags[i + 1].pos;\n    var c = drags[i].pos;\n\n    var v1 = c.sub(p1);\n    var v2 = p2.sub(c);\n\n    var n1 = v1.normalizeTo(thickness).getPerpendicular();\n    var n2 = v2.normalizeTo(thickness).getPerpendicular();\n\n    var p1a = p1.add(n1);\n    var p1b = p1.sub(n1);\n    var p2a = p2.add(n2);\n    var p2b = p2.sub(n2);\n\n    var c1a = c.add(n1);\n    var c1b = c.sub(n1);\n    var c2a = c.add(n2);\n    var c2b = c.sub(n2);\n\n    var line1a = new Line2D(p1a, c1a);\n    var line1b = new Line2D(p1b, c1b);\n    var line2a = new Line2D(p2a, c2a);\n    var line2b = new Line2D(p2b, c2b);\n\n    var split = (useSplitCurve && v1.angleBetween(v2, true) > Math.PI / 2);\n\n    if (!split) {\n      var ca = line1a.intersectLine(line2a).pos;\n      var cb = line1b.intersectLine(line2b).pos;\n    } else {\n      var t = MathUtils.getNearestPoint(p1, c, p2);\n      var pt = MathUtils.getPointInQuadraticCurve(t, p1, c, p2);\n\n      var t1 = p1.scale(1 - t).add(c.scale(t));\n      var t2 = c.scale(1 - t).add(p2.scale(t));\n\n      var vt = t2.sub(t1).normalizeTo(thickness).getPerpendicular();\n      var qa = pt.add(vt);\n      var qb = pt.sub(vt);\n\n      var lineqa = new Line2D(qa, qa.add(vt.getPerpendicular()));\n      var lineqb = new Line2D(qb, qb.add(vt.getPerpendicular()));\n\n      var q1a = line1a.intersectLine(lineqa).pos;\n      var q2a = line2a.intersectLine(lineqa).pos;\n      var q1b = line1b.intersectLine(lineqb).pos;\n      var q2b = line2b.intersectLine(lineqb).pos;\n    }\n\n    if (drawControlPoints) {\n      // draw control points\n      var r = 2;\n      ctx.beginPath();\n      if (!split) {\n        ctx.rect(ca.x - r, ca.y - r, r * 2, r * 2);\n        ctx.rect(cb.x - r, cb.y - r, r * 2, r * 2);\n      } else {\n        // ctx.rect(pt.x - r, pt.y - r, r * 2, r * 2);\n        ctx.rect(p1a.x - r, p1a.y - r, r * 2, r * 2);\n        ctx.rect(q1a.x - r, q1a.y - r, r * 2, r * 2);\n        ctx.rect(p2a.x - r, p2a.y - r, r * 2, r * 2);\n        ctx.rect(q2a.x - r, q2a.y - r, r * 2, r * 2);\n        ctx.rect(qa.x - r, qa.y - r, r * 2, r * 2);\n\n        ctx.rect(p1b.x - r, p1b.y - r, r * 2, r * 2);\n        ctx.rect(q1b.x - r, q1b.y - r, r * 2, r * 2);\n        ctx.rect(p2b.x - r, p2b.y - r, r * 2, r * 2);\n        ctx.rect(q2b.x - r, q2b.y - r, r * 2, r * 2);\n        ctx.rect(qb.x - r, qb.y - r, r * 2, r * 2);\n\n        ctx.moveTo(qa.x, qa.y);\n        ctx.lineTo(qb.x, qb.y);\n      }\n      ctx.closePath();\n      ctx.strokeStyle = \'#0072bc\';\n      ctx.stroke();\n      ctx.fillStyle = \'#0072bc\';\n      ctx.fill();\n\n      // draw dashed lines\n      ctx.beginPath();\n      if (!split) {\n        ctx.moveTo(p1a.x, p1a.y);\n        ctx.lineTo(ca.x, ca.y);\n        ctx.lineTo(p2a.x, p2a.y);\n\n        ctx.moveTo(p1b.x, p1b.y);\n        ctx.lineTo(cb.x, cb.y);\n        ctx.lineTo(p2b.x, p2b.y);\n      } else {\n        ctx.moveTo(p1a.x, p1a.y);\n        ctx.lineTo(q1a.x, q1a.y);\n        ctx.lineTo(qa.x, qa.y);\n        ctx.lineTo(q2a.x, q2a.y);\n        ctx.lineTo(p2a.x, p2a.y);\n\n        ctx.moveTo(p1b.x, p1b.y);\n        ctx.lineTo(q1b.x, q1b.y);\n        ctx.lineTo(qb.x, qb.y);\n        ctx.lineTo(q2b.x, q2b.y);\n        ctx.lineTo(p2b.x, p2b.y);\n      }\n      ctx.setLineDash([2, 4]);\n      ctx.stroke();\n      ctx.closePath();\n      ctx.setLineDash([]);\n    }\n\n    // central line\n    ctx.beginPath();\n    ctx.moveTo(p1.x, p1.y);\n    ctx.quadraticCurveTo(c.x, c.y, p2.x, p2.y);\n    ctx.strokeStyle = \'#959595\';\n    ctx.stroke();\n\n    // offset curve a\n    ctx.beginPath();\n    ctx.moveTo(p1a.x, p1a.y);\n    if (!split) {\n      ctx.quadraticCurveTo(ca.x, ca.y, p2a.x, p2a.y);\n    } else {\n      ctx.quadraticCurveTo(q1a.x, q1a.y, qa.x, qa.y);\n      ctx.quadraticCurveTo(q2a.x, q2a.y, p2a.x, p2a.y);\n    }\n    ctx.strokeStyle = \'#0072bc\';\n    ctx.lineWidth = 2;\n    ctx.stroke();\n\n    // offset curve b\n    ctx.beginPath();\n    ctx.moveTo(p1b.x, p1b.y);\n    if (!split) {\n      ctx.quadraticCurveTo(cb.x, cb.y, p2b.x, p2b.y);\n    } else {\n      ctx.quadraticCurveTo(q1b.x, q1b.y, qb.x, qb.y);\n      ctx.quadraticCurveTo(q2b.x, q2b.y, p2b.x, p2b.y);\n    }\n    ctx.strokeStyle = \'#0072bc\';\n    ctx.stroke();\n  }\n}\n\nfunction resize() {\n  canvas.width = window.innerWidth;\n  canvas.height = window.innerHeight;\n}\n\nfunction mousedown(e) {\n  e.preventDefault();\n\n  var m = new Vec2D(e.clientX, e.clientY);\n\n  for (var i = 0; i < drags.length; i++) {\n    var d = drags[i];\n    var dist = d.pos.distanceToSquared(m);\n    if (dist < d.hitRadiusSq) {\n      d.down = true;\n      break;\n    }\n  }\n}\n\nfunction mouseup() {\n  for (var i = 0; i < drags.length; i++) {\n    var d = drags[i];\n    d.down = false;\n  }\n}\n\nfunction mousemove(e) {\n  var m = new Vec2D(e.clientX, e.clientY);\n\n  for (var i = 0; i < drags.length; i++) {\n    var d = drags[i];\n    if (d.down) {\n      d.pos.x = m.x;\n      d.pos.y = m.y;\n      break;\n    }\n  }\n}\n\nfunction Drag(ctx, pos) {\n  this.ctx = ctx;\n  this.pos = pos;\n  this.radius = 6;\n  this.hitRadiusSq = 900;\n  this.down = false;\n}\n\nDrag.prototype = {\n  draw: function() {\n    this.ctx.beginPath();\n    this.ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);\n    this.ctx.closePath();\n    this.ctx.strokeStyle = \'#959595\'\n    this.ctx.stroke();\n  }\n}\n\n// http://toxiclibs.org/docs/core/toxi/geom/Vec2D.html\nfunction Vec2D(a, b) {\n  this.x = a;\n  this.y = b;\n}\n\nVec2D.prototype = {\n  add: function(a) {\n    return new Vec2D(this.x + a.x, this.y + a.y);\n  },\n  angleBetween: function(v, faceNormalize) {\n    if (faceNormalize === undefined) {\n      var dot = this.dot(v);\n      return Math.acos(this.dot(v));\n    }\n    var theta = (faceNormalize) ? this.getNormalized().dot(v.getNormalized()) : this.dot(v);\n    return Math.acos(theta);\n  },\n  distanceToSquared: function(v) {\n    if (v !== undefined) {\n      var dx = this.x - v.x;\n      var dy = this.y - v.y;\n      return dx * dx + dy * dy;\n    } else {\n      return NaN;\n    }\n  },\n  dot: function(v) {\n    return this.x * v.x + this.y * v.y;\n  },\n  getNormalized: function() {\n    return new Vec2D(this.x, this.y).normalize();\n  },\n  getPerpendicular: function() {\n    return new Vec2D(this.x, this.y).perpendicular();\n  },\n  interpolateTo: function(v, f) {\n    return new Vec2D(this.x + (v.x - this.x) * f, this.y + (v.y - this.y) * f);\n  },\n  normalize: function() {\n    var mag = this.x * this.x + this.y * this.y;\n    if (mag > 0) {\n      mag = 1.0 / Math.sqrt(mag);\n      this.x *= mag;\n      this.y *= mag;\n    }\n    return this;\n  },\n  normalizeTo: function(len) {\n    var mag = Math.sqrt(this.x * this.x + this.y * this.y);\n    if (mag > 0) {\n      mag = len / mag;\n      this.x *= mag;\n      this.y *= mag;\n    }\n    return this;\n  },\n  perpendicular: function() {\n    var t = this.x;\n    this.x = -this.y;\n    this.y = t;\n    return this;\n  },\n  scale: function(a) {\n    return new Vec2D(this.x * a, this.y * a);\n  },\n  sub: function(a, b) {\n    return new Vec2D(this.x - a.x, this.y - a.y);\n  },\n}\n\n// http://toxiclibs.org/docs/core/toxi/geom/Line2D.html\nfunction Line2D(a, b) {\n  this.a = a;\n  this.b = b;\n}\n\nLine2D.prototype = {\n  intersectLine: function(l) {\n    var isec,\n      denom = (l.b.y - l.a.y) * (this.b.x - this.a.x) - (l.b.x - l.a.x) * (this.b.y - this.a.y),\n      na = (l.b.x - l.a.x) * (this.a.y - l.a.y) - (l.b.y - l.a.y) * (this.a.x - l.a.x),\n      nb = (this.b.x - this.a.x) * (this.a.y - l.a.y) - (this.b.y - this.a.y) * (this.a.x - l.a.x);\n    if (denom !== 0) {\n      var ua = na / denom,\n        ub = nb / denom;\n      if (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) {\n        isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.INTERSECTING, this.a.interpolateTo(this.b, ua));\n      } else {\n        isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.NON_INTERSECTING, this.a.interpolateTo(this.b, ua));\n      }\n    } else {\n      if (na === 0 && nb === 0) {\n        isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.COINCIDENT, undefined);\n      } else {\n        isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.COINCIDENT, undefined);\n      }\n    }\n    return isec;\n  }\n}\n\nLine2D.LineIntersection = function(type, pos) {\n  this.type = type;\n  this.pos = pos;\n}\n\nLine2D.LineIntersection.Type = {\n  COINCIDENT: 0,\n  PARALLEL: 1,\n  NON_INTERSECTING: 2,\n  INTERSECTING: 3\n};\n\n\nwindow.MathUtils = {\n  getPointInQuadraticCurve: function(t, p1, pc, p2) {\n    var x = (1 - t) * (1 - t) * p1.x + 2 * (1 - t) * t * pc.x + t * t * p2.x;\n    var y = (1 - t) * (1 - t) * p1.y + 2 * (1 - t) * t * pc.y + t * t * p2.y;\n\n    return new Vec2D(x, y);\n  },\n\n  // http://microbians.com/math/Gabriel_Suchowolski_Quadratic_bezier_offsetting_with_selective_subdivision.pdf\n  // http://www.math.vanderbilt.edu/~schectex/courses/cubic/\n  getNearestPoint: function(p1, pc, p2) {\n    var v0 = pc.sub(p1);\n    var v1 = p2.sub(pc);\n\n    var a = v1.sub(v0).dot(v1.sub(v0));\n    var b = 3 * (v1.dot(v0) - v0.dot(v0));\n    var c = 3 * v0.dot(v0) - v1.dot(v0);\n    var d = -1 * v0.dot(v0);\n\n    var p = -b / (3 * a);\n    var q = p * p * p + (b * c - 3 * a * d) / (6 * a * a);\n    var r = c / (3 * a);\n\n    var s = Math.sqrt(q * q + Math.pow(r - p * p, 3));\n    var t = MathUtils.cbrt(q + s) + MathUtils.cbrt(q - s) + p;\n\n    return t;\n  },\n\n  // http://stackoverflow.com/questions/12810765/calculating-cubic-root-for-negative-number\n  cbrt: function(x) {\n    var sign = x === 0 ? 0 : x > 0 ? 1 : -1;\n    return sign * Math.pow(Math.abs(x), 1 / 3);\n  }\n}\n\ninit();
Run Code Online (Sandbox Code Playgroud)\r\n
html,\nbody {\n  height: 100%;\n  margin: 0\n}\n\ncanvas {\n  display: block\n}\n\n#btnControl {\n  position: absolute;\n  top: 10px;\n  left: 10px;\n}\n\n#btnSplit {\n  position: absolute;\n  top: 35px;\n  left: 10px;\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<button type="button" id="btnControl">control points on/off</button>\n<button type="button" id="btnSplit">split curve on/off</button>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n