graph = {
class RoundedPolygon {
constructor(opt) {
this.padding = 20;
this.size = opt.size || 400;
this.radius = this.size / 2 - this.padding;
this.svg = d3
.select(opt.svg)
.attr('viewBox', `0 0 ${this.size * 2} ${this.size}`);
this.polygon = this.svg
.selectAll('path')
.data([0])
.join('path')
.attr(
'transform',
`translate(
${this.size / 2},
${this.size / 2}
)`
)
.attr('stroke', 'black')
.attr('stroke-width', 3)
.attr('fill', 'none');
}
draw() {
this.handleDistance = handleDistance;
this.cornerRadius = cornerRadius;
this.sides = count;
this.polygon.attr('d', this.generatePath());
const g = this.svg
.selectAll('g.bezier')
.data(this.getCorners())
.join(enter => {
const g = enter.append('g');
g.append('line').attr('class', 'prev');
g.append('line').attr('class', 'next');
g.append('circle').attr('class', 'prev');
g.append('circle').attr('class', 'next');
g.append('rect').attr('class', 'prev');
g.append('rect').attr('class', 'next');
return g;
})
.attr('class', 'bezier')
.attr('opacity', showHandles ? 1 : 0)
.attr('transform', `translate(${this.size / 2}, ${this.size / 2})`);
g.selectAll('circle')
.attr('r', 3)
.attr('fill', 'black')
.attr('stroke', 'black');
g.select('circle.prev')
.attr('cx', d => d.handle1[0])
.attr('cy', d => d.handle1[1]);
g.select('circle.next')
.attr('cx', d => d.handle2[0])
.attr('cy', d => d.handle2[1]);
g.selectAll('rect')
.attr('width', 8)
.attr('height', 8)
.attr('fill', 'white')
.attr('stroke', 'black')
.attr('transform', 'translate(-4, -4)');
g.select('rect.prev')
.attr('x', d => d.startCurve[0])
.attr('y', d => d.startCurve[1]);
g.select('rect.next')
.attr('x', d => d.endCurve[0])
.attr('y', d => d.endCurve[1]);
g.selectAll('line').attr('stroke', 'black');
g.select('line.prev')
.attr('x1', d => d.startCurve[0])
.attr('y1', d => d.startCurve[1])
.attr('x2', d => d.handle1[0])
.attr('y2', d => d.handle1[1]);
g.select('line.next')
.attr('x1', d => d.endCurve[0])
.attr('y1', d => d.endCurve[1])
.attr('x2', d => d.handle2[0])
.attr('y2', d => d.handle2[1]);
}
getCorners() {
const offset = Math.PI / 1;
const corners_in_order = d3.range(this.sides).map(d => {
const radian = d * ((Math.PI * 2) / this.sides);
const x = Math.cos(radian + offset) * this.radius;
const y = Math.sin(radian + offset) * this.radius;
return [x, y];
});
const corners = [...Array(corners_in_order.length).keys()].map(
i => corners_in_order[(i*3) % corners_in_order.length]
);
return corners.map((d, i) => {
const prev = i === 0 ? corners[corners.length - 1] : corners[i - 1];
const curr = d;
const next = i <= corners.length - 2 ? corners[i + 1] : corners[0];
const cR = this.cornerRadius / 2;
const hD = cR - (cR / 1) * this.handleDistance;
return {
start: this.getPositionOnLine(prev, curr, 0.5),
startCurve: this.getPositionOnLine(prev, curr, 1 - cR),
handle1: this.getPositionOnLine(prev, curr, 1 - hD),
handle2: this.getPositionOnLine(curr, next, 0 + hD),
endCurve: this.getPositionOnLine(curr, next, cR)
};
});
}
generatePath() {
const c = this.getCorners();
let out = `M${c[0].start}`;
out += c.map(
d =>
`L${d.start}L${d.startCurve}C${d.handle1} ${d.handle2} ${d.endCurve}`
);
out += `Z`;
return out;
}
getPositionOnLine(start, end, distance) {
const x = start[0] + (end[0] - start[0]) * distance;
const y = start[1] + (end[1] - start[1]) * distance;
return [x, y];
}
}
return new RoundedPolygon({ svg });
}