Search code examples
javascripthtmlcanvascomputational-geometrybspline

Convert line to "rectangle snake". Canvas. JavaScript


May be someone know good JavaScript library or framework for canvas drawing.

I have following issue:

1) draw some line via free-hand (like Paint/Photoshop pencil)

2) need convert line into "rectangle snake" (build a line using rectangles with fixed width)

look at screenshot enter image description here

What kind of library will be better to use for this issue? May be some kind of library alredy has this functionality?

I mean need following functionality:

  • line approximation

  • spline separation

  • converting line to polygons/shapes/objects

Will be great if someone will help me. Thanks!


Solution

  • As you say in your question, your points can be turned into a spline consisting of a set of cubic Bezier curves. Hint: you might simplify the point-set before calculating the spline of the remaining (fewer) points.

    Here's how to calculate a set of rectangular-ish polygons along a cubic Bezier curve.

    Sidenote: The polygons must be rectangular-ish because it's not possible to create a set of side-connected rectangles along a curved path.

    enter image description here

    1. Calculate a set of points along the curve.
    2. Using the points in #1, calculate the tangent angle for each of the points.
    3. Using the points & angles, calculate perpendicular lines extending outward in both directions from each point.
    4. Use the endpoints of the perpendicular lines to build a set of polygons. Each new polygon is built from the prior & current perpendicular endpoints.

    Here is annotated code and a Demo:

    // canvas related variables
    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    
    // variables defining a cubic bezier curve
    var PI=Math.PI;
    var PI2=PI*2;
    var s={x:20,y:30};
    var c1={x:300,y:40};
    var c2={x:40,y:150};
    var e={x:370,y:170};
    
    // an array of points plotted along the bezier curve
    var points=[];
    // an array of polygons along the bezier curve
    var polys=[];
    
    // plot some points & tangent angles along the curve
    for(var t=0;t<=100;t+=4){
    
      var T=t/100;
    
      // plot a point on the curve
      var pos=getCubicBezierXYatT(s,c1,c2,e,T);
    
      // calculate the tangent 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 tangent angle
      points.push({
        x:pos.x,
        y:pos.y,
        angle:a
      });         
    }
    
    // create polygons that extend on either side of the 
    //     original points set
    for(var i=1;i<points.length;i++){
      var p0=points[i-1];
      var p1=points[i];
      polys.push({
        x0:p0.x+20*Math.cos(p0.angle),
        y0:p0.y+20*Math.sin(p0.angle),
        x1:p1.x+20*Math.cos(p1.angle),
        y1:p1.y+20*Math.sin(p1.angle),
        x2:p1.x+20*Math.cos(p1.angle-PI),
        y2:p1.y+20*Math.sin(p1.angle-PI),                
        x3:p0.x+20*Math.cos(p0.angle-PI),
        y3:p0.y+20*Math.sin(p0.angle-PI),
      });
    }
    
    // draw the polygons
    for(var i=0;i<polys.length;i++){
      var r=polys[i];
      ctx.beginPath();
      ctx.moveTo(r.x0,r.y0);
      ctx.lineTo(r.x1,r.y1);
      ctx.lineTo(r.x2,r.y2);
      ctx.lineTo(r.x3,r.y3);
      ctx.closePath();
      ctx.fillStyle=randomColor();
      ctx.fill();
      ctx.stroke();
    }
    
    // draw the bezier curve points
    ctx.beginPath();
    ctx.moveTo(points[0].x,points[0].y);
    for(var i=0;i<points.length;i++){
      ctx.lineTo(points[i].x,points[i].y);
    }
    ctx.lineWidth=3;
    ctx.strokeStyle='red';
    ctx.stroke();
    
    function randomColor(){ 
      return('#'+Math.floor(Math.random()*16777215).toString(16));
    }
    
    
    //////////////////////////////////////////
    // 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 tangent 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:white; padding:10px; }
    #canvas{border:1px solid red; margin:0 auto; }
    <h4>"Rectangular-ish" polygons along the red cubic bezier curve.</h4>
    <canvas id="canvas" width=400 height=300></canvas>

    Uniform width polygons:

    Again, because they are rectangular-ish polygons, you will not be able to get precisely uniform widths. To get semi-uniform widths:

    • Calculate many points along the curve (maybe 1000+).
    • Use the distance formula to reduce the 1000+ point-set to a point-set with uniform distances along the curve: var distance=Math.sqrt( (pt1.x-pt0.x)*(pt1.x-pt0.x) + (pt1.y-pt0.y)*(pt1.y-pt0.y) )