So4ne
So4ne

Reputation: 1182

Change a straight line into a curved line when length is overtaken

I want to display several legs into a rectangular form in canvas. Based on an array which groups the miles of my legs, I've made the algo to represent them proportionately on a canvas given.

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>

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

enter image description here

I've tried to manipulate quadracticCurveTo() but that's not really conclusive..

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>

Finally, I've created the snippet I will need when I'll know how to curve my lines and keep its length !. I've calculated the perimeter of my canvas surface in order to re-calculate 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>

I've commented all my code in order to help you understand the logic and what I want.

So, is it possible to curve the lines in a generic way?

Upvotes: 3

Views: 1469

Answers (3)

user12471601
user12471601

Reputation: 1

var angle = 0;

draw = function() {
    background(0, 0, 0);


    for(var j = 0;j<20;j++){
        fill(j*100,j*10,j);
        var offset = 0;
        for(var i =-27;i<20;i++){

        var a = angle +offset;
        var h = map(sin(a),-1,1,100,300);
        ellipse(i*20+j*20,h,20,20);
        offset+=10;
    }
    }

    angle+=2;
};

Upvotes: 0

user1693593
user1693593

Reputation:

I would probably do something like this:

  • Define a holding array with number of entries based on a resolution
  • Map the lines into that array setting 1's very there would be a line range, 0's for the gap.
  • Define a target shape such as an oval (can be any shape really!) which consists of equally many parts as the array resolution. Store each part and it's coordinate in an array (same length as the line array).
  • Morph each part using interpolation between the shape array and line array

Now you can produce the lines into almost any shape and form you desire.

Tip: you can of course skip one shape by mapping it directly the first time.
Tip 2: the shapes can be defined in normalized coordinates which makes it easier to translate and scale them.

Example

Here we define a rounded square and circle, then map the lines onto either, we can morph between the shapes to find a combination we like and use that (note: as the square in this example starts in "upper-right" corner and not where the circle has it's 0° there will be a small rotation as well, this can be dealt with separately as an exercise).

The rounded square could be a a bunny for that matter (for a more "tight" rounded square you can use cubic Bezier instead of quadratic as here). The key point is that the shape can be defined independently of the lines themselves. This may be overkill, but it's not so complicated and it's versatile, ie. generic.

See this answer for one way to add an arrow to the lines.

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's on, was off. create sub-path
        ctx.moveTo(x, y);
      }
      else {           // it's 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>

Upvotes: 2

markE
markE

Reputation: 105035

Calculate the middle control point that makes a quadratic Bezier curve become a specified length.

enter image description here

Given:

  • p0, p2: the QCurves starting and ending points.
  • length: the desired arc-length of the quadratic Bezier Curve.

You can calculate the control point that makes the QCurve's total arc-length equal length:

  1. Calculate the midpoint between p0 & p2.
  2. Calculate the angle of between p0 & p2.
  3. Calculate a point (p1) perpendicular to that midpoint at a specified distance. This is a possible control point. The perpendicular angle is the calculated angle from step#2 minus 90 degrees.
  4. Calculate the QCurve's arc-length using p0, p1 & p2 (calculatedLength).
  5. You've got the right middle control point if calculatedLength equals the desired length.

Here's example code and a Demo:

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's 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>

Upvotes: 2

Related Questions