javascriptsvggeometrybezier

Intersection of Quadratic bezier path and line


I am working with a svg element that contains a quadratic bezier curve in the form of a SVG path and a vertical line. How do I calculate the intersection between them programmatically?

I have referred to this and when I plug in the numbers from this question it returns the exact same value but not for the original numbers that I have in SVG.

//referred to https://stackoverflow.com/a/27664883/11156131
// linear interpolation utility
var lerp = function(a, b, x) { return (a + x * (b - a)); };

// qCurve & line defs - as in https://stackoverflow.com/a/27664883/11156131
//works for the following
/*var p1 = {x:125,y:200};
var p2 = {x:250,y:225};
var p3 = {x:275,y:100};
var a1 = {x:30,y:125};
var a2 = {x:300,y:175};*/

// current svg  points
//does not work for following
var p1 = { x: 200, y: 300 };
var p2 = { x: 640, y: 175 };
var p3 = { x: 1080, y: 300 };
var a1 = { x: 640, y: 50 };
var a2 = { x: 640, y: 550 };


// Calculate the intersections
var points = calcQLintersects(p1, p2, p3, a1, a2);

// Display the intersection points
var textPoints = 'Intersections: ';
for (var i = 0; i < points.length; i++) {
  var p = points[i];
  textPoints += ' [' + parseInt(p.x) + ',' + parseInt(p.y) + ']';
}
console.log(textPoints);
console.log(points);

function calcQLintersects(p1, p2, p3, a1, a2) {
  var intersections = [];

  // Inverse line normal
  var normal = {
    x: a1.y - a2.y,
    y: a2.x - a1.x,
  };

  // Q-coefficients
  var c2 = {
    x: p1.x + p2.x * -2 + p3.x,
    y: p1.y + p2.y * -2 + p3.y
  };

  var c1 = {
    x: p1.x * -2 + p2.x * 2,
    y: p1.y * -2 + p2.y * 2,
  };

  var c0 = {
    x: p1.x,
    y: p1.y
  };

  // Transform to line
  var coefficient = a1.x * a2.y - a2.x * a1.y;
  var a = normal.x * c2.x + normal.y * c2.y;
  var b = (normal.x * c1.x + normal.y * c1.y) / a;
  var c = (normal.x * c0.x + normal.y * c0.y + coefficient) / a;

  // Solve the roots
  var roots = [];
  d = b * b - 4 * c;
  if (d > 0) {
    var e = Math.sqrt(d);
    roots.push((-b + Math.sqrt(d)) / 2);
    roots.push((-b - Math.sqrt(d)) / 2);
  } else if (d == 0) {
    roots.push(-b / 2);
  }

  // Calculate the solution points
  for (var i = 0; i < roots.length; i++) {
    var minX = Math.min(a1.x, a2.x);
    var minY = Math.min(a1.y, a2.y);
    var maxX = Math.max(a1.x, a2.x);
    var maxY = Math.max(a1.y, a2.y);
    var t = roots[i];
    if (t >= 0 && t <= 1) {
      // Possible point -- pending bounds check
      var point = {
        x: lerp(lerp(p1.x, p2.x, t), lerp(p2.x, p3.x, t), t),
        y: lerp(lerp(p1.y, p2.y, t), lerp(p2.y, p3.y, t), t)
      };
      var x = point.x;
      var y = point.y;
      // Bounds checks
      if (a1.x == a2.x && y >= minY && y <= maxY) {
        // Vertical line
        intersections.push(point);
      } else if (a1.y == a2.y && x >= minX && x <= maxX) {
        // Horizontal line
        intersections.push(point);
      } else if (x >= minX && y >= minY && x <= maxX && y <= maxY) {
        // Line passed bounds check
        intersections.push(point);
      }
    }
  }
  return intersections;
};
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 600" id="svg">
  <rect class="vBoxRect" width="1280" height="600" fill="#EFEFEF"></rect>
  <g id="controlPoint">
    <circle cx="640" cy="175" r="5" fill="green" fill-opacity=".5"></circle>    
  </g>
  <g id="qBez">
    <path d="M200,300Q640,175,1080,300" stroke="tomato" fill="none"></path>
  </g>
  <g id="limit">
    <circle id="upper" cx="640" cy="50" r="5" fill="red"></circle>
    <circle id="lower" cx="640" cy="550" r="5" fill="red"></circle>
    <text id="0" x="650" y="60">(640, 50)</text>
    <text id="1" x="650" y="560">(640, 550)</text>
  </g>
  <g id="vert">
    <line x1="640" y1="550" x2="640" y2="50" stroke="blue" stroke-dasharray="500,500"></line>
  </g>
</svg>

//with the same numbers from https://stackoverflow.com/a/27664883/11156131
// linear interpolation utility
var lerp = function(a, b, x) { return (a + x * (b - a)); };

// qCurve & line defs - as in https://stackoverflow.com/a/27664883/11156131
//works for the following
var p1 = {x:125,y:200};
var p2 = {x:250,y:225};
var p3 = {x:275,y:100};
var a1 = {x:30,y:125};
var a2 = {x:300,y:175};


// Calculate the intersections
var points = calcQLintersects(p1, p2, p3, a1, a2);

