Search code examples
javascriptcanvaspaperjs

Paper.js draw multiple parallel paths from one path


I am trying to draw multiple parallel paths based on one set of coordinates like on this example:

enter image description here

I have my path created based on set of segments, then I clone it five times, and translate this way:

var myPath;
var lineData = []; // Long array of segments
myPath.segments = lineData;

for (var i = 1; i < 5; i++) {
    var clone = myPath.clone();
    clone.translate(new paper.Point(0, i*5));
}

And here is the result I get:

enter image description here

I want the lines to be full parallel but the distance is always different and they sometimes overlap. Is there a way to fix it or mayby i should try different approach in order to create this kind of curved lines?


Solution

  • A cubic bezier curve cannot be expanded to another parallel cubic bezier curve.

    @Blindman67's method of calculating normals (perpendiculars to the tangent line) along the original curve & drawing dots on those perpendiculars will work.

    To get started, see this SO Q&A that shows the algorithm to calculate points perpendicular to a Bezier curve.

    Here's an example proof-of-concept:

    If you just want solid parallel curves, then oversample by setting tCount higher (eg tCount=500). This proof-of-concept uses dots to create the line, but for solid curves you can use a set of line points.

    enter image description here

    To-Do if you want dotted lines: You'll need to oversample with the algorithm (maybe use tCount=500 instead of 60) and de-dupe the resulting point-set until you have points at uniform distance along the curve. Then your dots won't be sparse & clustered along the curve.

    enter image description here

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var cw=canvas.width;
    var ch=canvas.height;
    
    // variables defining a cubic bezier curve
    var PI2=Math.PI*2;
    var s={x:20,y:30};
    var c1={x:200,y:40};
    var c2={x:40,y:200};
    var e={x:270,y:220};
    
    // an array of points plotted along the bezier curve
    var points=[];
    
    // we use PI often so put it in a variable
    var PI=Math.PI;
    
    // plot 60 points along the curve
    // and also calculate the angle of the curve at that point
    var tCount=60;
    for(var t=0;t<=tCount;t++){
    
        var T=t/tCount;
    
        // plot a point on the curve
        var pos=getCubicBezierXYatT(s,c1,c2,e,T);
    
        // calculate the perpendicular angle of the curve at that point
        var tx = bezierTangent(s.x,c1.x,c2.x,e.x,T);
        var ty = bezierTangent(s.y,c1.y,c2.y,e.y,T);
        var a = Math.atan2(ty, tx)-PI/2;
    
        // save the x/y position of the point and the perpendicular angle
        // in the points array
        points.push({
            x:pos.x,
            y:pos.y,
            angle:a
        });
    }
    
    var PI2=Math.PI*2;
    var radii=[-12,-6,0,6,12];
    
    // fill the background
    ctx.fillStyle='navy';
    ctx.fillRect(0,0,cw,ch);
    
    // draw a dots perpendicular to each point on the curve
    ctx.beginPath();
    for(var i=0;i<points.length;i++){
        for(var j=-2;j<3;j++){
            var r=radii[j+2];
            var x=points[i].x+r*Math.cos(points[i].angle);
            var y=points[i].y+r*Math.sin(points[i].angle);
            ctx.moveTo(x,y);
            ctx.arc(x,y,1.5,0,PI2);
        }
    }
    ctx.fillStyle='skyblue';
    ctx.fill();
    
    
    //////////////////////////////////////////
    // helper functions
    //////////////////////////////////////////
    
    // calculate one XY point along Cubic Bezier at interval T
    // (where T==0.00 at the start of the curve and T==1.00 at the end)
    function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
        var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
        var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
        return({x:x,y:y});
    }
    
    // cubic helper formula at T distance
    function CubicN(T, a,b,c,d) {
        var t2 = T * T;
        var t3 = t2 * T;
        return a + (-a * 3 + T * (3 * a - a * T)) * T
        + (3 * b + T * (-6 * b + b * 3 * T)) * T
        + (c * 3 - c * 3 * T) * t2
        + d * t3;
    }
    
    // calculate the perpendicular angle at interval T on the curve
    function bezierTangent(a, b, c, d, t) {
        return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
    };
    body{ background-color: ivory; }
    #canvas{border:1px solid red; margin:0 auto; }
    <h4>Dotted parallel Bezier Curve.</h4>
    <canvas id="canvas" width=300 height=300></canvas>