I have a set of points through which I have drawn a path.
let path=svg.append("path").attr("id","path")
.data([points])
.attr("d", d3.line()
.curve(d3.curveCatmullRom));
I now want to get the distance of the path between the points so that I can break it into segments. I have tried incrementing the distance and checking if the points (rounded off to 5) match with that of the initial set of points and thereby getting the distance whenever there is a match. I then save the points until there as a list.
Here, xyArray has the list of points to which I append d and the list seg as well.
function distanceBetween2Points(path, xyArray) {
let distance = 0;
xyArray.forEach(function(d,i)
{
let flag=1;
seg=[];
while(flag)
{let pt=path.getPointAtLength(distance);
if(round5(pt.x)==round5(d.x) && round5(pt.y)==round5(d.y))
{console.log("d",i);
d.d=distance;
d.seg=seg;
flag=0;
break;}
seg.push([pt.x,pt.y]);
distance++;}
return 0;
});
}
It sometimes works (even though not accurately) but sometimes does not, depending on the data. Is there a better way to get the distance?
This is a demo using vanilla javascript not d3 but I hope you'll find it useful.
The function getLengthForPoint(p,thePath)is calculating the distance on the path for a given point p.
I'm setting a variable let precision = 100;. Depending on the length of the path you may want to change this value to something else.
Also keep in mind that a path can pass through the same point multiple times. This can be tricky and can give you an error.
Also as you may know you will get the approximated distance to a point. In this example the point p1 = {x:93.5,y:60}. The point at the calculated length has this coords: {x:93.94386291503906,y: 59.063079833984375}
// some points on the path
let p1 = {x:93.5,y:60}
let p2 = {x:165,y:106}
//the total length of the path
let pathLength = thePath.getTotalLength();
let precision = 100;
let division = pathLength / precision;
function getLengthForPoint(p,thePath){
let theRecord = pathLength;
let theSegment;
for (let i = 0; i < precision; i++) {
// get a point on the path for thia distance
let _p = thePath.getPointAtLength(i * division);
// get the distance between the new point _p and the point p
let theDistance = dist(_p, p);
if (theDistance < theRecord) {
// if the distance is smaller than the record set the new record
theRecord = theDistance;
theSegment = i;
}
}
return(theSegment * division);
}
let theDistanceOnThePath = getLengthForPoint(p1,thePath);
//if you calculate the coords of a point at the calculated distance you'll see that is very near the point
console.log(thePath.getPointAtLength(theDistanceOnThePath));
let theDistanceBetween2PointsOnThePath = getLengthForPoint(p2,thePath) - getLengthForPoint(p1,thePath);
// a helper function to measure the distance between 2 points
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
svg{border:solid}
<svg viewBox="0 10 340 120">
<path id="thePath" fill="none" stroke="black" d="M10, 24Q10,24,40,67Q70,110,93.5,60Q117,10,123.5,76Q130,142,165,106Q200,70,235,106.5Q270,143, 320,24"></path>
<circle cx="93.5" cy="60" r="2" fill="red"/>
<circle cx="165" cy="106" r="2" fill="red"/>
</svg>
To get the distance between 2 points on the path you can do:
let theDistanceBetween2PointsOnThePath = getLengthForPoint(p2,thePath) - getLengthForPoint(p1,thePath);
Related
Given is an SVG element which has a rotation transformation among others.
Transformations can be iterated in JS, finding the rotation transformation by its type SVGTransform.SVG_TRANSFORM_ROTATE. The angle can be extracted using SVGTransform.angle.
Everything else is encoded in SVGTransform.matrix.
How to reconstruct the rotation center offset from the transform object?
console.clear();
let r = document.querySelector('svg rect');
let transforms = r.transform.baseVal;
for(let i=0; i<transforms.length; i++)
{
let t=transforms[i];
if(t.type==SVGTransform.SVG_TRANSFORM_TRANSLATE)
{
console.log('Found translate '+t.matrix.e+', '+t.matrix.f);
}
else if(t.type==SVGTransform.SVG_TRANSFORM_ROTATE)
{
console.log('Found rotation');
console.log(' - angle is '+t.angle); //30
console.log(' - cx is '+'???'); //50 wanted
console.log(' - cy is '+'???'); //25 wanted
}
}
<svg width="300" height="200">
<rect style="fill: blue;" width="100" height="50" transform="translate(30 30) rotate(30 50 25)" />
</svg>
After logging t it seems there is no straightforward way to get back cx and cy.
But we may calculate it from some equations.
The center of rotation is the only point that maps to itself under a rotation (if the angle is not zero or equivalent).
If the center is located at x,y , it needs to satisfy the following equation involving matrix multiplication:
One can solve the two equations a*x+c*y+e=x and b*x+d*y+f=y. After some further calculations it's possible to find x and y:
console.clear();
let r = document.querySelector('svg rect');
let transforms = r.transform.baseVal;
for(let i=0; i<transforms.length; i++)
{
let t=transforms[i];
if(t.type==SVGTransform.SVG_TRANSFORM_TRANSLATE)
{
console.log('Found translate '+t.matrix.e+', '+t.matrix.f);
}
else if(t.type==SVGTransform.SVG_TRANSFORM_ROTATE)
{
console.log('Found rotation');
let{a,b,c,d,e,f}= t.matrix ;
let denominator = a - a*d + b*c+d-1
let x = ((d-1)*e-c*f)/denominator
let y = ((a-1)*f-b*e)/denominator
console.log(t, 'angle' in t )
console.log(' - angle is '+t.angle); //30
console.log(' - cx is '+ x); // 50.00000000000001
console.log(' - cy is '+ y); // 25.000000000000004
}
}
<svg width="300" height="200">
<rect style="fill: blue;" width="100" height="50" transform="translate(30 30) rotate(30 50 25)" />
</svg>
As you can see, while the results are really close to the original, it can't exactly recover the input. But then you probably have no more than 4 decimal places in the input, you may want to use something like +cx.toFixed(4).
Also, if the angle is 0 or other multiples of 360 degrees, the center cannot be recovered, as such rotation is just identity matrix, any center gives the same results. Depending on your use case, it might be better to save the data about cx, cy etc. somewhere and write to the rectangle based on that, rather than getting the transforms from the rectangle and trying to recover them.
I'm trying to draw a noisy line (using perlin noise) between two specific points.
for example A(100, 200) and B(400,600).
The line could be a points series.
Drawing random noisy line is so clear but I dont know how can I calculate distance specific points.
working of P5.js.
I don't have any code written yet to upload.
Please can anyone help me?
I tried to add sufficient comments that you would be able to learn how such a thing is done. There are a number of things that you should make yourself aware of if you aren't already, and it's hard to say which if these you're missing:
for loops
drawing lines using beginShape()/vertex()/endShape()
Trigonometry (in this case sin/cos/atan2) which make it possible to find angles and determine 2d offsets in X and Y components at a given angle
p5.Vector() and its dist() function.
// The level of detail in the line in number of pixels between each point.
const pixelsPerSegment = 10;
const noiseScale = 120;
const noiseFrequency = 0.01;
const noiseSpeed = 0.1;
let start;
let end;
function setup() {
createCanvas(400, 400);
noFill();
start = createVector(10, 10);
end = createVector(380, 380);
}
function draw() {
background(255);
let lineLength = start.dist(end);
// Determine the number of segments, and make sure there is at least one.
let segments = max(1, round(lineLength / pixelsPerSegment));
// Determine the number of points, which is the number of segments + 1
let points = 1 + segments;
// We need to know the angle of the line so that we can determine the x
// and y position for each point along the line, and when we offset based
// on noise we do so perpendicular to the line.
let angle = atan2(end.y - start.y, end.x - start.x);
let xInterval = pixelsPerSegment * cos(angle);
let yInterval = pixelsPerSegment * sin(angle);
beginShape();
// Always start with the start point
vertex(start.x, start.y);
// for each point that is neither the start nor end point
for (let i = 1; i < points - 1; i++) {
// determine the x and y positions along the straight line
let x = start.x + xInterval * i;
let y = start.y + yInterval * i;
// calculate the offset distance using noice
let offset =
// The bigger this number is the greater the range of offsets will be
noiseScale *
(noise(
// The bigger the value of noiseFrequency, the more erretically
// the offset will change from point to point.
i * pixelsPerSegment * noiseFrequency,
// The bigger the value of noiseSpeed, the more quickly the curve
// fluxuations will change over time.
(millis() / 1000) * noiseSpeed
) - 0.5);
// Translate offset into x and y components based on angle - 90°
// (or in this case, PI / 2 radians, which is equivalent)
let xOffset = offset * cos(angle - PI / 2);
let yOffset = offset * sin(angle - PI / 2);
vertex(x + xOffset, y + yOffset);
}
vertex(end.x, end.y);
endShape();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
This code makes jaggy lines, but they could be smoothed using curveVertex(). Also, making the line pass through the start and end points exactly is a little tricky because the very next point may be offset by a large amount. You could fix this by making noiseScale very depending on how far from an endpoint the current point is. This could be done by multiplying noiseScale by sin(i / points.length * PI) for example.
I am trying to change "d" attribute of all lines in certain SVG map to make straigh lines curved.
d="M514 222L488 66"
Is there any universal algorithm to change any straigt line "d" attribute (like this one above) and get curved line as result?
This is how I would do it: For the curve (a quadratic Bézier curve Q) I need to calculate the position of the control point. In this case I want the control point in the middle of the line at a distance R.
Please read the comments in the code to understand it.
// a variable to define the curvature
let R = 50;
// the points of the original line
let linePoints = [
{x:514,y:222},
{x:488,y:66}
]
//the length of the line
let length = thePath.getTotalLength();
//a point in the middle of the line
let point = thePath.getPointAtLength(length/2);
// calculate the angle of the line
let dy = linePoints[1].y - linePoints[0].y;
let dx = linePoints[1].x - linePoints[0].x;
let angle = Math.atan2(dy,dx);
let cp = {//control point for the bézier as a perpendicular line to thePath
x:point.x + R*Math.cos(angle + Math.PI/2),
y:point.y + R*Math.sin(angle + Math.PI/2)
}
//the new d attribute for the path
let d = `M${linePoints[0].x}, ${linePoints[0].y} Q${cp.x},${cp.y} ${linePoints[1].x}, ${linePoints[1].y}`;
//set the new d attribute
thePath.setAttributeNS(null,"d",d)
svg {
border: 1px solid;
width: 100vh;
}
path {
stroke: black;
fill: none;
}
<svg viewBox = "300 0 400 300">
<path id="thePath" d="M514, 222L488, 66" />
</svg>
I have an SVG element like so:
<ellipse class="solidLine" cx="649.9" cy="341.09" rx="39.49" ry="8.41"/>
and I need to find its length so that I can animate it's dashoffset to have it being "drawn".
I've done these calculations with the following tags: line polyline circle path, but now I need to calculate the length of an ellipse and I'm a little.. stuck. I've done some googling and can't seem to find a Javascript way to calculate the circumference of an ellipse.
Any help? Here's my function format so far:
const getEllipseLength = (ellipse) => {
let rx = ellipse.getAttribute('rx');
let ry = ellipse.getAttribute('ry');
let totalLength = //function to calculate ellipse circumference using radius-x (rx) and radius-y (ry) here!
return totalLength;
};
Thanks!
edit: if there's a quick way to do this that isn't 100% accurate (but close) that would be fine also. I just need to get in the ballpark of the actual circumference in order to do a smooth animation.
edit 2: I think using this equation will give me a close estimate without having to delve into Euler's series shenanigans.. gonna translate it into Javascript and see if it works.
Alright this was actually easier than I thought.. welp.
Since I only needed an approximate length I translated this equation into Javascript and came up with this:
const getEllipseLength = (ellipse) => {
let rx = parseInt(ellipse.getAttribute('rx'));
let ry = parseInt(ellipse.getAttribute('ry'));
let h = Math.pow((rx-ry), 2) / Math.pow((rx+ry), 2);
let totalLength = (Math.PI * ( rx + ry )) * (1 + ( (3 * h) / ( 10 + Math.sqrt( 4 - (3 * h) )) ));
return totalLength;
};
When used with rx="39.49" and ry="8.41" it gave me a value of 164.20811705227723, and google tells me the actual circumference is about 166.79. Not too bad, and just fine for SVG animation.
Note that that approximation works good for circle-like ellipses and gives significant error for long ones (with high a/b ratio).
If you aware about the second case, use iterative Gauss-Kummer approach
A = Pi * (a + b) * (1 + h^2/4 + h^4/64 + h^6/256...)
summing until next addend h^k/2^m becomes small enough
There is a workaround. Using this code:
<path
d="
M cx cy
m -rx, 0
a rx,ry 0 1,1 (rx * 2),0
a rx,ry 0 1,1 -(rx * 2),0
"
/>
We can create a path that's similar to the ellipse. Then, it's just a matter of using getTotalLength(). Check the demo snippet (I'm using a different cx and cy just to save some SVG space):
var length = document.getElementById("path").getTotalLength();
console.log(length)
<svg width="200" height="200">
<path id="path" fill="none" stroke="black" stroke-width="1" d="M 49.9, 41.09 m -39.49, 0 a 39.49,8.41 0 1,0 78.98,0 a 39.49,8.41 0 1,0 -78.98,0"/>
</svg>
It logs 166.82369995117188, which is very close to 166.79 (the circumference calculated by Google's tool) .
Here's my best approximation for the ellipse perimeter:
e = (a - b) / a
P = (1 - e) * (𝜋 * ((𝜋/2) * a + (2-(𝜋/2)) * b)) + (e * 4 * a)
function getEllipseLength (ellipse) {
let a = ellipse.getAttribute('rx');
let b = ellipse.getAttribute('ry');
if (a < b) {
var t = a;
a = b;
b = t;
}
var hpi = Math.PI/2;
var e = (a - b) / a; // e = 0 circle or 1 = line
return (1 - e) * (Math.PI * (hpi*a + (2-hpi)*b)) + (e * 4 * a);
}
I'm trying to determine a SVG path draw orientation. I'm working on something like this
var length = path.getTotalLength();
var horizontal = path.getPointAtLength(length/4).x - path.getPointAtLength(0).x;
var vertical = path.getPointAtLength(length/4).y - path.getPointAtLength(0).y;
Then do some comparisons with these values horizontal > 0 and vertical > 0, but this above idea isn't, in my mind, very successful.
My question is: is there anything I can use to determine the draw direction or perhaps some built in SVG methods/options?
Thank you
Use Math.atan2(yDiff, xDiff) to get the angle between the two reference points. Two visually identical shapes that go in opposite directions will have an angle difference of pi.
Be aware of the edge case where your two reference points are unluckily the same point. Not likely, especially given rounding errors, but keep it in mind in case you need this to be rock solid.
var paths = document.getElementsByTagName("path");
for (var pathNum = 0; pathNum < paths.length; pathNum += 1) {
var path = paths[pathNum];
var message = document.createElement('p');
message.innerHTML = "path #" + pathNum + ": angle = " + pathDir(path);
document.body.appendChild(message);
};
function pathDir(path) {
var length = path.getTotalLength();
var pt14 = path.getPointAtLength(1/4 * length);
var pt34 = path.getPointAtLength(3/4 * length);
var angle = Math.atan2(pt14.y - pt34.y, pt14.x - pt34.x);
return angle;
}
<svg width="300" height="80">
<g fill="none" stroke="black" stroke-width="4">
<path d="M 10,10 C 90,10 -30,60 50,60Z"/>
<path d="M110,10 C190,10 70,60 150,60Z"/>
<path d="M250,60 C170,60 290,10 210,10Z"/>
</g>
</svg>
<div></div>