Search code examples
javascriptcanvasintersectioncurve

Curve intersecting points in JavaScript


I wish to modify an image by moving columns of pixels up or down such that each column offset follows a curve.

I wish for the curve to intersect 6 or so points in some smooth way. I Imagine looping over image x co-ordinates and calling a curve function that returns the y co-ordinate for the curve at that offset, thus telling me how much to move each column of pixels.

I have investigated various types of curves but frankly I am a bit lost, I was hoping there would be a ready made solution that would allow me to plug in my point co-ords and spit out the data that I need. I'm not too fussed what kind of curve is used, as long as it looks "smooth".

Can anyone help me with this?

I am using HTML5 and canvas. The answer given here looks like the sort of thing I am after, but it refers to an R library (I guess) which is Greek to me!


Solution

  • Sigmoid curve

    A very simple solution if you only want the curve in the y direction is to use a sigmoid curve to interpolate the y pos between control points

    // where 0 <= x <= 1 and p > 1
    // return value between 0 and 1 inclusive.
    // p is power and determines the amount of curve
    function sigmoidCurve(x, p){
        x = x < 0 ? 0 : x > 1 ? 1 : x;
        var xx = Math.pow(x, p);
        return xx / (xx + Math.pow(1 - x, p))    
    }
    

    If you want the y pos at x coordinate px that is between two control points x1,y1 and x2, y2

    First find the normalized position of px between x1,x2

    var nx = (px - x1) / (x2 - x1); // normalised dist between points
    

    Plug the value into sigmoidCurve

    var c = sigmoidCurve(nx, 2); // curve power 2
    

    The use that value to calculate y

    var py = (y2 - y1) * c + y1;
    

    And you have a point on the curve between the points.

    As a single expression

    var py = (y2 - y1) *sigmoidCurve((px - x1) / (x2 - x1), 2) + y1;
    

    If you set the power for the sigmoid curve to 1.5 then it is almost a perfect match for a cubic bezier curve

    Example

    This example shows the curve animated. The function getPointOnCurve will get the y pos of any point on the curve at position x

    const ctx = canvas.getContext("2d");
    const curve = [[10, 0], [120, 100], [200, 50], [290, 150]];
    const pos = {};
    function  cubicInterpolation(x, p0, p1, p2, p3){
        x = x < 0 ? 0 : x > 1 ? 1 : x;
        return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));   
    }
    function sigmoidCurve(x, p){
        x = x < 0 ? 0 : x > 1 ? 1 : x;
    	var xx = Math.pow(x, p);
    	return xx / (xx + Math.pow(1 - x, p))    
    }
    // functional for loop
    const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
    // functional iterator 
    const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
    
    
    // find index for x in curve
    // returns pos{ index, y }
    // if x is at a control point then return the y value and index set to -1
    // if not at control point the index is of the point befor x
    function getPosOnCurve(x,curve, pos = {}){
      var len = curve.length;
      var i;
      pos.index = -1;
      pos.y = null;
      if(x <= curve[0][0]) { return (pos.y = curve[0][1], pos) }
      if(x >= curve[len - 1][0]) { return  (pos.y = curve[len - 1][1], pos) }
      i = 0;
      var found = false;
      while(!found){  // some JS optimisers will mark "Do not optimise" 
                      // code that do not have an exit clause.
        if(curve[i++][0] <x && curve[i][0] >= x) { break }
      }
      i -= 1;
      if(x === curve[i][0]) { return (pos.y = curve[i][1], pos) }
      pos.index =i
      return pos;
    }
    // Using Cubic interpolation to create the curve
    function getPointOnCubicCurve(x, curve, power){
      getPosOnCurve(x, curve, pos);
      if(pos.index === -1) { return pos.y };
      var i = pos.index;
      // get interpolation values for points around x
      var p0,p1,p2,p3;
      p1 = curve[i][1];
      p2 = curve[i+1][1];
      p0 = i === 0 ? p1 : curve[i-1][1];
      p3 = i === curve.length - 2 ? p2 : curve[i+2][1];
      // get unit distance of x between curve i, i+1
      var ux = (x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]);
      return cubicInterpolation(ux, p0, p1, p2, p3);
    }
    
    
    
    
    // Using Sigmoid function to get curve.
    // power changes curve power = 1 is line power > 1 tangents become longer
    // With the power set to 1.5 this is almost a perfect match for
    // cubic bezier solution.
    function getPointOnCurve(x, curve, power){
      getPosOnCurve(x, curve, pos);
      if(pos.index === -1) { return pos.y };
      var i = pos.index;
      var p = sigmoidCurve((x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]) ,power);
      return curve[i][1] + (curve[i + 1][1] - curve[i][1]) * p;
    }
    
    const step = 2;
    var w = canvas.width;
    var h = canvas.height;
    var cw = w / 2;  // center width and height
    var ch = h / 2;
    function update(timer){
        ctx.setTransform(1,0,0,1,0,0); // reset transform
        ctx.globalAlpha = 1;           // reset alpha
    		ctx.clearRect(0,0,w,h);
        eachOf(curve, (point) => {
          point[1] = Math.sin(timer / (((point[0] + 10) % 71) * 100) ) * ch * 0.8 + ch;
        });
        
        ctx.strokeStyle = "black";
        ctx.beginPath();
        doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCurve(x * step, curve, 1.5) - 10)});
        ctx.stroke();
        ctx.strokeStyle = "blue";
        ctx.beginPath();
        doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCubicCurve(x * step, curve, 1.5) + 10)});
        ctx.stroke();    
    
        
        ctx.strokeStyle = "black";
        eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 - 10, 4, 4) );
        eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 + 10, 4, 4) );
        requestAnimationFrame(update);
    }
    requestAnimationFrame(update);
    canvas { border : 2px solid black; }
    <canvas id="canvas"></canvas>

    Update

    I have added a second curve type to the above demo as the blue curve offset from the original sigmoid curve in black.

    Cubic polynomial

    The above function can be adapted to a variety of interpolation methods. I have added the function

    function  cubicInterpolation(x, p0, p1, p2, p3){
        x = x < 0 ? 0 : x > 1 ? 1 : x;
        return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));   
    }
    

    Which produces a curve based on the slope of the line at two points either side of x. This method is intended for evenly spaced points but still works if you have uneven spacing (such as this example). If the spacing gets too uneven you can notice a bit of a kink in the curve at that point.

    Also the curve over and under shoot may be an issue.

    For more on the Maths of cubic interpolation.