Transitions in combination with rotations have odd results.
Here is a fiddle with my problem: http://jsfiddle.net/emperorz/E3G3z/1/
Try clicking on each square to see the varying behaviour.
Please forgive the hacked code, but if I use transition with rotation (and x/y placement) then it loops about.
I have tried:
1) all in the transform (rotate then translate), and that seems mostly okay. A little wobbly.
2) just rotate in the transform, positioned using x/y attributes. Flies all over the place, but ends up at the correct spot. Very weird.
3) all in the transform (translate then rotate), flies away, and ends up in the (completely) wrong place.
Hmmm. Strange.
Is there a correct approach to rotating shapes with transitions?
Intuitively, it would be good if the second option worked.
Thanks
To rotate an SVG object on an arbitrary axis, you need two transformations: translate (to set the axis) and rotate. What you really want is to apply the translate fully first and then rotate the already moved element, but it appears that translate and rotate operate independently and simultaneously. This ends at the right place, but animating the translate is essentially moving the axis during rotation, creating the wobble. You can isolate the translate from the rotate by having them occur at separate places in the SVG element hierarchy. For example, take a look at the following:
<g class="outer">
<g class="rect-container">
<rect class="rotate-me" width=200 height=100 />
</g>
</g>
You can center the <rect> on (0,0) with translate (-100, -50). It will wobble if you apply your rotation to the <rect> element, but it will rotate cleanly if you rotate the g.rect-container element. If you want to reposition, scale, or otherwise transform the element further, do so on g.outer. That's it. You now have full control of your transforms.
Finding a <rect>'s center is easy, but finding the center of a <path>, <g>, etc. is much harder. Luckily, a simple solution is available in the .getBBox() method (code in CoffeeScript; see below for a JavaScript version*):
centerToOrigin = (el) ->
boundingBox = el.getBBox()
return {
x: -1 * Math.floor(boundingBox.width/2),
y: -1 * Math.floor(boundingBox.height/2)
}
You can now center your element/group by passing the non-wrapped element (using D3's .node() method)
group = d3.select("g.rotate-me")
center = centerToOrigin(group.node())
group.attr("transform", "translate(#{center.x}, #{center.y})")
For code that implements this on a both a single <rect> and <g> of of 2 rects with repositioning and scaling, see this fiddle.
*Javascript of the above code:
var center, centerToOrigin, group;
centerToOrigin = function(el) {
var boundingBox;
boundingBox = el.getBBox();
return {
x: -1 * Math.floor(boundingBox.width / 2),
y: -1 * Math.floor(boundingBox.height / 2)
};
};
group = d3.select("g.rotate-me");
center = centerToOrigin(group.node());
group.attr("transform", "translate(" + center.x + ", " + center.y + ")");
iirc translate is relative to 0,0 whereas rotate is around the center point of the object
As such, because your shapes are offset from 0,0 (e.g. 100,200, or 200,100) they end up migrating when translated. This can be seen by changing the offsets for Diamond3 to [50,50] - much smaller migration around the screen
The solution would be rebase the 0,0 point to the center of the diamond. There is a way to do this in D3 - but I can't remember what it is off the top of my head :(
Related
I'm making a diagramming library in Blazor which uses HTML nodes and SVG links. I am wondering how can I draw links between two nodes when they aren't always rectangular.
All the solutions I find are based on nodes that are rectangles/squares, where it's easy to draw a link on the borders (or even the center, but only works for direct links).
But what about nodes that have custom stuff in them that makes them non rectangular, for example a div with border-radius: 50%?
One possible solution is to draw the lines from/to the center of the elements, but that would only work with simple lines, curved lines would look weird.
In this example:
How does arrow position get calculated?
You need to have an container, width and height of the container, then inside the container find the x / y point of the element that you want to connect and draw a line to the next elements x / y point, the x/y points can be calculated using x,y,w,h of the element, for an example x:100 y:100 w:100 h:100 the center point sits at x:150, y:150 x = x + ( w / 2 ), y = y + ( h / 2 ).. using math just calculate the point of connection of the elements, the complexity of math for calculating the connection point is in the shape of the element, for each different shape you need a different calculation metod if not in center
First of all, yes, I have searched and I have seen the many other answers available. But they didn't work for me.
I would like to be able to center the text in my SVG.
Since I have to be able to put it left, center or right, (both horizontally and vertically) I tried to be a little bit generic and compute x and y programmatically.
Here is an example : https://codesandbox.io/s/ll6ppwkyq7
You can see in the result that the text is not vertically centered.
The red box is the bounding box of the text.
The black box is the box in which I am supposed to be centered.
The relationship between x coordinate of an element and the coordinate of the right position of the text is not the same as of the y coordinate of an element and the bottom position. In fact it's inverted.
That's because vertically, the text is place at the bottom of an element and y coordinates go from top to bottom, while horizontally coordinates go in the same direction as the text.
This is easily seen when using GetBBbox() method, which you are using. For example, when adding a text element at coordinates 0 for x and y, you'll see that getBBbox will give you a x = 0 but a negative y.
console.log(document.getElementById('testbbox').getBBox());
<svg id="testbbox"><text x=0 y=0>test</text></svg>
So already, calculations to center your text will need to be different. What you have now is:
outerRect.x - ((outerRect.width - innerRect.width) / 2)
This works because you assume that inner rectangle starts from 0, which is not the case. Your bounding box is vertically in the negative. So complete formula should be something like:
outerRect.x - innerRect.x - ((outerRect.width - innerRect.width) / 2)
Which in your example would be:
x - bboxx + ( (width - boxWidth) / 2)
Where bbox is x of getBBox()
You could use baseline, but with dy on the tspan, it makes the calculations a bit more complex, not sure it's the good approach. Since anyway getBBox takes baseline into account in the calculation, it's not really necessary.
Also, if you look here, baseline for text should be dominant-baseline, not alignement-baseline: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/dominant-baseline
See how it affects the bounding box, especially y coordinate:
console.log(document.getElementById('central').getBBox());
console.log(document.getElementById('top').getBBox());
<svg id="central">
<text y=10 dominant-baseline="central" >test</text>
</svg>
<svg id="top">
<text y=10 dominant-baseline="top" >test</text>
</svg>
See end result:
https://codesandbox.io/s/52wl31l8zn
I'm trying to scale and then rotate a triangle and then translate it to a given point in Snap SVG.
I want to rotate the triangle around the top of it not the center, so i can build something like a pie.
So I thought I scale first, then rotate and later translate.
var t = new Snap.Matrix();
t.scale(0.5);
t.rotate(45, bbox.cx, (bbox.cy-(bbox.h/2)));
But the scale and rotation somehow are allways a bit off.
I reused a jsfiddle I found and updated it, so you can see what I try:
http://jsfiddle.net/AGq9X/477/
Somehow the bbox.cx and bbox.cy are not in the center of the triangle.
On my local setup they are.
The strange thing is, just rotation without scaleing works fine,
but scaling and then roation always seems to be a bit off on the y axis, the triangle doesn't stays at the rotation point.
Any ideas how i can fix that?
EDIT:
Ok I found the Solution,thanks to lan, you were right, the center of scaleing is important, and
I thought it was useing the center of the object, but it was the upper left corner. I adjusted it
and now it works greate:
var bbox = obj.getBBox(); //get coords etc. of triangle object
var t = new Snap.Matrix();
var offset = (bbox.cy+(bbox.h)) - centerY; //translate Y to center,
//depends on scaleing factor (0.5 = bbox.h, 0.25 = bbox.h*2)
t.scale(0.5, 0.5, bbox.cx, (bbox.cy+(bbox.h/2))); //scale object
t.translate(0,-offset); //translate to center
t.rotate(45, bbox.cx, (bbox.cy+(bbox.h/2))); //rotate object
obj.transform(t); //apply transformation to object
EDIT2:
I wanted to know how to save transformation, so you don't need to apply them every time you use a new transformation. Ian recommended to use element.transform() like so to get the old transformations:
element.transform( element.transform() + 's2,2' )
This is slightly more complicated than one would expect, but you would be animating a matrix, which does some odd things sometimes.
Personally I would use Snaps alternate animate method Snap.animate() and not using a matrix. Set the scale first and then build your animation string.
Something like..
var triangle2 = p.select("#myShape2").transform('s0.5');
...
Snap.animate(0,90,function( val ) {
triangle2.transform('r'+ val + ',' + bbox.cx+','+(bbox.cy-(bbox.h/2))+'s0.5')
}, 2000)
jsfiddle
I would like to scale animate an SVG element to fit (preserving aspect ratio) a given area of the SVG.
I know about animate which performs relative animations
var s = Snap("#myelement");
s.animate({ 'transform' : 't100,100s5,5,165,175' },1000);
In principle it should be possible to achieve what I want by computing the parameters of the translation and the scaling.
The problem there is that I do not find accurate documentation of the parameters.
The arguments of t seem to be the relative x,y position and that of s the scale factors and the coordinates of the scale center.
However, how does the combined translation and scaling work? Does the relative translation position scale with the scaling, etc.?
In other words: How do I compute the relative translation and scaling parameters from the coordinates of the upper left and the lower right corner of the animation target element?
Alternatively: Is there a more suitable animate function in Snap?
You show a transform with several parts. The order of these parts is important. If you translate first and scale later, the resulting translation is scaled too. If you scale first and then translate the resulting translation is not affected by the scaling.
The animation you use in Snap.svg is the one I also use. (However I consider migrating to svg.js, since Snap.svg does not play well with Electron for example. I have to do some testing first, though)
Since Snap uses SVG syntax, to solve the problem one needs to understand SVG transformations (see here for an introduction: https://sarasoueidan.com/blog/svg-transformations/). In order to set up a combined SVG transformation it is important to understand that each transformation changes the coordinate system (rather than just the properties of an element in an absolute coordinate frame).
If you combine two transformations, scaling and translation, this means that the parameters of the second transformation depends on the first one.
To achieve a translation and scaling of an element to a given location and size in the coordinates of the ViewBox of an SVG, one can first perform the scaling to the new size choosing the center coordinates for the scaling as the center of the element. Then considerations for the following translations simplify as follows
function startAnimation() {
var svg = Snap("#baseSVG");
/* get the bounding box of the svg */
var bboxSvg = svg.getBBox();
var s = Snap("#element");
/* get the bounding box of the element */
var bbox = s.getBBox();
/* get the required scale factor (assuming that we want to fit the element inside the svg bounding box) */
var scale = Math.min(bboxSvg.width/bbox.width,bboxSvg.height/bbox.height)*0.8;
/* compute the translation needed to bring center of element to center of svg
the scale factor must be taken into account since the translation is based on the coordinate system obtained after the previous scaling */
var tx = (200-bbox.cx)/scale;
var ty = (200-bbox.cy)/scale;
/* perform the animation (make center of scaling the center of element) */
s.animate({ 'transform' : 's' + scale + ',' + scale + ',' + bbox.cx + ',' + bbox.cy + 't' + tx + ',' + ty },1000,mina.bounce);
s.drag();
}
This assumes that your SVG object has id baseSVG and the element you want to transform has id element. It is transformed such that it fits the SVG (adjust the factor 0.8 if you want it larger or smaller). If you know only the coordinates of the corners of the element you must first compute the center coordinates of the target (replace bbox.cx and bbox.cy) and the scale to apply this code snippet. This works in the obvious way in the coordinate frame of baseSVG.
Example: http://codepen.io/heroheman/pen/thdBH
Hi,
i have this boxed path, which changes the it corner points every second.. Also i have 4 icons which should be on the corner of the box.
Both is based on an array which recalculates the points, and sets new position.
But the position of the circles seems to have some kind of scaling - I tried absolute and relative paths (t and T seems to make no difference).
Maybe one of you guys could help!
You will need to account for the offset of the centre of the circles...
If you look at 'bubble' for example, its cx,rx attribute is 113,101.6
So ideally the transform for bubbles would logically be (new transform - original position)
't' + ( boxCoords[4] - 113 ) + ',' + (boxCoords[5] - 101.6 )
You could either hard code this in an array or object. Or if there are lots of icons, possibly you could grab the icons respective circle element, and get its element.attr('cx') value (or x if its a rect, or previous transform if its an arbitrary shape, or do a getBBox() on it to get its centre).