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;
};
2
Answers
The function works fine unless the line is vertical (
b
andc
become infinity).A simple workaround can be to give the line an invisible slant like so:
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:
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 only care about the y coordinates in solving this problem.