标准偏移算法将按如下方式工作:
- 将线段视为实线。以给定的数量在所选方向上将它们全部偏移。
- 找到每对相邻线的交点。这将是顶点的新位置。
- 检查消失的线段并删除相应的顶点。
请注意,杰罗姆的代码偏移点,而不是段。所以你正确地观察到它不能保持非常锐角的固定距离。它也完全忽略了第三步。
现在困难的部分是如何在其中包含变量偏移量?如果您为每个细分市场设置了偏移权重,那么整合将是直接的。但是在您的问题中,您声明每个顶点都有偏移权重。简单地添加将围绕获得的顶点移动的第 4 步将使第 3 步的结果无效。所以我宁愿寻找一种将变量偏移量集成到第一步的方法。事实上,正确的表示不仅需要偏移线,还需要旋转它们。它们当然不会再平行了,但在我看来,这将最好地代表可变顶点偏移性质。同样,如果您想减少此类困难,请考虑使用每段权重,这会使整个问题变得更容易,因为加权线偏移可以轻松集成到步骤 1 中。
更新
我从杰罗姆的回答中修复并扩展了这个例子。@Jerome,感谢您向我介绍 paper.js 以及最初的灵感。在我的版本中,我使用每个段的可变宽度,作为固定偏移的比率。在这里尝试一下。
//the paper.js is really stupid: the search is hidden by the execution controls
//and once I rename this class to "Line" which would be more appropriate
//(but my original intent was indeed a Halfline), it throws errors, //so I keep the slightly misleading name "Halfline"
class Halfline {
constructor(p, r)
{
const tangent = r - p;
this.p = p;
this.r = r;
//implicit line equation
this.a = -tangent.y;
this.b = tangent.x;
this.c = p.x * this.a + p.y * this.b;
//I didn't normalize a and b, so here a unit normal
this.n = new Point(this.a, this.b);
this.n /= this.n.length;
}
//offset the line by t in the direction of its normal
offset(t) {
return new Halfline(this.p + this.n * t, this.r + this.n * t)
}
//line intersection (infinite lines)
intersect(other) {
const det = this.a * other.b - other.a * this.b;
if (Math.abs(det) < 1e-5) //parallel
return undefined;
const x = (other.b * this.c - this.b * other.c) / det;
const y = (this.a * other.c - other.a * this.c) / det;
return new Point(x, y);
}
}
//look at this great tutorial for details on helper functions:
//https://algorithmtutor.com/Computational-Geometry/Check-if-two-Halfline-segment-intersect/
function fixOverlaps(polyline) {
function on_segment(p1, p2, p) {
return Math.min(p1.x, p2.x) <= p.x
&& p.x <= Math.max(p1.x, p2.x)
&& Math.min(p1.y, p2.y) <= p.y
&& p.y <= Math.max(p1.y, p2.y);
}
function cross_product(p1, p2) {
return p1.x * p2.y - p2.x * p1.y;
}
function direction(p1, p2, p3) {
return cross_product(p3 - p1, p2 - p1);
}
function segmentsIntersect(p1, p2, p3, p4) {
d1 = direction(p3, p4, p1);
d2 = direction(p3, p4, p2);
d3 = direction(p1, p2, p3);
d4 = direction(p1, p2, p4);
return ( (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0))
&& ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
|| (d1 == 0 && on_segment(p3, p4, p1))
|| (d2 == 0 && on_segment(p3, p4, p2))
|| (d3 == 0 && on_segment(p1, p2, p3))
|| (d4 == 0 && on_segment(p1, p2, p4)) );
}
//search for self-intersections
let intersectionsFound = true;
while (intersectionsFound)
{
let result = [polyline[0]];
for(let i = 1; i < polyline.length; ++i)
{
let anyIntersection = false;
for(let j = i + 2; j < polyline.length; ++j)
if (segmentsIntersect(polyline[i-1], polyline[i], polyline[j-1], polyline[j]))
{
const s = new Halfline(polyline[i-1], polyline[i]);
const t = new Halfline(polyline[j-1], polyline[j]);
result.push(s.intersect(t))
anyIntersection = true;
i = j;
break;
}
result.push(polyline[i])
}
intersectionsFound = polyline.length > result.length;
polyline = result;
}
return polyline;
}
const points = [
new Point(30, 40),
new Point(100, 200),
new Point(200, 60),
new Point(250, 50),
new Point(300, 70),
new Point(350, 250),
new Point(400, 60),
new Point(450, 50),
new Point(500, 70),
new Point(550, 90),
];
let poly1 = new Path();
poly1.strokeColor = 'black';
points.forEach(p => poly1.add(p));
const fixOffs = 10;
const offsets = [1, 2, 3, 2, 1, 0.5, 2.5, 2, 3, 1].map(x => x * fixOffs);
let fix = [];
let variable = [];
const size = points.length;
for(let i = 0; i < size; ++i){
let normal;
if(i == 0 || i == size - 1) { // first or last
const tangent = i == 0 ? points[i + 1] - points[i] : points[i] - points[i - 1];
const normal = new Point(-tangent.y, tangent.x) / tangent.length;
fix.push(points[i] + normal * fixOffs);
variable.push(points[i] + normal * offsets[i]);
} else {
const prevSegment = new Halfline(points[i - 1], points[i]);
const nextSegment = new Halfline(points[i], points[i + 1]);
//CONSTANT
const newConstVertex = prevSegment.offset(fixOffs).intersect(nextSegment.offset(fixOffs));
fix.push(newConstVertex || (points[i] + prevSegment.n * fixOffs));
//VARIABLE
const newVarVertex = prevSegment.offset(offsets[i]).intersect(nextSegment.offset(offsets[i + 1]));
variable.push(newVarVertex || (points[i] + prevSegment.n * offsets[i]));
}
}
//Resolve vanishing segments
const finalFix = fixOverlaps(fix);
const finalVar = fixOverlaps(variable);
let polyFix = new Path();
polyFix.strokeColor = 'red';
finalFix.forEach(p => polyFix.add(p));/**/
let polyVar = new Path();
polyVar.strokeColor = 'green';
finalVar.forEach(p => polyVar.add(p));/**/
请注意,它仍然不能处理所有特殊情况。例如,一旦偏移变得太大,非闭合多段线的角会产生奇怪的形状。另请注意,最后两个部分是并行的,这是 Jerome 的一个有趣的选择,并且有利于调试。我为它们分配了一个不同的偏移量,这迫使这些段放弃它们的平行性。