class Ring {
constructor(points, sketch) {
this.sketch = sketch;
this.points = points;
this.arcs = null;
this.connects = null;
}
draw() {
const PI2 = 2 * Math.PI;
this.sketch.beginShape();
this.sketch.vertex(this.arcs[0].start[0], this.arcs[0].start[1]);
for (let arcNum = 0; arcNum < this.arcs.length; arcNum++) {
const curArc = this.arcs[arcNum],
nextConnect = this.connects[arcNum],
angle = Math.atan2(curArc.start[1] - curArc.cy, curArc.start[0] - curArc.cx);
let angleDelta = Math.atan2(curArc.end[1] - curArc.cy, curArc.end[0] - curArc.cx) - angle;
const isSameAngle = Math.abs(angleDelta) < Number.EPSILON;
while (angleDelta < 0) angleDelta += PI2;
while (angleDelta > PI2) angleDelta -= PI2;
if (angleDelta < Number.EPSILON) {
angleDelta = isSameAngle ? 0 : PI2;
}
if (!curArc.sweep && !isSameAngle) {
if (angleDelta === PI2) {
angleDelta = -PI2;
} else {
angleDelta -= PI2;
}
}
if (!isSameAngle) {
let angleCount = (100 * width) / 800;
for (let i = 0; i < angleCount; i += 1) {
let curAngle = angle + (angleDelta * i) / angleCount,
x = curArc.cx + curArc.r * Math.cos(curAngle),
y = curArc.cy + curArc.r * Math.sin(curAngle);
this.sketch.vertex(x, y);
}
}
this.sketch.vertex(nextConnect[2], nextConnect[3]);
}
this.sketch.endShape();
}
generate(isWrapped, forceCentroid) {
this.connects = [];
this.arcs = [];
let centroid;
if (forceCentroid) {
centroid = [forceCentroid[0], forceCentroid[1]];
} else {
centroid = [0, 0];
this.points.forEach(t => {
(centroid[0] += t.cx), (centroid[1] += t.cy);
});
centroid = [centroid[0] / this.points.length, centroid[1] / this.points.length];
}
this.points.sort((pt1, pt2) => {
const angleDelta =
(Math.atan2(pt1.cy - centroid[1], pt1.cx - centroid[0]) -
Math.atan2(pt2.cy - centroid[1], pt2.cx - centroid[0])) %
(2 * Math.PI),
lengthDelta = -(
distance(pt1.cx, pt1.cy, centroid[0], centroid[1]) -
distance(pt2.cx, pt2.cy, centroid[0], centroid[1])
),
sameAngle = Math.abs(angleDelta) < 1e-4,
sameLen = Math.abs(lengthDelta) < 1e-4;
return sameAngle && sameLen ? pt1.id - pt2.id : sameAngle ? lengthDelta : angleDelta;
});
for (let ptNum = 0; ptNum < this.points.length && this.points.length > 1; ptNum += 1) {
const prevPt = this.points[ptNum - 1 < 0 ? this.points.length - 1 : ptNum - 1],
curPt = this.points[ptNum];
curPt.sortedOrder = ptNum;
const nextPt = this.points[(ptNum + 1) % this.points.length],
dxp = prevPt.cx - curPt.cx,
dyp = prevPt.cy - curPt.cy,
dxn = nextPt.cx - curPt.cx,
dyn = nextPt.cy - curPt.cy,
angle = Math.atan2(dxp * dyn - dyp * dxn, dxp * dxn + dyp * dyn);
curPt.isConcave =
(angle < 0 && Math.abs(angle) <= Math.PI) || Math.abs(Math.abs(angle) - Math.PI) < 1e-4;
}
for (let ptNum = 0; ptNum < this.points.length && this.points.length > 1; ptNum += 1) {
const curPt = this.points[ptNum],
nextPt = this.points[(ptNum + 1) % this.points.length],
tangents = this.getTangents(curPt.cx, curPt.cy, curPt.r, nextPt.cx, nextPt.cy, nextPt.r);
isWrapped
? curPt.isConcave && nextPt.isConcave
? this.connects.push(tangents[1])
: curPt.isConcave && !nextPt.isConcave
? this.connects.push(tangents[3])
: !curPt.isConcave && nextPt.isConcave
? this.connects.push(tangents[2])
: this.connects.push(tangents[0])
: this.connects.push(tangents[1]);
}
for (let ptNum = 0; ptNum < this.points.length && this.points.length > 1; ptNum += 1) {
const prevPtNum = ptNum - 1 < 0 ? this.points.length - 1 : ptNum - 1,
curPt = this.points[ptNum],
nextConnect = this.connects[ptNum],
prevConnect = this.connects[prevPtNum],
pc1 = [prevConnect[2], prevConnect[3]],
nc0 = [nextConnect[0], nextConnect[1]];
if (isWrapped) {
const pc0 = [prevConnect[0], prevConnect[1]],
nc1 = [nextConnect[2], nextConnect[3]],
dxp = pc1[0] - pc0[0],
dyp = pc1[1] - pc0[1],
dxn = nc0[0] - nc1[0],
dyn = nc0[1] - nc1[1],
angle = Math.atan2(dxp * dyn - dyp * dxn, dxp * dxn + dyp * dyn),
isLargeArc =
Math.abs(angle) < Math.PI / 2 &&
((angle < 0 && !curPt.isConcave) || (angle > 0 && curPt.isConcave))
? 1
: 0;
this.arcs.push({
start: pc1,
cx: curPt.cx,
cy: curPt.cy,
r: curPt.r,
largeArc: isLargeArc,
sweep: curPt.isConcave ? 1 : 0,
end: nc0,
display: Math.abs(angle) < Math.PI / 1.2,
});
} else {
this.arcs.push({
start: pc1,
cx: curPt.cx,
cy: curPt.cy,
r: curPt.r,
largeArc: curPt.isConcave ? 0 : 1,
sweep: 1,
end: nc0,
display: true,
});
}
}
}
getTangents(cx0, cy0, r0, cx1, cy1, r1) {
const distSq = (cx0 - cx1) * (cx0 - cx1) + (cy0 - cy1) * (cy0 - cy1);
if (distSq <= (r0 - r1) * (r0 - r1)) return [];
const dist = Math.sqrt(distSq),
cos = (cx1 - cx0) / dist,
sin = (cy1 - cy0) / dist,
res = Array(4)
.fill(0)
.map(t => Array(4).fill(0));
let resLen = 0;
for (let sign1 = 1; sign1 >= -1; sign1 -= 2) {
const d = (r0 - sign1 * r1) / dist;
if (d * d > 1) continue;
const dd = Math.sqrt(Math.max(0, 1 - d * d));
for (let sign2 = 1; sign2 >= -1; sign2 -= 2) {
const a = cos * d - sign2 * dd * sin,
b = sin * d + sign2 * dd * cos,
resItem = res[resLen++];
resItem[0] = cx0 + r0 * a;
resItem[1] = cy0 + r0 * b;
resItem[2] = cx1 + sign1 * r1 * a;
resItem[3] = cy1 + sign1 * r1 * b;
}
}
return res.slice(0, resLen);
}
}