Determine bounding box of svg element from string in javascript - javascript

If I load an SVG in chrome, I am able to get bounding box of it's elements like so:
document.getElementById('char1').getBBox()
->
SVGRect {x: 348.5628356933594, y: 78.95916748046875, width: 202.74807739257812, height: 845.1696166992188}
However if i load the same SVG using DOMParser, i get a bounding box with 0 values.
EG:
const svgString = document.documentElement.outerHTML // in practice this would be an svg fetched from elsewhere
let svgElement = new DOMParser().parseFromString(svgString, 'text/xml').documentElement
svgElement.getElementById('char1').getBBox()
->
SVGRect {x: 0, y: 0, width: 0, height: 0}
a) why does this happen?
b) how do i get the correct bounding box value by using the svg string and without loading it into the document?

It isn't as pretty as it ought to be, but the solution is to temporarily add, measure, then remove it from the DOM. Because execution never returns while running these three lines of JS, the element won't actually appear on the page. Therefore, this should most likely have no side effects (unless, perhaps, you're doing something on the body with mutation observers or similar).
const toRemove = document.body.insertAdjacentElement("beforeend", svgElement);
const bounds = svgElement.getBBox();
toRemove?.remove();

As commented by #Robert Longson: getBBox() requires your element to be appended to DOM. Temporarily appending a svg (and eventually removing it) is usually the most convenient approach.
If you can't do this ...
Calculate approximated bounding box from pathData
We can get a bounding box approximation by calculating points from a pathData array
parse pathData via (polyfilled) getPathData() method. We also use the normalize:true parameter to convert all commands to absolute coordinates and concert A (arcs) to c (cubic béziers)
loop through command array to get segment end points as well as interpolated points on curves via custom pathDataToPolygonPoints() helper
We need to calculate interpolated points on curve segments to get x/y extrema.
The more interpolated points are calculated - the higher the accuracy.
loop through point array to find min/max x and y coordinates
let svgMarkup = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
<path fill="#474bff" d="M 400 297 Q 396 362 353 402.5 Q 309 451 238.5 457 Q 176 570 137 400.5 Q 106 338 69.5 289 Q 7 236 62 186 Q 91 132 135.5 93 Q 124 12 239.5 56.5 Q 342 18 366 79.5 Q 433 100 418 170 Q 403 240 400 297 Z" />
</svg>`;
let svg = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml').querySelector('svg');
let path = svg.querySelector('path');
let pathData = path.getPathData({
normalize: true
});
// init
upDataBBox();
inputAccuracy.addEventListener('input', e => {
upDataBBox()
})
function upDataBBox() {
/**
* calculate points from pathData
* interpolate additional points for curve segements
* to increase accuracy
*/
let accuracy = +inputAccuracy.value;
let polypoints = pathDataToPolygonPoints(pathData, true, accuracy);
// approximated bbox
let polyBBox = getPolygonBBox(polypoints);
// compare with getBBox()
let bb = pathInline.getBBox();
let bbox = {
x: bb.x,
y: bb.y,
width: bb.width,
height: bb.height
};
bboxOrig.textContent = beautify(JSON.stringify(bbox));
bboxPoly.textContent = beautify(JSON.stringify(polyBBox));
points.innerHTML = '';
polypoints.forEach(point => {
renderPoint(points, point)
})
}
function beautify(str) {
str = str.replaceAll('{', '').replaceAll('}', '').split(',').join('\n')
return str
}
function getPolygonBBox(polyPoints) {
let xArr = [];
let yArr = [];
polyPoints.forEach(point => {
xArr.push(point.x);
yArr.push(point.y);
})
let xmin = Math.min(...xArr);
let xmax = Math.max(...xArr);
let ymin = Math.min(...yArr);
let ymax = Math.max(...yArr);
return {
x: xmin,
y: ymin,
width: xmax - xmin,
height: ymax - ymin
}
}
/**
* convert path d to polygon point array
*/
function pathDataToPolygonPoints(pathData, addControlPointsMid = false, splitNtimes = 0, splitLines = false) {
let points = [];
// close path fix
pathData = addClosePathLineto(pathData);
pathData.forEach((com, c) => {
let type = com.type;
let values = com.values;
let valL = values.length;
// optional splitting
let splitStep = splitNtimes ? (0.5 / splitNtimes) : (addControlPointsMid ? 0.5 : 0);
let split = splitStep;
// M
if (c === 0) {
let M = {
x: pathData[0].values[valL - 2],
y: pathData[0].values[valL - 1]
};
points.push(M);
}
if (valL && c > 0) {
let prev = pathData[c - 1];
let prevVal = prev.values;
let prevValL = prevVal.length;
let p0 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
};
// cubic curves
if (type === "C") {
if (prevValL) {
let cp1 = {
x: values[valL - 6],
y: values[valL - 5]
};
let cp2 = {
x: values[valL - 4],
y: values[valL - 3]
};
let p = {
x: values[valL - 2],
y: values[valL - 1]
};
if (addControlPointsMid && split) {
// split cubic curves
for (let s = 0; split < 1 && s < 9999; s++) {
let midPoint = getPointAtCubicSegmentLength(p0, cp1, cp2, p, split);
points.push(midPoint);
split += splitStep
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
// quadratic curves
else if (type === "Q") {
if (prevValL) {
let cp1 = {
x: values[valL - 4],
y: values[valL - 3]
};
let p = {
x: values[valL - 2],
y: values[valL - 1]
};
//let coords = prevCoords.concat(values);
if (addControlPointsMid && split) {
// split cubic curves
for (let s = 0; split < 1 && s < 9999; s++) {
let midPoint = getPointAtQuadraticSegmentLength(p0, cp1, p, split);
points.push(midPoint);
split += splitStep
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
// linetos
else if (type === "L") {
if (splitLines) {
//let prevCoords = [prevVal[prevValL - 2], prevVal[prevValL - 1]];
let p1 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
}
let p2 = {
x: values[valL - 2],
y: values[valL - 1]
}
if (addControlPointsMid && split) {
for (let s = 0; split < 1; s++) {
let midPoint = interpolatedPoint(p1, p2, split);
points.push(midPoint);
split += splitStep
}
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
});
return points;
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
/**
* calculate single points on segments
*/
function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t) {
let t1 = 1 - t;
return {
x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
}
}
function getPointAtQuadraticSegmentLength(p0, cp1, p, t = 0.5) {
let t1 = 1 - t;
return {
x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y
}
}
/**
* Add closing lineto:
* needed for path reversing or adding points
*/
function addClosePathLineto(pathData) {
let pathDataL = pathData.length;
let closed = pathData[pathDataL - 1]["type"] == "Z" ? true : false;
let M = pathData[0];
let [x0, y0] = [M.values[0], M.values[1]];
let lastCom = closed ? pathData[pathDataL - 2] : pathData[pathDataL - 1];
let lastComL = lastCom.values.length;
let [xE, yE] = [lastCom.values[lastComL - 2], lastCom.values[lastComL - 1]];
if (closed && (x0 != xE || y0 != yE)) {
//console.log('add final lineto')
pathData.pop();
pathData.push({
type: "L",
values: [x0, y0]
}, {
type: "Z",
values: []
});
}
return pathData;
}
/**
* render point
* accepts coordinate array and point object
**/
function renderPoint(svg, coords, fill = "red", r = "2") {
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
body {
font-family: sans-serif
}
svg {
overflow: visible;
border: 1px solid #ccc;
width: 20em;
}
p {
white-space: pre-line;
}
<p>Accuracy: <input type="range" id="inputAccuracy" min="1" max="10" steps="1" value="1"></p>
<p><strong>BBox exact: </strong>
<span id="bboxOrig"></span></p>
<p><strong>BBox poly:</strong>
<span id="bboxPoly"></span></p>
<svg id="svgInline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="36.3 40.73 385.5 461.5">
<path id="pathInline" fill="#474bff" d="M 400 297 Q 396 362 353 402.5 Q 309 451 238.5 457 Q 176 570 137 400.5 Q 106 338 69.5 289 Q 7 236 62 186 Q 91 132 135.5 93 Q 124 12 239.5 56.5 Q 342 18 366 79.5 Q 433 100 418 170 Q 403 240 400 297 Z" />
<g id="points"></g>
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
This approach works pretty well for all geometry elements, since getpathData() can also retrieve a pathData array from primitives:
<circle>, <ellipse>,
<polygon>, <polyline>
<rect>, <line>
Doesn't work for <text> or <use> elements.

Related

Recursive golden triangle, which point does the triangles approach?

I am trying to make a rotating zooming recursive golden triangle. It draws a golden triangle, then it draws another one inside it and so on. This was easy, but the challenge is making it zoom in and rotate around the point that the triangles are approaching.
To make it zoom in on that point infinitely I need to come up with the formula to calculate which point the triangles are approaching.
Running demo at this point: https://waltari10.github.io/recursive-golden-triangle/index.html
Repository: https://github.com/Waltari10/recursive-golden-triangle
/**
*
* #param {float[]} pivot
* #param {float} angle
* #param {float[]} point
* #returns {float[]} point
*/
function rotatePoint(pivot, angle, point)
{
const s = Math.sin(angle);
const c = Math.cos(angle);
const pointOriginX = point[0] - pivot[0];
const pointOriginY = point[1] - pivot[1];
// rotate point
const xNew = (pointOriginX * c) - (pointOriginY * s);
const yNew = (pointOriginX * s) + (pointOriginY * c);
const newPoint = [
pivot[0] + xNew,
pivot[1] + yNew,
]
return newPoint;
}
// https://www.onlinemath4all.com/90-degree-clockwise-rotation.html
// https://stackoverflow.com/questions/2259476/rotating-a-point-about-another-point-2d
// Position is half way between points B and C 72 and 72, because AB/BC is golden ratio
function drawGoldenTriangle(pos, height, rotation, color = [0,255,0,255], pivot) {
// golden triangle degrees 72, 72, 36
// golden gnomon 36, 36, 108
// AB/BC is the golden ratio number
// https://www.mathsisfun.com/algebra/sohcahtoa.html
const baseLength = (Math.tan(degToRad(18)) * height) * 2;
const pointA = rotatePoint(pos, rotation, [pos[0], pos[1] - height]); // sharpest angle
const pointB = rotatePoint(pos, rotation, [pos[0] - (baseLength / 2), pos[1]]);
const pointC = rotatePoint(pos, rotation, [pos[0] + (baseLength / 2), pos[1]]);
drawTriangle(pointA, pointB, pointC, [0,255,0,255]);
}
let i = 0;
function drawRecursiveGoldenTriangle(pos, height, rotation, pivot) {
drawGoldenTriangle(pos, height, rotation, [0,255,0,255], pivot);
i++;
if (i > 10) {
return;
}
const hypotenuseLength = height / Math.cos(degToRad(18));
const baseLength = (Math.tan(degToRad(18)) * height) * 2;
const goldenRatio = hypotenuseLength / baseLength;
const newHeight = height / goldenRatio;
const newRotation = rotation - 108 * Math.PI/180
const newPointC = rotatePoint(pos, rotation, [pos[0] + (baseLength / 2), pos[1]]);
// Go half baselength up CA direction from pointC to get new position
const newHypotenuseLength = baseLength;
const newBaseLength = newHypotenuseLength / goldenRatio;
let newPosXRelative = Math.cos(newRotation) * (newBaseLength / 2)
let newPosYRelative = Math.sin(newRotation) * (newBaseLength / 2)
const newPos = [newPointC[0] + newPosXRelative, newPointC[1] + newPosYRelative];
drawRecursiveGoldenTriangle(newPos, newHeight, newRotation, [0,255,0,255], pivot);
}
let triangleHeight = height - 50;
let pivotPoint = [(width/2),(height/2) -50];
let triangleLocation = [width/2, height/2 + 300];
let triangleRotation = 0;
function loop() {
i = 0;
const startTime = Date.now()
wipeCanvasData();
// triangleHeight++;
// triangleRotation = triangleRotation + 0.005;
// drawX(pivotPoint)
// drawX(triangleLocation)
// Pivot point determines the point which the recursive golden
// triangle rotates around. Should be the point that triangles
// approach.
drawRecursiveGoldenTriangle(triangleLocation, triangleHeight, triangleRotation, pivotPoint);
updateCanvas()
const renderTime = Date.now() - startTime
timeDelta = renderTime < targetFrameDuration ? targetFrameDuration : renderTime
this.setTimeout(() => {
loop()
}, targetFrameDuration - renderTime)
}
loop()
What would be the formula to calculate the point that recursive golden triangle is approaching? Or is there some clever hack I could do in this situation?
The starting point of the logarithmic spiral is calculated by startingPoint(a,b,c) where a,b,c are the points of your triangle:
The triangle in the snippet is not a proper 'golden triangle' but the calculations should be correct...
const distance = (p1, p2) => Math.hypot(p2.x - p1.x, p2.y - p1.y);
const intersection = (p1, p2, p3, p4) => {
const l1A = (p2.y - p1.y) / (p2.x - p1.x);
const l1B = p1.y - l1A * p1.x;
const l2A = (p4.y - p3.y) / (p4.x - p3.x);
const l2B = p3.y - l2A * p3.x;
const x = (l2B - l1B) / (l1A - l2A);
const y = x * l1A + l1B;
return {x, y};
}
const startingPoint = (a, b, c) => {
const ac = distance(a, c);
const ab = distance(a, b);
const bc = distance(b, c);
// Law of cosines
const alpha = Math.acos((ab * ab + ac * ac - bc * bc) / (2 * ab * ac));
const gamma = Math.acos((ac * ac + bc * bc - ab * ab) / (2 * ac * bc));
const delta = Math.PI - alpha / 2 - gamma;
// Law of sines
const cd = ac * Math.sin(alpha / 2) / Math.sin(delta);
const d = {
x: cd * (b.x - c.x) / bc + c.x,
y: cd * (b.y - c.y) / bc + c.y
};
const e = {
x: (a.x + c.x) / 2,
y: (a.y + c.y) / 2
};
const f = {
x: (a.x + b.x) / 2,
y: (a.y + b.y) / 2,
};
return intersection(c, f, d, e);
};
d3.select('svg').append('path')
.attr('d', 'M 100,50 L150,200 H 50 Z')
.style('fill', 'none')
.style('stroke', 'blue')
const point = startingPoint({x: 50, y: 200},{x: 100, y: 50},{x: 150, y: 200});
console.log(point);
d3.select('svg').append('circle')
.attr('cx', point.x)
.attr('cy', point.y)
.attr('r', 5)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200" height="400"></svg>

SVG & JS - Light stopped by obstacle

I'm actually working on a little game and i'm trying to block map reveal with obstacle. Currently i have this :
And i of course want this :
I tried SAT.js library which let me know when there is a collision but i don't really know what yo do with this. My first idea was to create a black (or white just for hide) polygon behind the obstacle but i'm sure the is a better solution.
For this "light" effect i use trick like this in SVG :
<clippath id="clips" >
<path class="view" tokenname="" d="M 1 1000 L 250 1 L 500 1000 z"/>
</clippath>
<image xlink:href="https://i.pinimg.com/originals/46/3a/12/463a1244e2e2627c53ff9806e2012c84.jpg" width="1920" height="1080" x="0" y="0" class="one"/>
<image xlink:href="https://i.pinimg.com/originals/46/3a/12/463a1244e2e2627c53ff9806e2012c84.jpg" width="1920" height="1080" x="0" y="0" clip-path="url(#clips)"/>
Thank you for your help.
The next demo is inspired by this tutorial: 2D Raycasting;
The main idea is using the white rays as a mask for your image. Please change the stroke-width in css for a dimmer / clearer image.
Please move the mouse over the svg canvas to see it changing.
Also read the comments in the code and do not forget to see Daniel Shiffman's tutorial.
let SVG_NS = "http://www.w3.org/2000/svg";
let SVG_XLINK = "http://www.w3.org/1999/xlink";
let svg = document.querySelector("svg");
let m = { x: 0, y: 0 };// the initial mouse position
let record = 600;//the maxim length of a ray
let walls = [];//the array of the walls
//list of points for the boundary (walls)
let p1 = {
x: 300,
y: 100
};
let p2 = {
x: 200,
y: 300
};
let p3 = {
x: 10,
y: 300
};
let p4 = {
x: 300,
y: 200
};
class Particle {
constructor(pos) {
this.pos = pos;
this.rays = [];
for (let a = 0; a < 2 * Math.PI; a += Math.PI / 360) {
this.rays.push(new Ray(this.pos, a));
}
// Uncomment to visualize the particle
//let o = { cx: this.pos.x, cy: this.pos.y, r: 2, fill: "red" };
//this.element = drawSVGelmt(o, "circle", svg);
}
show(m) {
//update the position of the mouse
this.update(m.x, m.y);
//empty the group of rays
rys.innerHTML = "";
// Uncomment to visualize the particle
//let o = { cx: this.pos.x, cy: this.pos.y };
//this.element = updateSVGElmt(o, this.element);
//first cast the rays
for (let i = 0; i < this.rays.length; i++) {
for (let w = 0; w < walls.length; w++) {
this.rays[i].cast(walls[w]);
}
}
//next draw the rays and append them to the rys group
for (let i = 0; i < this.rays.length; i++) {
//if the ray is intersecting one of the walls
if (this.rays[i].intersection) {
//set the attributes of the ray line
var l = {};
l.x1 = this.pos.x;
l.y1 = this.pos.y;
l.x2 = this.rays[i].intersection.x;
l.y2 = this.rays[i].intersection.y;
//draw ray and append it to the rys group
this.line = drawSVGelmt(l, "line", rys);
}
}
}
update(x, y) {
//update all the rays inside the rys
this.rays.map((r) => {
r.update();
});
//reset the position of the particle
this.pos.x = x;
this.pos.y = y;
}
}
class Ray {
constructor(pos, a) {
this.pos = pos;//the starting point
this.angle = a;//the angle of the ray
this.maxLength = record;
this.dir = {//the direction of a ray with an initial length of 1 unit
x: Math.cos(this.angle),
y: Math.sin(this.angle)
};
}
cast(wall) {//cast the ray against the wall
let p4 = {};
p4.x = this.pos.x + this.dir.x;
p4.y = this.pos.y + this.dir.y;
// see if the ray is intersecting the wall
let Intersection = Intersect(wall.a, wall.b, this.pos, p4);
if (Intersection) {
let length = dist(this.pos, Intersection);
if (length < this.maxLength) {
this.maxLength = length;
this.intersection = Intersection;
}
}
}
// update the ray when the mouse (m) is moving
update() {
this.pos = { x: m.x, y: m.y };
this.intersection = false;
this.maxLength = record;
}
}
//the walls
class Boundary {
constructor(a, b) {
this.a = a;
this.b = b;
}
show() {
// the attributes for the line
let o = {};
o.x1 = this.a.x;
o.y1 = this.a.y;
o.x2 = this.b.x;
o.y2 = this.b.y;
o.class = "boundary";// a class to style the walls
//draw and append the wall line
this.line = drawSVGelmt(o, "line", wls);
}
}
walls.push(new Boundary(p1, p2));
walls.push(new Boundary(p3, p4));
walls.forEach((w) => {
w.show();
});
let p = new Particle(m);
p.show(m);
//HELPERS
// a function to get the intersection point of 2 lines, Returns the point of intersection or false if there is no intersection point
function Intersect(p1, p2, p3, p4) {
var denominator =
(p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
var ua =
((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) /
denominator;
var ub =
((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) /
denominator;
var x = p1.x + ua * (p2.x - p1.x);
var y = p1.y + ua * (p2.y - p1.y);
if (ua > 0 && ua < 1 && ub > 0 /*&& ub < 1*/) {
return { x: x, y: y };
} else {
return false;
}
}
// a function to draw an svg element
function drawSVGelmt(o, tag, parent) {
let elmt = document.createElementNS(SVG_NS, tag);
for (let name in o) {
if (o.hasOwnProperty(name)) {
elmt.setAttributeNS(null, name, o[name]);
}
}
parent.appendChild(elmt);
return elmt;
}
// a function to update an svg element
function updateSVGElmt(o, element) {
for (var name in o) {
if (o.hasOwnProperty(name)) {
element.setAttribute(name, o[name]);
}
}
return element;
}
//a function to get the angle of a line from p1 to p2
function getAngle(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let angle = Math.atan2(dy, dx);
return angle < 0 ? 2 * Math.PI + angle : angle;
}
//a function to get the distance between 2 points: p1 & p2
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
//a function to get the mouse position inside an svg element
function oMousePosSVG(e) {
let p = svg.createSVGPoint();
p.x = e.clientX;
p.y = e.clientY;
let ctm = svg.getScreenCTM().inverse();
p = p.matrixTransform(ctm);
return p;
}
svg.addEventListener("mousemove", function (e) {
m = oMousePosSVG(e);
p.show(m);
});
*{margin:0;padding:0;}
body{background:black;}
svg{border:1px solid silver;width:min(100vw,100vh)}
line{stroke:white;stroke-width:1px}
.boundary{stroke:white;stroke-width:2px}
<svg viewBox="0 0 400 400">
<g id="wls"></g>
<mask id="m">
<g id="rys"></g>
</mask>
<image xlink:href="https://assets.codepen.io/222579/darwin300.jpg" height="400" width="400" mask="url(#m)"></image>
</svg>

Intersection of 2 SVG Paths

I need to check if two SVG Path elements intersect. Checking for intersection of the bounding boxes with .getBBox() is too inaccurate.
What I'm currently doing is iterating both paths with .getTotalLength() and then checking if two points .getPointAtLength() are equal. Below is a snippet, but as you can see this is very slow and blocks the browser tab.
There must be a more efficient method to check for intersections between two paths.
var path1 = document.getElementById("p1");
var path2 = document.getElementById("p2");
var time = document.getElementById("time");
var btn = document.getElementById("start");
btn.addEventListener("click", getIntersection);
function getIntersection() {
var start = Date.now();
for (var i = 0; i < path1.getTotalLength(); i++) {
for (var j = 0; j < path2.getTotalLength(); j++) {
var point1 = path1.getPointAtLength(i);
var point2 = path2.getPointAtLength(j);
if (pointIntersect(point1, point2)) {
var end = Date.now();
time.innerHTML = (end - start) / 1000 + "s";
return;
}
}
}
}
function pointIntersect(p1, p2) {
p1.x = Math.round(p1.x);
p1.y = Math.round(p1.y);
p2.x = Math.round(p2.x);
p2.y = Math.round(p2.y);
return p1.x === p2.x && p1.y === p2.y;
}
svg {
fill: none;
stroke: black;
}
#start {
border: 1px solid;
display: inline-block;
position: absolute;
}
<div id="start">Start
</div>
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 10 c 120 120 120 120 120 20 z" id="p1"></path>
<path d="M 150 10 c 120 120 120 120 120 20 z" id="p2"></path>
</svg>
<div id="time"></div>
I'm not sure but it may be possible to solve this mathematically if you could extract the vectors and curves from the paths. However, your function can be optimized by caching the points from one path, and reducing the number of calls to getTotalLength and getPointAtLength.
function getIntersection() {
var start = Date.now(),
path1Length = path1.getTotalLength(),
path2Length = path2.getTotalLength(),
path2Points = [];
for (var j = 0; j < path2Length; j++) {
path2Points.push(path2.getPointAtLength(j));
}
for (var i = 0; i < path1Length; i++) {
var point1 = path1.getPointAtLength(i);
for (var j = 0; j < path2Points.length; j++) {
if (pointIntersect(point1, path2Points[j])) {
var end = Date.now();
time.innerHTML = (end - start) / 1000 + "s";
return;
}
}
}
}
This can calculate the example paths in around 0.07 seconds instead of 4-5 seconds.
jsfiddle
time 0.027s
function getIntersection2() {
function Intersect(p1, p2) {
return p1.z!==p2.z && p1.x === p2.x && p1.y === p2.y;
}
var paths = [path1,path2];
var start = Date.now(),
pathLength = [path1.getTotalLength(),path2.getTotalLength()],
pathPoints = [],
inters = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < pathLength[i]; j++) {
var p = paths[i].getPointAtLength(j);
p.z=i;
p.x=Math.round(p.x);
p.y=Math.round(p.y);
pathPoints.push(p);
}
}
pathPoints.sort((a,b)=>a.x!=b.x?a.x-b.x:a.y!=b.y?a.y-b.y:0)
// todos os pontos
.forEach((a,i,m)=>i&&Intersect(m[i-1],a)?inters.push([a.x,a.y]):0)
// somente o primeiro
//.some((a,i,m)=>i&&Intersect(m[i-1],a)?inters.push([a.x,a.y]):0);
result.innerHTML = inters;
var end = Date.now();
time.innerHTML = (end - start) / 1000 + "s";
return;
}
And this, while totally not being what the OP asked for, is what I was looking for.
A way to detect intersections over a large number of paths by sampling:
function pointIntersects(p1, p2) {
return (Math.abs(p1.x - p2.x) > 10)
? false
: (Math.abs(p1.y - p2.y) < 10)
}
function pointsIntersect(points, point) {
return _.some(points, p => pointIntersects(p, point))
}
function samplePathPoints(path) {
const pathLength = path.getTotalLength()
const points = []
for (let i = 0; i < pathLength; i += 10)
points.push(path.getPointAtLength(i))
return points
}
let pointCloud = []
_(document.querySelectorAll('svg path'))
.filter(path => {
const points = samplePathPoints(path)
if (_.some(pointCloud, point => pointsIntersect(points, point)))
return true
points.forEach(p => pointCloud.push(p))
})
.each(path => path.remove())
note: underscore/lodash has been used for brevity
You can further optimize performance by reducing the amount of sample points.
getPointAtLength() is rather expensive especially when run >100 times.
The following examples should usually need only a few milliseconds.
Example 1: Boolean result (intersecting true/false)
let svg = document.querySelector("svg");
function check() {
perfStart();
let intersections = checkPathIntersections(p0, p1, 24);
time.textContent = '1. stroke intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
//render indtersection point
gInter.innerHTML = '';
renderPoint(gInter, intersections[0], 'red', '2%');
}
function checkPathIntersections(path0, path1, checksPerPath = 24, threshold = 2) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 - threshold &&
top <= bottom1 - threshold &&
bottom >= top1 - threshold &&
right >= left1 - threshold ?
true :
false;
if (!bboxIntersection) {
return false;
}
// path0
let pathLength0 = path0.getTotalLength();
// set temporary stroke
let style0 = window.getComputedStyle(path0);
let fill0 = style0.fill;
let strokeWidth0 = style0.strokeWidth;
path0.style.strokeWidth = threshold;
// path1
let pathLength1 = path1.getTotalLength();
// set temporary stroke
let style1 = window.getComputedStyle(path1);
let fill1 = style1.fill;
let strokeWidth1 = style1.strokeWidth;
path1.style.strokeWidth = threshold;
/**
* 1. check sample point intersections
*/
let checks = 0;
let intersections = [];
/**
* 1.1 compare path0 against path1
*/
for (let c = 0; c < checksPerPath && !intersections.length; c++) {
let pt = path1.getPointAtLength((pathLength1 / checksPerPath) * c);
let inStroke = path0.isPointInStroke(pt);
let inFill = path0.isPointInFill(pt);
// check path 1 against path 2
if (inStroke || inFill) {
intersections.push(pt)
} else {
/**
* no intersections found:
* check path1 sample points against path0
*/
let pt1 = path0.getPointAtLength(
(pathLength0 / checksPerPath) * c
);
let inStroke1 = path1.isPointInStroke(pt1);
let inFill1 = path1.isPointInFill(pt1);
if (inStroke1 || inFill1) {
intersections.push(pt1)
}
}
// just for benchmarking
checks++;
}
// reset styles
path0.style.fill = fill0;
path0.style.strokeWidth = strokeWidth0;
path1.style.fill = fill1;
path1.style.strokeWidth = strokeWidth1;
console.log('sample point checks:', checks);
return intersections;
}
/**
* simple performance test
*/
function perfStart() {
t0 = performance.now();
}
function perfEnd(text = "") {
t1 = performance.now();
total = t1 - t0;
console.log(`excecution time ${text}: ${total} ms`);
return total;
}
function renderPoint(
svg,
coords,
fill = "red",
r = "2",
opacity = "1",
id = "",
className = ""
) {
//console.log(coords);
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
svg {
fill: none;
stroke: black;
}
<p><button onclick="check()">Check intersection</button></p>
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 10 c 120 120 120 120 120 20 z" id="p0"></path>
<path d="M 150 10 c 120 120 120 120 120 20 z" id="p1"></path>
<g id="gInter"></g>
</svg>
<p id="time"></p>
How it works
1. Check BBox intersections
Checking for intersection of the bounding boxes with .getBBox() is too
inaccurate.
That's true, however we should always start with a bbox intersection test to avoid unnecessary calculations.
2. Check intersection via isPointInStroke() and isPointInFill()
These natively supported methods are well optimized so we don't need to compare retrieved point arrays against each other.
By increasing the stroke-width of the paths we can also increase the tolerance threshold for intersections.
3. Reduce sample points
If we don't need all intersecting points but only a boolean value, we can drastically reduce the number of intersection checks by creating them progressively within the testing loop.
Once we found any intersection (in stroke or fill) – we stop the loop and return true.
Besides we can usually reduce the number by splitting the path length in e.g 24-100 steps.
Example 2: get all intersection points
let svg = document.querySelector("svg");
let paths = svg.querySelectorAll("path");
function check() {
// reset results
gInter2.innerHTML = '';
gInter.innerHTML = '';
time.textContent = '';
/**
* Boolean check
*/
perfStart();
let intersections = checkPathIntersections(p0, p1, 24);
time.textContent += '1. stroke intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
renderPoint(svg, intersections[0]);
perfStart();
let intersections1 = checkPathIntersections(p2, p3, 24);
time.textContent += '2. fill intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
renderPoint(svg, intersections1[0])
/**
* Precise check
*/
perfStart();
let intersections3 = checkIntersectionPrecise(p4, p5, 100, 1);
time.textContent += '3. multiple intersections: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
if (intersections3.length) {
intersections3.forEach(p => {
renderPoint(svg, p, 'red')
})
}
// no bbox intersection
perfStart();
let intersections4 = checkPathIntersections(p5, p6, 24);
time.textContent += '4. no bbBox intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
perfStart();
let intersections5 = checkIntersectionPrecise(p8, p9, 1200, 0);
time.textContent += '5. multiple intersections: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
if (intersections5.length) {
intersections5.forEach(p => {
renderPoint(gInter2, p, 'green', '0.25%');
})
}
}
function checkIntersectionPrecise(path0, path1, split = 1000, decimals = 0) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 &&
top <= bottom1 &&
bottom >= top1 &&
right >= left1 ?
true :
false;
if (!bboxIntersection) {
console.log('no intersections at all');
return false;
}
// path0
let pathData0 = path0.getPathData({
normalize: true
})
let points0 = pathDataToPolygonPoints(pathData0, true, split);
let points0Strings = points0.map(val => {
return val.x.toFixed(decimals) + '_' + val.y.toFixed(decimals)
});
// filter duplicates
points0Strings = [...new Set(points0Strings)];
// path1
let pathLength1 = path1.getTotalLength();
let pathData1 = path1.getPathData({
normalize: true
})
let points1 = pathDataToPolygonPoints(pathData1, true, split);
let points1Strings = points1.map(val => {
return val.x.toFixed(decimals) + '_' + val.y.toFixed(decimals)
});
points1Strings = [...new Set(points1Strings)];
// 1. compare
let intersections = [];
let intersectionsFilter = [];
for (let i = 0; i < points0Strings.length; i++) {
let p0Str = points0Strings[i];
let index = points1Strings.indexOf(p0Str);
if (index !== -1) {
let p1 = p0Str.split('_');
intersections.push({
x: +p1[0],
y: +p1[1]
});
}
}
// filter nearby points
if (intersections.length) {
intersectionsFilter = [intersections[0]];
let length = intersections.length;
for (let i = 1; i < length; i += 1) {
let p = intersections[i];
let pPrev = intersections[i - 1];
let diffX = Math.abs(pPrev.x - p.x);
let diffY = Math.abs(pPrev.y - p.y);
let diff = diffX + diffY;
if (diff > 1) {
intersectionsFilter.push(p)
}
}
} else {
return false
}
return intersectionsFilter;
}
/**
* convert path d to polygon point array
*/
function pathDataToPolygonPoints(pathData, addControlPointsMid = false, splitNtimes = 0, splitLines = false) {
let points = [];
pathData.forEach((com, c) => {
let type = com.type;
let values = com.values;
let valL = values.length;
// optional splitting
let splitStep = splitNtimes ? (0.5 / splitNtimes) : (addControlPointsMid ? 0.5 : 0);
let split = splitStep;
// M
if (c === 0) {
let M = {
x: pathData[0].values[valL - 2],
y: pathData[0].values[valL - 1]
};
points.push(M);
}
if (valL && c > 0) {
let prev = pathData[c - 1];
let prevVal = prev.values;
let prevValL = prevVal.length;
let p0 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
};
// cubic curves
if (type === "C") {
if (prevValL) {
let cp1 = {
x: values[valL - 6],
y: values[valL - 5]
};
let cp2 = {
x: values[valL - 4],
y: values[valL - 3]
};
let p = {
x: values[valL - 2],
y: values[valL - 1]
};
if (addControlPointsMid && split) {
// split cubic curves
for (let s = 0; split < 1 && s < 9999; s++) {
let midPoint = getPointAtCubicSegmentLength(p0, cp1, cp2, p, split);
points.push(midPoint);
split += splitStep
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
// linetos
else if (type === "L") {
if (splitLines) {
//let prevCoords = [prevVal[prevValL - 2], prevVal[prevValL - 1]];
let p1 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
}
let p2 = {
x: values[valL - 2],
y: values[valL - 1]
}
if (addControlPointsMid && split) {
for (let s = 0; split < 1; s++) {
let midPoint = interpolatedPoint(p1, p2, split);
points.push(midPoint);
split += splitStep
}
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
});
return points;
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
/**
* calculate single points on segments
*/
function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t=0.5) {
let t1 = 1 - t;
return {
x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
}
}
function checkPathIntersections(path0, path1, checksPerPath = 24, threshold = 2) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 - threshold &&
top <= bottom1 - threshold &&
bottom >= top1 - threshold &&
right >= left1 - threshold ?
true :
false;
if (!bboxIntersection) {
return false;
}
// path0
let pathLength0 = path0.getTotalLength();
// set temporary stroke
let style0 = window.getComputedStyle(path0);
let fill0 = style0.fill;
let strokeWidth0 = style0.strokeWidth;
path0.style.strokeWidth = threshold;
// path1
let pathLength1 = path1.getTotalLength();
// set temporary stroke
let style1 = window.getComputedStyle(path1);
let fill1 = style1.fill;
let strokeWidth1 = style1.strokeWidth;
path1.style.strokeWidth = threshold;
/**
* 1. check sample point intersections
*/
let checks = 0;
let intersections = [];
/**
* 1.1 compare path0 against path1
*/
for (let c = 0; c < checksPerPath && !intersections.length; c++) {
let pt = path1.getPointAtLength((pathLength1 / checksPerPath) * c);
let inStroke = path0.isPointInStroke(pt);
let inFill = path0.isPointInFill(pt);
// check path 1 against path 2
if (inStroke || inFill) {
intersections.push(pt)
} else {
/**
* no intersections found:
* check path1 sample points against path0
*/
let pt1 = path0.getPointAtLength(
(pathLength0 / checksPerPath) * c
);
let inStroke1 = path1.isPointInStroke(pt1);
let inFill1 = path1.isPointInFill(pt1);
if (inStroke1 || inFill1) {
intersections.push(pt1)
}
}
// just for benchmarking
checks++;
}
// reset styles
path0.style.fill = fill0;
path0.style.strokeWidth = strokeWidth0;
path1.style.fill = fill1;
path1.style.strokeWidth = strokeWidth1;
console.log('sample point checks:', checks);
return intersections;
}
/**
* simple performance test
*/
function perfStart() {
t0 = performance.now();
}
function perfEnd(text = "") {
t1 = performance.now();
total = t1 - t0;
console.log(`excecution time ${text}: ${total} ms`);
return total;
}
function renderPoint(
svg,
coords,
fill = "red",
r = "2",
opacity = "1",
id = "",
className = ""
) {
//console.log(coords);
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
body {
font-family: sans-serif;
}
svg {
width: 100%;
}
path {
fill: none;
stroke: #000;
stroke-width: 1px;
}
p {
white-space: pre-line;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 150">
<path id="p0" d="M27.357,21.433c13.373,3.432,21.433,17.056,18.001,30.43
c-3.432,13.374-17.057,21.434-30.43,18.002" />
<path id="p1" d="M80.652,80.414c-12.205,6.457-27.332,1.8-33.791-10.403
c-6.458-12.204-1.8-27.333,10.404-33.791" />
<path id="p2"
d="M159.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p3"
d="M191.27 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<path id="p4"
d="M259.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p5"
d="M291.27 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<path id="p6"
d="M359.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p7"
d="M420 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<g id="gInter"></g>
</svg>
<p>Based on #Netsi1964's codepen:
https://codepen.io/netsi1964/pen/yKagwx/</p>
<svg id="svg2" viewBox="0 0 2000 700">
<path d=" M 529 664 C 93 290 616 93 1942 385 C 1014 330 147 720 2059 70 C 1307 400 278 713 1686 691 " style="stroke:orange!important" stroke="orange" id="p8"/>
<path d=" M 1711 363 C 847 15 1797 638 1230 169 C 1198 443 1931 146 383 13 C 1103 286 1063 514 521 566 " id="p9"/>
<g id="gInter2"></g>
</svg>
<p><button onclick="check()">Check intersection</button></p>
<p id="time"></p>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
This example calculates sample points from a parsed pathData array - retrieved with getPathData() (needs a polyfill).
All commands are normalized/converted via
path.getPathData({normalize:true})
to absolute coordinates using only M,C,L and Z.
We can now easily calculate points on bézier C commands with an interpolation helper.
function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t=0.5) {
let t1 = 1 - t;
return {
x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
}
}
p0 = previous commands last point
cp1 = first C control point
cp2 = second control point
p = C control end point
t = split position: t=0.5 => middle of the curve
Admittedly, quite a chunk of code.
However way faster for calculating hundreds of sample points than using getPointAtLength().
Codepen example.

To find coordinates of nearest point on a line segment from a point

I need to calculate the foot of a perpendicular line drawn from a point P to a line segment AB. I need coordinates of point C where PC is perpendicular drawn from point P to line AB.
I found few answers on SO here but the vector product process does not work for me.
Here is what I tried:
function nearestPointSegment(a, b, c) {
var t = nearestPointGreatCircle(a,b,c);
return t;
}
function nearestPointGreatCircle(a, b, c) {
var a_cartesian = normalize(Cesium.Cartesian3.fromDegrees(a.x,a.y))
var b_cartesian = normalize(Cesium.Cartesian3.fromDegrees(b.x,b.y))
var c_cartesian = normalize(Cesium.Cartesian3.fromDegrees(c.x,c.y))
var G = vectorProduct(a_cartesian, b_cartesian);
var F = vectorProduct(c_cartesian, G);
var t = vectorProduct(G, F);
t = multiplyByScalar(normalize(t), R);
return fromCartesianToDegrees(t);
}
function vectorProduct(a, b) {
var result = new Object();
result.x = a.y * b.z - a.z * b.y;
result.y = a.z * b.x - a.x * b.z;
result.z = a.x * b.y - a.y * b.x;
return result;
}
function normalize(t) {
var length = Math.sqrt((t.x * t.x) + (t.y * t.y) + (t.z * t.z));
var result = new Object();
result.x = t.x/length;
result.y = t.y/length;
result.z = t.z/length;
return result;
}
function multiplyByScalar(normalize, k) {
var result = new Object();
result.x = normalize.x * k;
result.y = normalize.y * k;
result.z = normalize.z * k;
return result;
}
function fromCartesianToDegrees(pos) {
var carto = Cesium.Ellipsoid.WGS84.cartesianToCartographic(pos);
var lon = Cesium.Math.toDegrees(carto.longitude);
var lat = Cesium.Math.toDegrees(carto.latitude);
return [lon,lat];
}
What I am missing in this?
Here's a vector-based way:
function foot(A, B, P) {
const AB = {
x: B.x - A.x,
y: B.y - A.y
};
const k = ((P.x - A.x) * AB.x + (P.y - A.y) * AB.y) / (AB.x * AB.x + AB.y * AB.y);
return {
x: A.x + k * AB.x,
y: A.y + k * AB.y
};
}
const A = { x: 1, y: 1 };
const B = { x: 4, y: 5 };
const P = { x: 4.5, y: 3 };
const C = foot(A, B, P);
console.log(C);
// perpendicular?
const AB = {
x: B.x - A.x,
y: B.y - A.y
};
const PC = {
x: C.x - P.x,
y: C.y - P.y
};
console.log((AB.x * PC.x + AB.y * PC.y).toFixed(3));
Theory:
I start with the vector from A to B, A➞B. By multiplying this vector by a scalar k and adding it to point A I can get to any point C on the line AB.
I) C = A + k × A➞B
Next I need to establish the 90° angle, which means the dot product of A➞B and P➞C is zero.
II) A➞B · P➞C = 0
Now solve for k.
function closestPointOnLineSegment(pt, segA, segB) {
const A = pt.x - segA.x,
B = pt.y - segA.y,
C = segB.x - segA.x,
D = segB.y - segA.y
const segLenSq = C**2 + D**2
const t = (segLenSq != 0) ? (A*C + B*D) / segLenSq : -1
return (t<0) ? segA : (t>1) ? segB : {
x: segA.x + t * C,
y: segA.y + t * D
}
}
can.width = can.offsetWidth
can.height = can.offsetHeight
const ctx = can.getContext('2d')
const segA = {x:100,y:100},
segB = {x:400, y:200},
pt = {x:250, y:250}
visualize()
function visualize() {
ctx.clearRect(0, 0, can.width, can.height)
const t = Date.now()
pt.x = Math.cos(t/1000) * 150 + 250
pt.y = Math.sin(t/1000) * 100 + 150
segA.x = Math.cos(t / 2000) * 50 + 150
segA.y = Math.sin(t / 2500) * 50 + 50
segB.x = Math.cos(t / 3000) * 75 + 400
segB.y = Math.sin(t / 2700) * 75 + 100
line(segA, segB, 'gray', 2)
const closest = closestPointOnLineSegment(pt, segA, segB)
ctx.setLineDash([5, 8])
line(pt, closest, 'orange', 2)
ctx.setLineDash([])
dot(closest, 'rgba(255, 0, 0, 0.8)', 10)
dot(pt, 'blue', 7)
dot(segA, 'black', 7)
dot(segB, 'black', 7)
window.requestAnimationFrame(visualize)
}
function dot(p, color, w) {
ctx.fillStyle = color
ctx.fillRect(p.x - w/2, p.y - w/2, w, w)
}
function line(a, b, color, n) {
ctx.strokeStyle = color
ctx.lineWidth = n
ctx.beginPath()
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.stroke()
}
html, body { height:100%; min-height:100%; margin:0; padding:0; overflow:hidden }
canvas { width:100%; height:100%; background:#ddd }
<canvas id="can"></canvas>

How to draw Bezier curves with native Javascript code without ctx.bezierCurveTo?

I need to draw and get coordinates of bezier curves of each steps with native Javascript without ctx.bezierCurveTo method.
I found several resources, but I confused. Esspecially this looks pretty close, but I couldnt implemented clearly.
How can I accomplish this?
You can plot out the Bezier:
bezier = function(t, p0, p1, p2, p3){
var cX = 3 * (p1.x - p0.x),
bX = 3 * (p2.x - p1.x) - cX,
aX = p3.x - p0.x - cX - bX;
var cY = 3 * (p1.y - p0.y),
bY = 3 * (p2.y - p1.y) - cY,
aY = p3.y - p0.y - cY - bY;
var x = (aX * Math.pow(t, 3)) + (bX * Math.pow(t, 2)) + (cX * t) + p0.x;
var y = (aY * Math.pow(t, 3)) + (bY * Math.pow(t, 2)) + (cY * t) + p0.y;
return {x: x, y: y};
},
(function(){
var accuracy = 0.01, //this'll give the bezier 100 segments
p0 = {x: 10, y: 10}, //use whatever points you want obviously
p1 = {x: 50, y: 100},
p2 = {x: 150, y: 200},
p3 = {x: 200, y: 75},
ctx = document.createElement('canvas').getContext('2d');
ctx.width = 500;
ctx.height = 500;
document.body.appendChild(ctx.canvas);
ctx.moveTo(p0.x, p0.y);
for (var i=0; i<1; i+=accuracy){
var p = bezier(i, p0, p1, p2, p3);
ctx.lineTo(p.x, p.y);
}
ctx.stroke()
})()
Here's a fiddle http://jsfiddle.net/fQYsU/
Here is a code example for any number of points you want to add to make a bezier curve.
Here points you will pass is an array of objects containing x and y values of points.
[ { x: 1,y: 2 } , { x: 3,y: 4} ... ]
function factorial(n) {
if(n<0)
return(-1); /*Wrong value*/
if(n==0)
return(1); /*Terminating condition*/
else
{
return(n*factorial(n-1));
}
}
function nCr(n,r) {
return( factorial(n) / ( factorial(r) * factorial(n-r) ) );
}
function BezierCurve(points) {
let n=points.length;
let curvepoints=[];
for(let u=0; u <= 1 ; u += 0.0001 ){
let p={x:0,y:0};
for(let i=0 ; i<n ; i++){
let B=nCr(n-1,i)*Math.pow((1-u),(n-1)-i)*Math.pow(u,i);
let px=points[i].x*B;
let py=points[i].y*B;
p.x+=px;
p.y+=py;
}
curvepoints.push(p);
}
return curvepoints;
}

Categories

Resources