// Display the intersection points
var textPoints = 'Intersections: ';
for (var i = 0; i < points.length; i++) {
  var p = points[i];
  textPoints += ' [' + parseInt(p.x) + ',' + parseInt(p.y) + ']';
}
console.log(textPoints);
console.log(points);

function calcQLintersects(p1, p2, p3, a1, a2) {
  var intersections = [];

  // Inverse line normal
  var normal = {
    x: a1.y - a2.y,
    y: a2.x - a1.x,
  };

  // Q-coefficients
  var c2 = {
    x: p1.x + p2.x * -2 + p3.x,
    y: p1.y + p2.y * -2 + p3.y
  };

  var c1 = {
    x: p1.x * -2 + p2.x * 2,
    y: p1.y * -2 + p2.y * 2,
  };

  var c0 = {
    x: p1.x,
    y: p1.y
  };

  // Transform to line
  var coefficient = a1.x * a2.y - a2.x * a1.y;
  var a = normal.x * c2.x + normal.y * c2.y;
  var b = (normal.x * c1.x + normal.y * c1.y) / a;
  var c = (normal.x * c0.x + normal.y * c0.y + coefficient) / a;

  // Solve the roots
  var roots = [];
  d = b * b - 4 * c;
  if (d > 0) {
    var e = Math.sqrt(d);
    roots.push((-b + Math.sqrt(d)) / 2);
    roots.push((-b - Math.sqrt(d)) / 2);
  } else if (d == 0) {
    roots.push(-b / 2);
  }

  // Calculate the solution points
  for (var i = 0; i < roots.length; i++) {
    var minX = Math.min(a1.x, a2.x);
    var minY = Math.min(a1.y, a2.y);
    var maxX = Math.max(a1.x, a2.x);
    var maxY = Math.max(a1.y, a2.y);
    var t = roots[i];
    if (t >= 0 && t <= 1) {
      // Possible point -- pending bounds check
      var point = {
        x: lerp(lerp(p1.x, p2.x, t), lerp(p2.x, p3.x, t), t),
        y: lerp(lerp(p1.y, p2.y, t), lerp(p2.y, p3.y, t), t)
      };
      var x = point.x;
      var y = point.y;
      // Bounds checks
      if (a1.x == a2.x && y >= minY && y <= maxY) {
        // Vertical line
        intersections.push(point);
      } else if (a1.y == a2.y && x >= minX && x <= maxX) {
        // Horizontal line
        intersections.push(point);
      } else if (x >= minX && y >= minY && x <= maxX && y <= maxY) {
        // Line passed bounds check
        intersections.push(point);
      }
    }
  }
  return intersections;
};


Solution

  • What you're basically doing is finding the roots of a parabola in a rotated/transformed coordinate system. E.g. these two images are the same pair of line and curve, but one is hard to work with, and the other super easy:

             

    The reason the second is super easy is because we just need to solve the standard high school quadratic equation By(t) = liney using the quadratic formula.

    If we then also move the line down to y=0 (and the curve by the same amount) now we're literally just root finding, i.e solving By(t) = 0.

    We can do this, because Bezier curves are invariant under transformation and rotation so finding the t values for the roots in our rotates/translated setup is the same as finding the intersections in the original setup.

    So... let's just do that:

    const { atan2, cos, sin, sqrt } = Math;
    
    function getRoots(pts, [[x1,y1], [x2,y2]]) {
      // Transform and rotate our coordinates as per above,
      // noting that we only care about the y coordinate:
      const angle = atan2(y2-y1, x2-x1);
      const v = pts.map(([x,y]) => (x-x1) * sin(-angle) + (y-y1) * cos(-angle));  
      // And now we're essentially done, we can trivially find our roots: 
      return solveQuadratic(v[0], v[1], v[2])
      // ...as long as those roots are in the Bezier interval [0,1] of course.
      .filter(t => (0 <= t && t <=1));
    }
    
    function solveQuadratic(v1,v2,v3) {
      const a = v1 - 2*v2 + v3, b = 2 * (v2 - v1), c = v1;
      // quick check, is "a" zero? if so, the solution is linear. 
      if (a === 0) return (b === 0) ? [] : [-c/b];
      const u = -b/(2*a), v = b**2 - 4*a*c;
      if (v < 0) return []; // If there are no roots, there are no roots. Done.
      if (v === 0) return [u]; // If there's only one root, return that.
      // And if there are two roots we compute the "full" formula
      const w = sqrt(v)/(2*a);
      return [u + w, u - w];
    }
    
    // Let's test the coordinates from the question:
    const [[x1,y1],[x2,y2],[x3,y3]] = points = [
      [200,300],
      [640,175],
      [1080,300]
    ];
    
    const line = [
      [650,60],
      [650,550]
    ];
    
    const roots = getRoots(points, line);
    console.log(`roots: ${roots.join(`, `)}`);
    
    const coordForRoot = t => {
      const mt = 1-t;
      return [
        x1*mt**2 + 2*x2*t*mt + x3*t**2,
        y1*mt**2 + 2*y2*t*mt + y3*t**2,
      ];
    };
    
    const coordinates = roots.map(t => coordForRoot (t).map(v => v.toFixed(2)));
    console.log(`coordinates: ${coordinates.join(`, `)}`);

    As you can see, there is surprisingly little code required here, simply because we know we can turn a hard problem into a super easy problem with a change in perspective, and further simplify things by realizing we only care about the y coordinates in solving this problem.