I'm currently trying to generate a SVG path representing a sine wave that fit the width of the webpage.
The algorithm I'm currently using is drawing small line to between two point which is drawing the sine wave.
The algorithm :
for(var i = 0; i < options.w; i++) {
var operator = ' M ';
d += operator + ((i - 1) * rarity + origin.x) + ', ';
d += (Math.sin(freq * (i - 1 + phase)) * amplitude + origin.y);
if(operator !== ' L ') { operator = ' L '; }
d += ' L ' + (i * rarity + origin.x) + ', ';
d += (Math.sin(freq * (i + phase)) * amplitude + origin.y);
}
Which generates a path for the svg :
M 9, 82.66854866662797 L 10, 102.5192336707523
M 10, 102.5192336707523 L 11, 121.18508371540987
M 11, 121.18508371540987 L 12, 129.88725786264592
M 12, 129.88725786264592 L 13, 124.53298763579338
M 13, 124.53298763579338 L 14, 107.64046998532105
M 14, 107.64046998532105 L 15, 87.15451991511547
M 15, 87.15451991511547 L 16, 72.70999984499424
M 16, 72.70999984499424 L 17, 71.10039326578718
M 17, 71.10039326578718 L 18, 83.08272330249196
M 18, 83.08272330249196 L 19, 103.02151290977501
The thing is, at the end of the sinus I wanted to draw a line to close the rest of the path (with the Z)
Sorry for my drawing skills ! :D
The reason for closing the path and having a path linked is to be able to fill this path with a background or a gradient
I found that I could represent the sine waves in a single path where it's linked
M0 50 C 40 10, 60 10, 100 50 C 140 90, 160 90, 200 50 Z
Which looks like this :
But the thing is the algorithm I'm using lets me play with the sine function so that I could animate this waves (which is something I need) and I dont see how to animate the representation of the sine waves.
So to sum up, either you can help me find a way to link all the lines drawed by the actual algorithm ? or a way to animate the other representation to draw a waves without caring about the sinus.
Thanks in advance for your help !
You can animate the sine wave by just making the path the width of two wavelengths and then moving it left or right.
<svg width="200" height="100" viewBox="0 0 200 100">
<defs>
<path id="double-wave"
d="M0 50
C 40 10, 60 10, 100 50 C 140 90, 160 90, 200 50
C 240 10, 260 10, 300 50 C 340 90, 360 90, 400 50
L 400 100 L 0 100 Z" />
</defs>
<use xlink:href="#double-wave" x="0" y="0">
<animate attributeName="x" from="0" to="-200" dur="3s"
repeatCount="indefinite"/>
</use>
</svg>
I'm animating the x attribute of a <use> here because IMO it is more obvious what is going on.
What we are doing is animating the position that our two-wavelength path is rendered. Once it has moved one wavelength of distance, it jumps back to it's original position and repeats. The effect is seamless because the two waveshapes are identical. And the rest of the wave is off the edge of the SVG.
If you want to see what's going on behaind the scenes, we can make the SVG wider so you can see what's going on off to the left and right of the original SVG.
<svg width="400" height="100" viewBox="-200 0 600 100">
<defs>
<path id="double-wave"
d="M0 50
C 40 10, 60 10, 100 50 C 140 90, 160 90, 200 50
C 240 10, 260 10, 300 50 C 340 90, 360 90, 400 50
L 400 100 L 0 100 Z" />
</defs>
<use xlink:href="#double-wave" x="0" y="0">
<animate attributeName="x" from="0" to="-200" dur="3s"
repeatCount="indefinite"/>
</use>
<rect width="200" height="100" fill="none" stroke="red" stroke-width="2"/>
</svg>
Below is an example of sine wave across the width of the svg. It creates a polyline via a parametric equation. Animation can be had by adjusting the amplitude and/or phase angle.
Edit - added animation to phase angle.
<!DOCTYPE HTML>
<html>
<head>
<title>Sine Wave</title>
</head>
<body onload=amplitudeSelected() >
<div style=background:gainsboro;width:400px;height:400px;>
<svg id="mySVG" width="400" height="400">
<polyline id="sineWave" stroke="black" stroke-width="3" fill="blue" ></polyline>
</svg>
</div>
Amplitide:<select id="amplitudeSelect" onChange=amplitudeSelected() >
<option selected>10</option>
<option>20</option>
<option>30</option>
<option>40</option>
<option>50</option>
<option>60</option>
<option>70</option>
<option>80</option>
<option>90</option>
<option>100</option>
</select>
<button onClick=animatePhaseAngle();this.disabled=true >Animate Phase Angle</button>
<script>
//---onload & select---
function amplitudeSelected()
{
var startPoint=[0,400]
var endPoint=[400,400]
var originX=0
var originY=200
var width=400
var amplitude=+amplitudeSelect.options[amplitudeSelect.selectedIndex].text
var pointSpacing=1
var angularFrequency=.02
var phaseAngle=0
var origin = { //origin of axes
x: originX,
y: originY
}
var points=[]
points.push(startPoint)
var x,y
for (var i = 0; i < width/pointSpacing; i++)
{
x= i * pointSpacing + origin.x
y= Math.sin(angularFrequency*(i + phaseAngle)) * amplitude + origin.y
points.push([x,y])
}
points.push(endPoint)
sineWave.setAttribute("points",points.join(" "))
}
//---buton---
function animatePhaseAngle()
{
setInterval(animate,20)
var seg=.5
var cntr=0
var cntrAmp=0
var startPoint=[0,400]
var endPoint=[400,400]
var originX=0
var originY=200
var origin = { //origin of axes
x: originX,
y: originY
}
var width=400
var pointSpacing=1
var angularFrequency=.02
setInterval(animate,10)
function animate()
{
phaseAngle=seg*cntr++
var amplitude=+amplitudeSelect.options[amplitudeSelect.selectedIndex].text
var points=[]
points.push(startPoint)
var x,y
for (var i = 0; i < width/pointSpacing; i++)
{
x= i * pointSpacing + origin.x
y= Math.sin(angularFrequency*(i + phaseAngle)) * amplitude + origin.y
points.push([x,y])
}
points.push(endPoint)
sineWave.setAttribute("points",points.join(" "))
}
}
</script>
</body>
</html>
Related
I'm looking into ways to calculate path.getTotalLength() in Node.js and it seems that a polyfill is not available. So far I managed to compute all other pathCommands except A.
For instance, knowing last X and Y from M segment, and also having all the A segment values, how to determine the length of this path?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8 15 A7 7 0 1 0 8 1">
</svg>
Thank you
MDN has some great documentation in regards to the various path commands.
MDN > SVG Tutorial > Paths
Here is how you decompose the provided path commands:
M 8 15 = Move To (Absolute)
x = 8
y = 15
A 7 7 0 1 0 8 1 = Arc (Absolute)
rx = 7
ry = 7
x-axis-rotation = 0
large-arc-flag = 1
sweep-flag = 0
x = 8
y = 1
I followed this Mathematics Exchange post to compute the arc length, given the states path commands. Since the x and y radii of your arc are equal, this is a bit easier.
Note: I am not sure what you would need to do if they are different.
const x1 = 8, y1 = 15;
const x2 = 8, y2 = 1;
const r = 7; // Since rx === ry, this is a bit easier
const d = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
const theta = Math.acos(1 - (Math.pow(d, 2) / (2 * Math.pow(r, 2))));
const arcLength = theta * r;
console.log(arcLength); // Arc Length = 21.9911
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="
M 8 15
A 7 7 0 1 0 8 1
" fill="none" stroke="black" stroke-width="2" />
<!-- Move to and begin arc at (8, 15) -->
<circle cx="8" cy="15" r="1" stroke="none" fill="red" />
<!-- End arc at (8, 1) -->
<circle cx="8" cy="1" r="1" stroke="none" fill="cyan" />
<!-- Radius of (7, 7) -->
<circle cx="15" cy="7.5" r="1" stroke="none" fill="lime" />
<!-- Center -->
<circle cx="8" cy="7.5" r="1" stroke="none" fill="gold" />
</svg>
SVG path element for example:
<path id="path1"
d="M 160 180 C 60 140 230 20 200 170 C 290 120 270 300 200 240 C 160 390 50 240 233 196"
stroke="#009900" stroke-width="4" fill="none"/>
It has 4 svg segments (3 curve segments in human eyes):
M 160 180
C 60 140 230 20 200 170
C 290 120 270 300 200 240
C 160 390 50 240 233 196
when click on the path, I get the x and y of mouse position, then how to detect which curve segment is clicked?
function isInWhichSegment(pathElement,x,y){
//var segs = pathElement.pathSegList; //all segments
//
//return the index of which segment is clicked
//
}
There are a few methods for SVGPathElements that you can use. Not really straighforward, but you could get the total length of your path, then check at every point of length the coordinates with getPointAtLength and compare it with coordinates of the click. Once you figure the click was at which length, you get the segment at that length with getPathSegAtLength. like that for example:
var pathElement = document.getElementById('path1')
var len = pathElement.getTotalLength();
pathElement.onclick = function(e) {
console.log('The index of the clicked segment is', isInWhichSegment(pathElement, e.offsetX, e.offsetY))
}
function isInWhichSegment(pathElement, x, y) {
var seg;
// You get get the coordinates at the length of the path, so you
// check at all length point to see if it matches
// the coordinates of the click
for (var i = 0; i < len; i++) {
var pt = pathElement.getPointAtLength(i);
// you need to take into account the stroke width, hence the +- 2
if ((pt.x < (x + 2) && pt.x > (x - 2)) && (pt.y > (y - 2) && pt.y < (y + 2))) {
seg = pathElement.getPathSegAtLength(i);
break;
}
}
return seg;
}
<svg>
<path id="path1" d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" stroke="#009900" stroke-width="4" fill="none" />
</svg>
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>
I want to connect two SVG points (e.g. the centers of two circles) using arcs. If there is only one connection, the line (<path>) will be straight. If there are two connections, both will be rounded and will be symmetrical, this way:
So, in fact, there are few rules:
Everything should be symmetrical to to the imaginary line that connects the two points.
From 1, it's obvious that if the number of connections is:
odd: we do not display the straight line
even: we display the straight line
There should be a value k which defines the distance between two connections between same points.
The tangent that goes through the middle of the elliptical arc should be parallel with the straight line that connects the two points. And obviously, the middle of the line will be perpendicular to the tangent.
I'm struggling to get a formula to calculate the A parameters in the <path> element.
What I did until now is:
<path d="M100 100, A50,20 0 1,0 300,100" stroke="black" fill="transparent"/>
M100 100 is clear: that's the starting point (move to 100,100)
Last two numbers are also clear. The path ends in 300,100
I also saw that if I put 0 instead of 20, I obtain a straight line.
If I replace 1,0 with 1,1, the path is flipped.
What I don't know is how to calculate the A parameters. I read the docs, but the imagine is still unclear to me. How to calculate these values?
svg {
width: 100%;
height: 100%;
position: absolute;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<?xml version="1.0" standalone="no" ?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<!-- Connect A(100,100) with B(300, 100) -->
<path d="M100 100, A50,0 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 0 1,1 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,30 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,30 0 1,1 300,100" stroke="black" fill="transparent" />
<!-- A(100, 100) B(300, 400) -->
<path d="M100 100, A50,0 57 1,0 300,400" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 57 1,0 300,400" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 57 1,1 300,400" stroke="black" fill="transparent" />
</svg>
</body>
</html>
I'm using SVG.js to create the paths.
You're making life very difficult for yourself by requiring circular arcs.
If you use quadratic curves instead, then the geometry becomes very simple — just offset the central X coordinate by half the difference in Y coordinates, and vice versa.
function arc_links(dwg,x1,y1,x2,y2,n,k) {
var cx = (x1+x2)/2;
var cy = (y1+y2)/2;
var dx = (x2-x1)/2;
var dy = (y2-y1)/2;
var i;
for (i=0; i<n; i++) {
if (i==(n-1)/2) {
dwg.line(x1,y1,x2,y2).stroke({width:1}).fill('none');
}
else {
dd = Math.sqrt(dx*dx+dy*dy);
ex = cx + dy/dd * k * (i-(n-1)/2);
ey = cy - dx/dd * k * (i-(n-1)/2);
dwg.path("M"+x1+" "+y1+"Q"+ex+" "+ey+" "+x2+" "+y2).stroke({width:1}).fill('none');
}
}
}
function create_svg() {
var draw = SVG('drawing').size(300, 300);
arc_links(draw,50,50,250,50,2,40);
arc_links(draw,250,50,250,250,3,40);
arc_links(draw,250,250,50,250,4,40);
arc_links(draw,50,250,50,50,5,40);
draw.circle(50).move(25,25).fill('#fff').stroke({width:1});
draw.circle(50).move(225,25).fill('#fff').stroke({width:1});
draw.circle(50).move(225,225).fill('#fff').stroke({width:1});
draw.circle(50).move(25,225).fill('#fff').stroke({width:1});
}
create_svg();
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.3.2/svg.min.js"></script>
<div id="drawing"></div>
For drawing SVG path's arc you need 2 points and radius, there are 2 points and you just need to calculate radius for given distances.
Formula for radius:
let r = (d, x) => 0.125*d*d/x + x/2;
where:
d - distance between points
x - distance between arcs
it derived from Pythagorean theorem:
a here is a half of distance between points
let r = (d, x) => !x?1e10:0.125*d*d/x + x/2;
upd();
function upd() {
let n = +count.value;
let s = +step.value/10;
let x1 = c1.getAttribute('cx'), y1 = c1.getAttribute('cy');
let x2 = c2.getAttribute('cx'), y2 = c2.getAttribute('cy');
let dx = Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
paths.innerHTML = [...Array(n)].map((_, i) => [
n%2&&i===n-1?0:1+parseInt(i/2),
i%2
]).map(i => `<path d="${[
'M', x1, y1,
'A', r(dx, s*i[0]), r(dx, s*i[0]), 0, 0, i[1], x2, y2
].join(' ')}"></path>`).join('');
}
<input id="count" type="range" min=1 max=9 value=5 oninput=upd() >
<input id="step" type="range" min=1 max=200 value=100 oninput=upd() >
<svg viewbox=0,0,300,100 stroke=red fill=none >
<circle id=c1 r=10 cx=50 cy=60></circle>
<circle id=c2 r=10 cx=250 cy=40></circle>
<g id=paths></g>
</svg>
Here is a solution that uses arcs, as asked for, rather than quadratic curves.
// Internal function
function connectInternal(x1,y1,x2,y2,con){
var dx=x2-x1
var dy=y2-y1
var dist=Math.sqrt(dx*dx+dy*dy)
if(dist==0 || con==0){
return "M"+x1+","+y1+"L"+x2+","+y2
}
var xRadius=dist*0.75
var yRadius=dist*0.3*(con*0.75)
var normdx=dx/dist
if(normdx<-1)normdx=-1
if(normdx>1)normdx=1
var angle=Math.acos(normdx)*180/Math.PI
if(x1>x2){
angle=-angle
}
return "M"+x1+","+y1+"A"+xRadius+","+yRadius+","+
angle+",00"+x2+","+y2+
"M"+x1+","+y1+"A"+xRadius+","+yRadius+","+
angle+",01"+x2+","+y2
}
// Returns an SVG path that represents
// "n" connections between two points.
function connect(x1,y1,x2,y2,n){
var ret=""
var con=n
if(con%2==1){
ret+=connectInternal(x1,y1,x2,y2,con)
con-=1
}
for(var i=2;i<=con;i+=2){
ret+=connectInternal(x1,y1,x2,y2,i)
}
return ret
}
Via this forum and other sources I finally have a tidy "zoom-and-recenter" Javascript solution that fits my specific needs and requirements:
<script>
var viewPortSize = 500; // fixed for now
var viewBoxSize = 500; // the zoom factor
function updateViewBox()
{
min = (viewPortSize - viewBoxSize) / 2;
max = viewBoxSize;
str = " ";
str = str.concat( "", min );
str = str.concat( " ", min );
str = str.concat( " ", max );
str = str.concat( " ", max );
trackBox = document.getElementById("trackBox");
trackBox.setAttribute( "viewBox", str );
}
function zoomIn()
{
viewBoxSize -= 50;
updateViewBox();
}
function zoomOut()
{
viewBoxSize += 50;
updateViewBox();
}
</script>
The target of this operation is:
<svg id="trackBox" x="600"
width="500" height="500"
viewBox="0 0 500 500">
<g>
<circle id="trackPoint"
cx="250" cy="250" r="5" fill="red"/>
<path id="track" d=
"
M 250 250
L 245 225
L 200 225
L 200 250
"
stroke="blue" stroke-width="1" fill="transparent"/>
</g>
</svg>
However, when applying the updateViewBox() function, the circle radius and the path stroke-width are also magnified or reduced.
Does there exist a simple way to make these attributes invariant with respect to changing the viewBox size?