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

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

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

是否有任何公式或算法可以从贝塞尔曲线生成多边形?我在互联网上找不到任何信息,但也许我正在使用错误的词进行搜索......
如果你想要一个恒定的厚度,这被称为偏移曲线,你使用法线的想法是正确的。
这确实带来了两个困难:
偏移曲线不能完全表示为贝塞尔曲线;您可以改用折线,或将 Beziers 改造为折线;
当曲率半径变得小于偏移宽度时,确实会出现尖点。您必须检测多段线的自交点。
据我所知,没有简单的解决方案。
有关更多信息,请查看38. Curve offsetting。
此处详细介绍了分步过程:如何绘制偏移曲线
\n解决方案基于 Gabriel Suchowolski 的论文 \xe2\x80\x98 Quadratic bezier offsetting with selected subdivision \xe2\x80\x98。作者的更多内容:数学+代码
\n交互示例:CodePen
\nvar 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\nhtml,\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| 归档时间: |
|
| 查看次数: |
2810 次 |
| 最近记录: |