I would like to create a rounded edge for a corner where the user can specify the corner's radius in D3js.
I found a post that has potential solutions, but the examples are in Observable notebook.
I tried converting to plain Javascript. But it didn't work for me.
https://observablehq.com/@carpiediem/svg-paths-with-circular-corners
Any help is much appreciated, thanks.
CodePudding user response:
I think the post you shared may be overly complicated. Assuming you are using d3.line()
or d3.area()
, I would suggest looking into the different curve interpolators available in D3. Many of them allow an extra parameter to specify, for example, a tension that can be manipulated.
CodePudding user response:
Here it is:
const drag = () => {
function dragstarted(d) {
d3.select(this).raise().attr("stroke", "black");
}
function dragged(d) {
d3.select(this)
.attr("cx", d.x = d3.event.x)
.attr("cy", d.y = d3.event.y);
d3.select('path.angled')
.attr('d', 'M' points.map(d => `${d.x} ${d.y}`).join(','));
const angle = Math.atan2(points[1].y-points[0].y, points[1].x-points[0].x)
- Math.atan2(points[1].y-points[2].y, points[1].x-points[2].x);
const acuteAngle = Math.min(Math.abs(angle), 2*Math.PI-Math.abs(angle));
const shortestRay = Math.min(
Math.sqrt(Math.pow(points[1].x-points[0].x, 2) Math.pow(points[1].y-points[0].y, 2)),
Math.sqrt(Math.pow(points[1].x-points[2].x, 2) Math.pow(points[1].y-points[2].y, 2))
);
const radiusToUse = Math.min( cornerRadius, shortestRay * Math.tan(acuteAngle/2) );
const distanceToTangentPoint = Math.abs(radiusToUse / Math.tan(acuteAngle/2));
const determinant = (points[1].x-points[0].x)*(points[1].y-points[2].y) - (points[1].x-points[2].x)*(points[1].y-points[0].y);
const sweepFlag = determinant < 0 ? 1 : 0;
const anchorIn = alongSegment(points[1], points[0], distanceToTangentPoint);
const anchorOut = alongSegment(points[1], points[2], distanceToTangentPoint);
const manualPathDesc = `
M${points[0].x} ${points[0].y}
L${anchorIn.x} ${anchorIn.y}
A${radiusToUse} ${radiusToUse} 0 0 ${sweepFlag} ${anchorOut.x} ${anchorOut.y}
L${points[2].x} ${points[2].y}
`;
d3.select('path.arced').attr('d', manualPathDesc);
d3.select('rect.anchor.in')
.attr("x", anchorIn.x - 3)
.attr("y", anchorIn.y - 3);
d3.select('rect.anchor.out')
.attr("x", anchorOut.x - 3)
.attr("y", anchorOut.y - 3);
const circleCenter = alongSegment(
points[1],
{ x: (anchorIn.x anchorOut.x)/2, y: (anchorIn.y anchorOut.y)/2 },
Math.sqrt(Math.pow(radiusToUse, 2) Math.pow(distanceToTangentPoint, 2))
);
d3.select('path.triangles')
.attr("d", `M${points[1].x} ${points[1].y} L${circleCenter.x} ${circleCenter.y} L${anchorIn.x} ${anchorIn.y} L${circleCenter.x} ${circleCenter.y} L${anchorOut.x} ${anchorOut.y}`);
d3.select('text.angle').text(`${Math.round(acuteAngle * 180 / Math.PI)}°`);
d3.select('text.shortest').text(Math.round(shortestRay));
d3.select('text.maxradius').text(Math.round(shortestRay * Math.tan(acuteAngle/2)));
d3.select('text.toAnchor').text(Math.round(distanceToTangentPoint));
d3.select('text.determinate').text(determinant < 0 ? 'neg.' : 'pos.');
}
function dragended(d) {
d3.select(this).attr("stroke", null);
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
function alongSegment(from, toward, distanceAlong) {
const bearing = Math.atan2(from.y-toward.y, from.x-toward.x);
return {
bearing,
x: from.x - distanceAlong * Math.cos(bearing),
y: from.y - distanceAlong * Math.sin(bearing)
};
}
const chart = () => {
var color = d3.scaleOrdinal().range(d3.schemeCategory20);
const svg = d3.select("svg")
.attr("viewBox", [0, 0, width, height]);
const angle = Math.atan2(points[1].y-points[0].y, points[1].x-points[0].x)
- Math.atan2(points[1].y-points[2].y, points[1].x-points[2].x);
const acuteAngle = Math.min(Math.abs(angle), 2*Math.PI-Math.abs(angle));
const shortestRay = Math.min(
Math.sqrt(Math.pow(points[1].x-points[0].x, 2) Math.pow(points[1].y-points[0].y, 2)),
Math.sqrt(Math.pow(points[1].x-points[2].x, 2) Math.pow(points[1].y-points[2].y, 2))
);
const radiusToUse = Math.min( cornerRadius, shortestRay * Math.tan(acuteAngle/2) );
const distanceToTangentPoint = Math.abs(radiusToUse / Math.tan(acuteAngle/2));
const determinant = (points[1].x-points[0].x)*(points[1].y-points[2].y) - (points[1].x-points[2].x)*(points[1].y-points[0].y);
const sweepFlag = determinant < 0 ? 1 : 0;
const anchorIn = alongSegment(points[1], points[0], distanceToTangentPoint);
const anchorOut = alongSegment(points[1], points[2], distanceToTangentPoint);
const circleCenter = alongSegment(
points[1],
{ x: (anchorIn.x anchorOut.x)/2, y: (anchorIn.y anchorOut.y)/2 },
Math.sqrt(Math.pow(radiusToUse, 2) Math.pow(distanceToTangentPoint, 2))
);
const manualPathDesc = `M${points[0].x} ${points[0].y}
L${anchorIn.x} ${anchorIn.y}
A${radiusToUse} ${radiusToUse} 0 0 ${sweepFlag} ${anchorOut.x} ${anchorOut.y}
L${points[2].x} ${points[2].y}
`;
svg.append('rect')
.attr("x", 8)
.attr("y", 10)
.attr("width", 160)
.attr("height", 105)
.attr("fill", '#eee');
svg.append('text')
.attr("class", 'angle')
.attr("x", 35)
.attr("y", 25)
.attr("text-anchor", "end")
.text(`${Math.round(acuteAngle * 180 / Math.PI)}°`);
svg.append('text')
.attr("class", 'shortest')
.attr("x", 35)
.attr("y", 45)
.attr("text-anchor", "end")
.text(Math.round(shortestRay));
svg.append('text')
.attr("class", 'maxradius')
.attr("x", 35)
.attr("y", 65)
.attr("text-anchor", "end")
.text(Math.round(shortestRay * Math.tan(acuteAngle/2)));
svg.append('text')
.attr("class", 'toAnchor')
.attr("x", 35)
.attr("y", 85)
.attr("text-anchor", "end")
.text(Math.round(distanceToTangentPoint));
svg.append('text')
.attr("class", 'determinate')
.attr("x", 35)
.attr("y", 105)
.attr("text-anchor", "end")
.text(determinant < 0 ? 'neg.' : 'pos.');
svg.append('text')
.attr("x", 40)
.attr("y", 25)
.attr("text-anchor", "start")
.text('angle between rays');
svg.append('text')
.attr("x", 40)
.attr("y", 45)
.attr("text-anchor", "start")
.text('length of shortest ray');
svg.append('text')
.attr("x", 40)
.attr("y", 65)
.attr("text-anchor", "start")
.text('max radius, to fit');
svg.append('text')
.attr("x", 40)
.attr("y", 85)
.attr("text-anchor", "start")
.text('from vertex to anchors');
svg.append('text')
.attr("x", 40)
.attr("y", 105)
.attr("text-anchor", "start")
.text('determinant value');
svg.append('path')
.attr("class", 'arced')
.datum(points)
.attr("d", manualPathDesc)
.attr("stroke", 'orange')
.attr("stroke-width", 5)
.attr("fill", 'none');
svg.append('path')
.attr("class", 'angled')
.attr("d", 'M' points.map(d => `${d.x} ${d.y}`).join(', '))
.attr("stroke", '#888')
.attr("fill", 'none');
svg.append('rect')
.attr("class", 'anchor in')
.attr("x", anchorIn.x - 3)
.attr("y", anchorIn.y - 3)
.attr("width", 6)
.attr("height", 6)
.attr("fill", '#888');
svg.append('rect')
.attr("class", 'anchor out')
.attr("x", anchorOut.x - 3)
.attr("y", anchorOut.y - 3)
.attr("width", 6)
.attr("height", 6)
.attr("fill", '#ccc');
svg.append('path')
.attr("class", 'triangles')
.attr("d", `M${points[1].x} ${points[1].y} L${circleCenter.x} ${circleCenter.y} L${anchorIn.x} ${anchorIn.y} L${circleCenter.x} ${circleCenter.y} L${anchorOut.x} ${anchorOut.y}`)
.attr("stroke", '#ccc')
.attr("fill", 'none');
svg.selectAll("circle")
.data(points)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 6)
.attr("fill", (d, i) => color(i))
.on("mouseover", function (d) {d3.select(this).style("cursor", "move");})
.on("mouseout", function (d) {})
.call(drag());
return svg.node();
}
const width = 1000;
const height = 600;
const cornerRadius = 50;
const points = d3.range(3).map(i => ({
x: Math.random() * (width - 10 * 2) 10,
y: Math.random() * (300 - 10 * 2) 10,
}));
chart();
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="1000" height="600"></svg>