Home > Software design >  Path with rounded corners in Javascript d3js
Path with rounded corners in Javascript d3js

Time:01-15

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>

  • Related