简体   繁体   中英

Path with rounded corners in Javascript d3js

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.

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.

Here it is:

 const drag = () => { function dragstarted(d) { d3.select(this).raise().attr("stroke", "black"); } function dragged(d) { d3.select(this).attr("cx", dx = d3.event.x).attr("cy", dy = d3.event.y); d3.select('path.angled').attr('d', 'M' + points.map(d => `${dx} ${dy}`).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 => `${dx} ${dy}`).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 => dx).attr("cy", d => dy).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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM