Change a straight line to a curved line when the length is overtaken

I want to show multiple legs in a rectangular shape in a canvas. Based on the array that groups the miles of my legs, I made them algow proportionally on the given canvas.

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

var width = c.width;
var somme = 0;
var prevValue = 0;
var recapProp = [];

function drawArrow(fromx, fromy, tox, toy){
    //variables to be used when creating the arrow
    
    var headlen = 5;
    
    var angle = Math.atan2(toy-fromy,tox-fromx);
    
    //starting path of the arrow from the start square to the end square and drawing the stroke
    ctx.beginPath();
    ctx.moveTo(fromx, fromy);
    ctx.lineTo(tox, toy);
    ctx.strokeStyle = "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
    
    //starting a new path from the head of the arrow to one of the sides of the point
    ctx.beginPath();
    ctx.moveTo(tox, toy);
    ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
    
    //path from the side point of the arrow, to the other side point
    ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));
    
    //path from the side point back to the tip of the arrow, and then again to the opposite side point
    ctx.lineTo(tox, toy);
    ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
    
    //draws the paths created above
    ctx.strokeStyle = "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.fillStyle = "blue";
    ctx.fill();
}

function drawCircle(centerXFrom, centerYFrom){   
    var radius = 3;
    
    ctx.beginPath();
    ctx.arc(centerXFrom, centerYFrom, radius, 0, 2 * Math.PI, false);
    ctx.fillStyle = 'green';
    ctx.fill();
    ctx.lineWidth = 1;
    ctx.strokeStyle = '#003300';
    ctx.stroke();
    ctx.beginPath();
    
}

function sumTab(tabTT){

    for (var i = 0; i < tabTT.length; i++){
         somme += tabTT[i];
    }
    return somme;
}

function findProportion(tabTT){
    var tailleMax = tabTT.length;
    sumTab(tabTT);
    for(var i = 0; i < tabTT.length; i++){
        var percentLeg = (tabTT[i]/somme)*100;
        var tailleLeg = ((width- 20)*percentLeg)/100 ;
        recapProp.push(tailleLeg);
    }
    for(var i = 0; i <= recapProp.length; ++i){
        console.log(prevValue);
        drawCircle(prevValue +5, 5);
        drawArrow(prevValue + 7, 5, prevValue+recapProp[i],5);
        prevValue += recapProp[i];
    }
        
}

var tabTT = [0,5,1,8,2];
findProportion(tabTT);
      

<canvas id="myCanvas" height="200" width="500"></canvas>
      

Run codeHide result


Then I want to display then in a rectangular shape to create a loop (below is not rectangular, but it helps you understand):

enter image description here

I tried to manipulate quadracticCurveTo()

, but it is not entirely convincing ..

var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");

function drawArrow(fromx, fromy, tox, toy, radius){
    //variables to be used when creating the arrow    
    var headlen = 5;  
    var r = fromx + tox;
    var b = fromy + toy;
    var angle = Math.atan2(r,b);
    
    
    //starting path of the arrow from the start square to the end square and drawing the stroke
    ctx.beginPath();
    ctx.moveTo(fromx+radius, fromy);
    ctx.lineTo(r-radius, fromy);
    ctx.quadraticCurveTo(r, fromy, r, fromy+radius);
    ctx.lineWidth = "2";
    ctx.strokeStyle = '#ff0000';
    ctx.stroke();
    
    //starting a new path from the head of the arrow to one of the sides of the point
    ctx.beginPath();
    ctx.moveTo(r, b);
    ctx.lineTo(r-headlen*Math.cos(angle-Math.PI/7),b-headlen*Math.sin(angle-Math.PI/7));
    
    //path from the side point of the arrow, to the other side point
    ctx.lineTo(r-headlen*Math.cos(angle+Math.PI/7),b-headlen*Math.sin(angle+Math.PI/7));
    
    //path from the side point back to the tip of the arrow, and then again to the opposite side point
    ctx.lineTo(r, b);
    ctx.lineTo(r-headlen*Math.cos(angle-Math.PI/7),b-headlen*Math.sin(angle-Math.PI/7));
    
    //draws the paths created above
    ctx.strokeStyle = "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.fillStyle = "blue";
    ctx.fill();
}

drawArrow(50,5, 80,25, 25);
      

<canvas id="myCanvas" height="2000" width="2000"></canvas>
      

Run codeHide result


Finally, I created a snippet that I will need when I know how to curl my lines and keep it long! ... I calculated the perimeter of my canvas surface to recalculate the proportions of my legs.

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

var width = c.width;
var height = c.height;
var perimetre = (width*2 + height*2);

var up = 0;
var right = 0;
var left = 0;
var bot = 0;

var somme = 0;
var prevValue = 0;
var recapProp = [];

/**********************************/
/*****<<Straight>> Arrows*********/
/********************************/
function drawArrow(fromx, fromy, tox, toy){
    var headlen = 5;    
    var angle = Math.atan2(toy-fromy,tox-fromx);    
    ctx.beginPath();
    ctx.moveTo(fromx, fromy);
    ctx.lineTo(tox, toy);
    ctx.strokeStyle = "blue";
    ctx.lineWidth = 2;
    ctx.stroke();    
    ctx.beginPath();
    ctx.moveTo(tox, toy);
    ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));    
    ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));    
    ctx.lineTo(tox, toy);
    ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));   
    ctx.strokeStyle = "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.fillStyle = "blue";
    ctx.fill();
}


/**********************************/
/************Points***************/
/********************************/
function drawCircle(centerXFrom, centerYFrom){   
    var radius = 3;    
    ctx.beginPath();
    ctx.arc(centerXFrom, centerYFrom, radius, 0, 2 * Math.PI, false);
    ctx.fillStyle = 'green';
    ctx.fill();
    ctx.lineWidth = 1;
    ctx.strokeStyle = '#003300';
    ctx.stroke();
    ctx.beginPath();    
}


function sumTab(tabTT){
    for (var i = 0; i < tabTT.length; i++){
         somme += tabTT[i];
    }
    return somme;
}

/***************************************************/
/************Get length for each leg***************/
/*************************************************/
function findProportion(tabTT){
    var tailleMax = tabTT.length;
    sumTab(tabTT);
    
    for(var i = 0; i < tabTT.length; i++){
        var percentLeg = (tabTT[i]/somme)*100;
        var tailleLeg = ((perimetre - 20)*percentLeg)/100 ;
        recapProp.push(tailleLeg);
    }
    
    /* For each leg I draw the circle and the arrow, due to the length calculated previously. If the length > the width of the canva, the arrow has to be curved */
    for(var i = 0; i <= recapProp.length; ++i){
        if(prevValue > width && top == 1){
            drawCircle(prevValue +5, 5);
            drawArrowBot(prevValue + 7, 5, prevValue+recapProp[i],5);   
            right = 1;
            top = 0;
        }       
        else if(prevValue > height && right == 1){
            drawCircle(prevValue +5, 5);
            drawArrowLeft(prevValue + 7, 5, prevValue+recapProp[i],5);   
            bot = 1;
            right = 0;
        }
        else if (prevValue > width && bot == 1){
            drawCircle(prevValue +5, 5);
            drawArrowTop(prevValue + 7, 5, prevValue+recapProp[i],5);   
            bot = 0;
            left = 0;   
        }
        else {
            drawCircle(prevValue +5, 5);
            drawArrow(prevValue + 7, 5, prevValue+recapProp[i],5);             
        }
       
        prevValue += recapProp[i];
    }
        
}

var tabTT = [0,5,1,8,2];
findProportion(tabTT);
      

<canvas id="myCanvas" height="200" width="500"  style="border:1px solid #000000;"></canvas>
      

Run codeHide result


I have commented out all of my code to help you understand the logic and what I want.

So, can curved lines be generalized?

+3


source to share


2 answers


I would probably do something like this:

  • Define storage array with number of entries based on resolution
  • Match the strings in this setting of array 1, there will be a range of strings, 0 for space.
  • Define a target shape, such as an oval (can be any shape!), That consists of the same number of pieces as the array resolution. Store each piece and copy it to an array (same length as the line array).
  • Morph each part using interpolation between shape array and string array

Now you can create lines in almost any shape and shape you want.

Council. You can of course skip one shape by copying it directly the first time.
Tip 2: Shapes can be defined in normalized coordinates, making them easier to translate and scale.

Example



Here we define a rounded square and a circle, then we match the lines on both, we can morph between the shapes to find the combination we like and use it (note: since the square in this example starts with "top right", the corner, and not where the circle has 0 Β° there will also be a little rotation, this can be considered separately as an exercise).

A square square can be a rabbit for that matter (for a "tighter" rounded square, you can use a cubic bezier instead of a square one like here). The key point is that the shape can be determined independently of the lines themselves. It might be overkill, but it is not that difficult and versatile, i.e. generic.

See this answer for one way to add an arrow to strings.

var ctx = document.querySelector("canvas").getContext("2d"),
    resolution = 2000,
    raster = new Uint8Array(resolution),      // line raster array
    shape = new Float32Array(resolution * 2), // target shape array (x2 for x/y)
    shape2 = new Float32Array(resolution * 2),// target shape array 2
    lines = [100, 70, 180, 35],               // lines, lengths only
    tLen = 0,                                 // total length of lines + gaps
    gap = 20,                                 // gap in pixels
    gapNorm,                                  // normalized gap value for mapping
    p = 0,                                    // position in lines array
    radius = 100,                             // target circle radius
    angleStep = Math.PI * 2 / resolution,     // angle step to reach circle / res.
    cx = 150, cy = 150,                       // circle center
    interpolation = 0.5,                      // t for interpolation
    i;

// get total length of lines + gaps so we can normalize
for(i = 0; i < lines.length; i++) tLen += lines[i];
tLen += (lines.length - 2) * gap;
gapNorm = gap / tLen * 0.5;

// convert line and gap ranges to "on" in the lines array
for(i = 0; i < lines.length; i++) {
  var sx = p,                                 // start position in lines array
      ex = p + ((lines[i] / tLen) * resolution)|0; // end position in lines array (int)
  
  // fill array
  while(sx <= ex) raster[sx++] = 1;

  // update arrqay pointer incl. gap
  p = ex + ((gapNorm * resolution)|0);
}

// Create a circle target shape split into same amount of segments as lines array:
p = 0;                                        // reset pointer for shape array
for(var angle = 0; angle < Math.PI*2; angle += angleStep) {
  shape[p++] = cx + radius * Math.cos(angle);
  shape[p++] = cy + radius * Math.sin(angle);
}

// create a rounded rectangle
p = i = 0;
var corners = [
    {x1: 250, y1: 150, cx: 250, cy: 250, x2: 150, y2: 250}, // bottom-right
    {x1: 150, y1: 250, cx: 50, cy: 250, x2: 50, y2: 150},   // bottom-left
    {x1: 50, y1: 150, cx: 50, cy: 50, x2: 150, y2: 50},     // upper-left
    {x1: 150, y1: 50, cx: 250, cy: 50, x2: 250, y2: 150}    // upper-right
  ],
   c, cres = resolution * 0.25;
while(c = corners[i++]) {
  for(var t = 0; t < cres; t++) {
    var pos = getQuadraticPoint(c.x1, c.y1, c.cx, c.cy, c.x2, c.y2, t / cres);
    shape2[p++] = pos.x;
    shape2[p++] = pos.y;
  }
}


// now we can map the lines array onto our shape depending on the values
// interpolation. Make it a reusable function so we can regulate the "morph"
function map(raster, shape, shape2, t) {

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.beginPath();
  
  for(var i = 0, x, y, x1, y1, x2, y2, prev = 0; i < resolution; i++) {

    x1 = shape[i*2];
    y1 = shape[i*2 + 1];
    x2 = shape2[i*2];
    y2 = shape2[i*2 + 1];
    x = x1 + (x2 - x1) * t;
    y = y1 + (y2 - y1) * t;
    
    // do we have a change?
    if (prev !== raster[i]) {
      if (raster[i]) {  // it on, was off. create sub-path
        ctx.moveTo(x, y);
      }
      else {           // it off, was on, render and reset path
        ctx.stroke();
        ctx.beginPath();

        // create "arrow"
        ctx.moveTo(x + 3, y);
        ctx.arc(x, y, 3, 0, 6.28);
        ctx.fill();
        ctx.beginPath();
      }
    }
    
    // add segment if on
    else if (raster[i]) {
      ctx.lineTo(x, y);
    }
    
    prev = raster[i];
  }
}
ctx.fillStyle = "red";
map(raster, shape, shape2, interpolation);

document.querySelector("input").onchange = function() {
  map(raster, shape, shape2, +this.value / 100);
};

function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {

  var t1 = (1 - t),       // (1 - t)
      t12 = t1 * t1,      // (1 - t) ^ 2
      t2 = t * t,         // t ^ 2
      t21tt = 2 * t1 * t; // 2(1-t)t

  return {
    x: t12 * z0x + t21tt * cx + t2 * z1x,
    y: t12 * z0y + t21tt * cy + t2 * z1y
  }
}
      

<script src="https://cdn.rawgit.com/epistemex/slider-feedback/master/sliderfeedback.min.js"></script>

<label>Interpolation: <input type="range" min=0 max=400 value=50></label><br>
<canvas width=400 height=400></canvas>
      

Run codeHide result


+3


source


Calculate the midpoint control that makes the square BΓ©zier curve equal to the specified length.

enter image description here

Given:

  • p0

    , p2

    : the start and end points of QCurves.
  • length

    : the desired arc length of the quadratic Bezier curve.

You can compute a breakpoint that makes the total arc length of QCurve equal length

:



  • Calculate the midpoint between p0 and p2.
  • Calculate the angle between p0 and p2.
  • Calculate a point ( p1

    ) perpendicular to this midpoint at a given distance. This is a possible checkpoint. The perpendicular angle is the calculated angle from step # 2 minus 90 degrees.
  • Calculate the arc length of QCurve using p0, p1 and p2 ( calculatedLength

    ).
  • You have the correct midpoint if calculatedLength

    equal to the desired one length

    .

Here's some sample code and demo example:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
  var BB=canvas.getBoundingClientRect();
  offsetX=BB.left;
  offsetY=BB.top;        
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }


var $length=$('#length');
var PI2=Math.PI*2;
var radius=5+1; // 5==fill, 1=added stroke
var p0={x:50,y:100,color:'red'};
var p2={x:175,y:150,color:'gold'};
var p1={x:0,y:0,color:'green'};
var midpoint={x:0,y:0,color:'purple'};
var perpendicularPoint={x:0,y:0,color:'cyan'};
//var points=[p0,p1,p2];
//var draggingPoint=-1;

setQLength(p0,p2,150,1);

draw();



function draw(){
  ctx.clearRect(0,0,cw,ch);
  ctx.beginPath();
  ctx.moveTo(p0.x,p0.y);
  ctx.quadraticCurveTo(p1.x,p1.y,p2.x,p2.y);
  ctx.strokeStyle='blue';
  ctx.lineWidth=3;
  ctx.stroke();
  dot(p0);
  dot(p1);
  dot(p2);
  dot(midpoint);
  dot(perpendicularPoint)
  $length.text('Curve length: '+parseInt(QCurveLength(p0,p1,p2)))
}
//
function dot(p){
  ctx.beginPath();
  ctx.arc(p.x,p.y,radius,0,PI2);
  ctx.closePath();
  ctx.fillStyle=p.color;
  ctx.fill();
  ctx.lineWidth=1;
  ctx.strokeStyle='black';
  ctx.stroke();
}

function setQLength(p0,p2,length,tolerance){
  var dx=p2.x-p0.x;
  var dy=p2.y-p0.y;
  var alength=Math.sqrt(dx*dx+dy*dy);

  // impossible to fit
  if(alength>length){
    alert('The points are too far apart to have length='+length);
    return;
  }

  // fit
  for(var distance=0;distance<200;distance++){
    // calc the point perpendicular to midpoint at specified distance
    var p=pointPerpendicularToMidpoint(p0,p2,distance);
    p1.x=p.x;
    p1.y=p.y;
    // calc the result qCurve length
    qlength=QCurveLength(p0,p1,p2);
    // draw the curve
    draw();
    // break if qCurve length is within tolerance
    if(Math.abs(length-qlength)<tolerance){
      break;
    }
  }
  return(p1);
}


function pointPerpendicularToMidpoint(p0,p2,distance){
  var dx=p2.x-p0.x;
  var dy=p2.y-p0.y;
  var perpAngle=Math.atan2(dy,dx)-Math.PI/2;
  midpoint={ x:p0.x+dx*0.50, y:p0.y+dy*0.50, color:'purple' };
  perpendicularPoint={
    x: midpoint.x+distance*Math.cos(perpAngle),
    y: midpoint.y+distance*Math.sin(perpAngle),
    color:'cyan'        
  };
  return(perpendicularPoint);
}

// Attribution: Mateusz Matczak
// http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/
function QCurveLength(p0,p1,p2){
  var a={x: p0.x-2*p1.x+p2.x, y: p0.y-2*p1.y+p2.y}
  var b={x:2*p1.x-2*p0.x, y:2*p1.y-2*p0.y}
  var A=4*(a.x*a.x+a.y*a.y);
  var B=4*(a.x*b.x+a.y*b.y);
  var C=b.x*b.x+b.y*b.y;
  var Sabc=2*Math.sqrt(A+B+C);
  var A2=Math.sqrt(A);
  var A32=2*A*A2;
  var C2=2*Math.sqrt(C);
  var BA=B/A2;
  if(A2==0 || BA+C2==0){
    var dx=p2.x-p0.x;
    var dy=p2.y-p0.y;
    var length=Math.sqrt(dx*dx+dy*dy);
  }else{
    var length=(A32*Sabc+A2*B*(Sabc-C2)+(4*C*A-B*B)*Math.log((2*A2+BA+Sabc)/(BA+C2)))/(4*A32)
    }
  return(length);
};
      

body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
      

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id=length>Curve length:</h4>
<h4>Red,Gold == start and end points<br>Purple == midpoint between start & end<br>Cyan == middle control point.</h4>
<canvas id="canvas" width=300 height=300></canvas>
      

Run codeHide result


+2


source







All Articles