I am attempting to animate a group of SVG objects. What should be happening is the 4 coloured rectangle should move to the right, while spinning around it's center axis, but what is actually happening is a rotation around point 0,0 on the screen. Can someone help me understand what I am doing wrong?
Here is the HTML/SVG
<head></head>
<body>
<button id="startBtn">START/STOP</button>
<button id="resetBtn">RESET</button>
<svg id="thesvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1362.98 768">
<g id="rectangle">
<rect style="fill: red;" x="0" y="0" width="50" height="50" />
<rect style="fill: green;" x="50" y="0" width="50" height="50" />
<rect style="fill: yellow;" x="0" y="50" width="50" height="50" />
<rect style="fill: blue;" x="50" y="50" width="50" height="50" />
</g>
</svg>
To do this I am running the following function
var timer = null;
var started = false;
var x = 0;
var y = 0;
// Center of rectangle
var cx=50, cy=50;
var angle = 0;
/**
* Ran every tick. Should make the rectangle move diagonally right/down
* while spinning around it's center
*/
function running(){
updateRectanglePosition();
// Get the matrix of the parent element
var rect = document.getElementById('rectangle');
var ctm = rect.parentNode.getScreenCTM();
var matrix = new DOMMatrix([ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]);
// Translate the center of the group to 0,0 of parent matrix
matrix = matrix.translate(-(cx), -(cy));
// Rotate around this point
matrix = matrix.rotate(angle % 360);
// Translate to actual x,y position
matrix = matrix.translate(x , y);
rect.setAttribute('transform', matrix.inverse().toString());
if(timer){
timer = setTimeout(running, 100);
}
}
// Updates rectangle position every frame
function updateRectanglePosition()
{
x += 1;
y += 1;
angle = (angle + 10);
}
Example https://codepen.io/comfydemon/pen/jObYXYL
The way I had to modify this was to rotate the translation vector by the opposite of the amount I rotated the shape to "undo" the coordinate system that was changed by the rotation
// Translate the element to the x,y pos
let translation = new DOMMatrix();
let movement = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
// take the x and y translation values and rotate them to the opposite of the rotation of the shape
// toRadians just converts deg to rad
translation = translation.translate(movement * Math.cos(toRadians(360 - angle)),movement * Math.sin(toRadians(360 - angle)));
The original question says that 4 rectangles are only rotating but not translating. Copying the exact code revealed that 4 boxes are infact rotating and translating as well. You are translating it by only 1 pixel after 100 ms that gave the impression that object is not moving. Try increasing pixel value and as your coordinate system is reversed, you have to use negative value for x-axis to move the object towards right.
See below Working code (Desired result):
<body>
<button id="startBtn">START/STOP</button>
<svg id="thesvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1362.98 768">
<g id="rectangle">
<rect style="fill: red;" x="0" y="0" width="50" height="50" />
<rect style="fill: green;" x="50" y="0" width="50" height="50" />
<rect style="fill: yellow;" x="0" y="50" width="50" height="50" />
<rect style="fill: blue;" x="50" y="50" width="50" height="50" />
</g>
</svg>
<script>
var timer = null;
var started = false;
var x = 0;
var y = 0;
// Center of rectangle
var cx=50, cy=50;
var angle = 0;
/**
* Ran every tick. Should make the rectangle move diagonally right/down
* while spinning around it's center
*/
function running(){
updateRectanglePosition();
// Get the matrix of the parent element
var rect = document.getElementById('rectangle');
var ctm = rect.parentNode.getScreenCTM();
var matrix = new DOMMatrix([ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]);
// Translate the center of the group to 0,0 of parent matrix
matrix = matrix.translate(50, 50);
// Rotate around this point
matrix = matrix.rotate(angle % 360);
matrix = matrix.translate(x, -250);
// Translate to actual x,y position
//matrix = matrix.translate(x , y);
rect.setAttribute('transform', matrix.inverse().toString());
if(timer){
timer = setTimeout(running, 100);
}
}
// Updates rectangle position every frame
function updateRectanglePosition()
{
x -= 10;
y += 1;
angle = (angle + 10);
}
// Starts and stops the setTimeout timer that runs the animation
function startAnimation() {
started = !started;
if(started){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(running, 100);
}
else {
timer = clearTimeout(timer);
}
}
// Resets the variables
function reset(){
x = 0;
y = 0;
angle = 0;
}
document.getElementById('startBtn').addEventListener('click', startAnimation);
</script>
</body>
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.
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 came across this fiddle:
http://jsfiddle.net/wz32sy7y/1/
I'm having a hard time understanding how I would expand the circle to have a bigger radius.
I tried change the radius property r, but this desynchronizez the animation.
The radius seems to be some magical number, but I cannot determine how it's calculated.
<svg width="160" height="160" xmlns="http://www.w3.org/2000/svg">
<g>
<title>Layer 1</title>
<circle id="circle" class="circle_animation" r="79.85699"
cy="81" cx="81" stroke-width="8" stroke="#6fdb6f" fill="none"/>
</g>
</svg>
For a given radius r, the circumference of the circle is 2πr.
The values in this fiddle are slightly off due to rounding, but you can verify that the relationship holds by setting new values for the radius and circumference.
There are three places in the fiddle where the circumference is used. Once in the JavaScript:
var initialOffset = '440';
Twice in the CSS:
.circle_animation {
stroke-dasharray: 440; /* this value is the pixel circumference of the circle */
stroke-dashoffset: 440;
transition: all 1s linear;
}
Here is a version of the fiddle where the radius is set to 20 and the circumference to 2 π × 20 = 125.664:
http://jsfiddle.net/6x3rbpfu/1/
Here we set the radius to 50 and the radius to 314.159:
http://jsfiddle.net/6x3rbpfu/2/
The following fiddle will allow you to set the width arbitrarily, using the tag and the "r" attribute, and not changing your CSS every time. Try changing the value in the "r" attribute in the SVG to whatever you like.
https://jsfiddle.net/ma46yjvx/1/
Dash offset animation in SVG works by making a really long dash, using SVG's dashed outline features, and then creeping the border along that path, using an offset in pixels. It makes it look like it is drawing.
So when we scale the radius, we need to scale the amount that we offset the dash per animation step. Thus, using the same magic number the author used (dunno where it comes from, but it works!), we have this:
var time = 10;
var initialOffset = '440';
var i = 1
var r = $(".circle_animation").attr("r"); //Get the radius, so we can know the multiplier
var interval = setInterval(function() {
$('.circle_animation').css(
'stroke-dashoffset',
initialOffset-(i*(initialOffset/time)*(r/69.85699)) //Scale it!
);
$('h2').text(i);
if (i == time) {
clearInterval(interval);
}
i++;
}, 1000);