Javascript: How to determine a SVG path draw direction? - javascript

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>

Related

Get pivot cx / cy from rotation SVGMatrix

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.

Percentage animation on the circle

I have a code that displays the percentage as a circle. Is it possible to do something to make the animation start from the top, to the right, and not like now, it starts from the right. Is it possible to round this line? Is there any other, better code to do something like that? I'm only interested in vanillaJS.
var circle = document.querySelector('circle');
var radius = circle.r.baseVal.value;
var circumference = radius * 2 * Math.PI;
circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = circumference;
function setProgress(percent) {
var offset = circumference - percent / 100 * circumference;
circle.style.strokeDashoffset = offset;
}
setProgress(60);
<svg class="progress-ring" width="120" height="120">
<circle class="progress-ring__circle" stroke="#000" stroke-width="8" fill="transparent" r="56" cx="60" cy="60">
</svg>
As I've commented you may rotate the svg element transform:rotate(-90deg). Alternatively you may rotate the circle. Also you can use a path instead of a circle and make it start at the top.
If you want to use a path this is how you do it:
In this case the path starts at the top M60,4
Next comes an arc where both radiuses are 56. The first arc ends at 60,116
Follows a second arc A56,56,0 0 1 60,4 and finnaly you close the path z
For the circumference you don't need to know the radius. You can do var circumference = circle.getTotalLength(); where getTotalLength is a method that is returning the total length of a path.
var circle = document.querySelector('path');
var circumference = circle.getTotalLength();
circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = circumference;
function setProgress(percent) {
var offset = circumference - percent / 100 * circumference;
circle.style.strokeDashoffset = offset;
}
setProgress(60);
<svg class="progress-ring" width="120" height="120">
<path fill="none" class="progress-ring__circle" stroke="black" stroke-linecap="round" stroke-width="8" d="M60,4A56,56,0 0 1 60,116A56,56,0 0 1 60,4z" />
</svg>
First of all, Welcome on StackOverflow.
I think you have a trigonometry problem here. You have a trigonometric circle with your code and it start like others trigonometric circles at the right :
A simple solution is to rotate your circle with CSS :
svg{
transform: rotate(-90deg);
}

d3 javascript, get distance between two points in an svg path

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);

D3 How to keep element same size while transform scale / translate

This example illustrates my problem: https://bl.ocks.org/feketegy/ce9ab2efa9439f3c59c381f567522dd3
I have a couple of paths in a group element and I want to pan/zoom these elements except the blue rectangle path, which is in another group element.
The zooming and panning is done by applying transform="translate(0,0) scale(1) to the outer most group element then capturing the zoom delta and applying it to the same-size group element to keep it the same size.
This is working, but the blue rectangle position, which should remain the same size, is messed up, I would like to keep it in the same relative position to the other paths.
The rendered html structure looks like this:
<svg width="100%" height="100%">
<g class="outer-group" transform="translate(0,0)scale(1)">
<path d="M100,100 L140,140 L200,250 L100,250 Z" fill="#cccccc" stroke="#8191A2" stroke-width="2px"></path>
<path d="M400,100 L450,100 L450,250 L400,250 Z" fill="#cccccc" stroke="#8191A2" stroke-width="2px"></path>
<g class="same-size-position" transform="translate(300,250)">
<g class="same-size" transform="scale(1)">
<path d="M0,0 L50,0 L50,50 L0,50 Z" fill="#0000ff"></path>
</g>
</g>
</g>
</svg>
I've tried to get the X/Y position of the same-size-position group and create a delta from the translate x/y of the outer-group, but that doesn't seem to work.
After dusting off my high school geometry books I found a solution.
You need to get the bounding box of the element you want to keep the same size of and calculate a matrix conversion on it like so:
const zoomDelta = 1 / d3.event.transform.k;
const sameSizeElem = d3.select('.same-size');
const bbox = sameSizeElem.node().getBBox();
const cx = bbox.x + (bbox.width / 2);
const cy = bbox.y + (bbox.height / 2);
const zx = cx - zoomDelta * cx;
const zy = cy - zoomDelta * cy;
sameSizeElem
.attr('transform', 'matrix(' + zoomDelta + ', 0, 0, ' + zoomDelta + ', ' + zx + ', ' + zy + ')');
The matrix transformation will keep the relative position of the element which size remains the same and the other elements will pan/zoom.

SVG find orientation(angle) of a point to the x axis on a path

I am making parallax by moving an object on a path and it is working fine with getPointAtlength() but I also need to rotate this object with the path.
I need something like getPointAtLength() but for angles that I get the angle of the point. Rapheal seems to have a method to it but it isn't friendly to svg elements that is created in html or I don't know how to deal with it. Any ideas?
var l = document.getElementById('path');
var element=$('#svg_26')
$(window).scroll(function(){
var pathOffset=parseInt($('#l1').css('stroke-dashoffset'));
var p = l.getPointAtLength(-1*pathOffset);
translation = 'translate('+p.x+'px,'+p.y+'px)'
$(element).css('transform',translation);
})
Using a library for this kind of task would be overkill. Its actually quite simple to write your own function to calculate the angle. All you have to do is use pointAtLength two time with a little offset:
var p1 = path.getPointAtLength(l)
var p2 = path.getPointAtLength(l + 3)
and then calculate the angle of the resulting line and the x-axis using Math.atan2
var deg = Math.atan2(p1.y - p2.y, p1.x - p2.x) * (180 / Math.PI);
here is a little example using the above formula
var path = document.getElementById("path")
var obj = document.getElementById("obj")
var l = 0
var tl = path.getTotalLength()
function getPointAtLengthWithRotation(path, length) {
var p1 = path.getPointAtLength(length)
var p2 = path.getPointAtLength(length + 3)
var deg = Math.atan2(p1.y - p2.y, p1.x - p2.x) * (180 / Math.PI);
return {
x: p1.x,
y: p1.y,
angle: deg
}
}
setInterval(function() {
l += 1
if (l > tl) l = 0;
var p = getPointAtLengthWithRotation(path, l)
obj.setAttribute("transform", "translate(" + p.x + "," + p.y + ") rotate(" + (p.angle + 180) + ")")
}, 30)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="200" height="200">
<path id="path" d="M 81.713425,82.629068 C 77.692791,85.788547 73.298237,77.367896 68.194886,79.039107 63.091534,80.710434 58.027628,96.952068 53.04637,97.140958 48.065112,97.329732 50.503508,75.285207 45.397105,74.05952 40.290703,72.833834 38.487501,93.968537 33.85932,91.287114 29.23114,88.605807 32.245641,70.914733 29.647307,66.19971 27.048973,61.484686 19.604932,68.733636 17.542589,63.315055 15.480245,57.896474 32.172733,59.004979 32.053727,53.363216 31.93472,47.721442 8.0865997,39.989401 9.2246856,34.665848 10.362772,29.342295 28.830448,38.693055 31.065274,33.7132 33.300101,28.733334 22.734045,13.601966 26.210126,9.6067771 29.686208,5.6115765 41.809938,29.357138 46.524268,27.383715 c 4.71433,-1.973424 3.011846,-23.1001292 8.022646,-23.3332919 5.0108,-0.2331744 4.529056,18.3713929 9.45006,20.4259809 4.921003,2.054588 12.017373,-15.4803016 16.717604,-13.058602 4.700233,2.421699 -6.261038,14.180819 -2.913997,18.778859 3.347041,4.59804 12.339067,-3.78046 13.896719,1.543011 1.557652,5.323471 -9.713912,13.199372 -9.176986,18.679109 0.536926,5.479772 19.347976,2.957331 18.124596,8.213665 -1.223374,5.256392 -21.036293,1.236997 -24.253076,5.968111 -3.216785,4.731114 9.342224,14.869033 5.321591,18.028511 z"
fill="none" stroke="grey" />
<path id="obj" d="M-5 -5 L5 0L-5 5z" fill="green" />
</svg>
getPointAtLength in Raphael returns an object with attribute 'alpha'. Alpha is the angle that you need along the curve. In the example above it would be p.alpha
So you should be able to apply a rotation to the object rotated by p.alpha,
Eg..
myRaphElement.transform('t' + p.x + ',' + p.y + 'r' + p.alpha).
The last part will rotate the element around its center.
If you can't create the raph element itself as the svg is inline, I suspect you may be better off with a library like Snap.svg (which has mostly same commands as by same author), or you could possibly dynamically rotate by css transform using something like 'rotate('+l.alpha+','+l.x+','+l.y+')'
Edit: I misread as it had Raphael in the tags, when its not being used.
I personally would use Snap for this case, as Raphael doesn't add a lot here. You could possibly create a Raphael element off screen with the same path as the inline element just to use the angle, but feels like overkill to load a library for that.
In Snap you could access the element with..
myElement = Snap('#svg_26')
p = myElement.getPointAtLength(-1*pathOffset);
myElement.transform('t' + p.x + ',' + p.y + 'r' + p.alpha)
<animateMotion rotate="auto" ... performs the guidance of automatically
<svg viewBox="0 0 150 100" width="300" height="200">
<path id="path" d="M 81.713425,82.629068 C 77.692791,85.788547 73.298237,77.367896 68.194886,79.039107 63.091534,80.710434 58.027628,96.952068 53.04637,97.140958 48.065112,97.329732 50.503508,75.285207 45.397105,74.05952 40.290703,72.833834 38.487501,93.968537 33.85932,91.287114 29.23114,88.605807 32.245641,70.914733 29.647307,66.19971 27.048973,61.484686 19.604932,68.733636 17.542589,63.315055 15.480245,57.896474 32.172733,59.004979 32.053727,53.363216 31.93472,47.721442 8.0865997,39.989401 9.2246856,34.665848 10.362772,29.342295 28.830448,38.693055 31.065274,33.7132 33.300101,28.733334 22.734045,13.601966 26.210126,9.6067771 29.686208,5.6115765 41.809938,29.357138 46.524268,27.383715 c 4.71433,-1.973424 3.011846,-23.1001292 8.022646,-23.3332919 5.0108,-0.2331744 4.529056,18.3713929 9.45006,20.4259809 4.921003,2.054588 12.017373,-15.4803016 16.717604,-13.058602 4.700233,2.421699 -6.261038,14.180819 -2.913997,18.778859 3.347041,4.59804 12.339067,-3.78046 13.896719,1.543011 1.557652,5.323471 -9.713912,13.199372 -9.176986,18.679109 0.536926,5.479772 19.347976,2.957331 18.124596,8.213665 -1.223374,5.256392 -21.036293,1.236997 -24.253076,5.968111 -3.216785,4.731114 9.342224,14.869033 5.321591,18.028511 z"
fill="none" stroke="grey" />
<polygon points="0,0 -5,-5 -5,5" style="fill:green">
<animateMotion begin="0s" dur="10s" rotate="auto" repeatCount="indefinite">
<mpath xlink:href="#path"></mpath>
</animateMotion>
</polygon>
</svg>

Categories