SVG group to child transforms - svg

I have a group that has 2 or more SVG elements.
Group has a SVG matrix applied and the same apply for children.
Each element, including group can have any transformations applied through the matrix (rotate, skew, scale etc). Example of SVG with the group and 2 inner elements:
<g transform="matrix(0.679039, -0.734102, 0.734102, 0.679039, -21.7964, 262.861)">
<path transform="matrix(1, 0, 1.63636, 1, 82.9909, 49.1)" d="M92.35,45.6 A92.35,22.8 0 1 1 92.35016 45.6"></path>
<rect width="225.1" height="63.3" transform="matrix(0.742794, -0.66952, 0.66952, 0.742794, 242.258, 216.595)" rx="0"></rect>
</g>
I would like to do an ungroup function that removes the <g> element and place inner elements outside of g, but keeps all matrix transformations from group and their own transformations.
Tried multiple methods but they all have different issues where inner elements are not placed in the correct position or transformations are not copied.
The following seems to do something, but works for rotation only, if skew is applied - elements are totally different.
matrixMultiply(...matrices: DOMMatrix[]) : DOMMatrix {
let args = arguments, i = args.length, m = args[i-1];
while(i-- > 1) {
let m1 = args[i-1] as Matrix;
m = m1.multiply(m);
}
return m;
}
let groupMatrix = groupElement.transform.baseVal.getItem(0).matrix;
groupElement.childNodes.forEach(child => {
let childTransform = (child as SVGGraphicsElement).transform.baseVal.getItem(0),
childMatrix = childTransform.matrix,
childInverseMatrix = childMatrix.inverse();
let gm = matrixMultiply( childInverseMatrix, groupMatrix, childMatrix );
childTransform.setMatrix(gm);
});

You are making your life much too complicated. All you need to know is that a sequence of transform attributes, read in the direction of parent to child, has the same result as a sequence of transforms written left to right in one transform attribute:
<g transform="skewX(...)">
<path transform="matrix(...)" d="..." />
</g>
<!-- does the same transformation as -->
<path transform="skewX(...) matrix(...)" d="..." />
That simplifies your code to the following. If you want, you can compute the new transform value yourself by initializing a DOMMatrix from the transform strings. But why would you? The browser will do it for you anyway.
let groupTransform = groupElement.getAttribute('transform');
groupElement.childNodes.forEach(child => {
let childTransform = child.getAttribute('transform');
// not really needed...
let gm = new DOMMatrix(`${groupTransform} ${childTransform}`);
child.setAttribute('transform', gm.toString());
//...this would suffice
child.setAttribute('transform', `${groupTransform} ${childTransform}`);
});

Related

How to draw a responsive SVG path with points on it?

I am trying to draw this SVG path.
I could achieve it using SVG Line and Curve properties for a fixed height and width using fixed coordinates.
But, I need to draw this responsively which means, the width of the path, space between the lines, the distance between each point, and the curves at the sides should be responsive.
It contains levels indicated by numbers as shown in the image, and the length of the path should be determined by the number of levels given.
While trying to draw this responsively, I was stuck at
Getting the start and end-point of the Lines
Getting control points for the curves
Responsive adjustment of the distance between each point and space between the curves
Determining the length of the path based on the number of levels given
I am trying to do these using percentages based on the parent div's width by some mathematical calculations. But, it seems it breaks some or other cases due to its complexity.
There are some other things to do along with these, but this is the top-level version of what needs to be done.
Is there any direct method or formula or calculation for achieving this?
(or)
Is there any other approach to draw this?
(or)
Are there any tutorials for learning to draw these types of SVG paths?
After creating the path you need to calculate the position of the numbers and circles on the path. For this you need to know the length of the path calculated with the getTotalLength() method. Next you need a loop to calculate the position of the numbers on the path. For this I've used the getPointAtLength()
method. On each of this pointd you create a new circle (use) element and a new text element.
Please read the comments in the code.
//constants used to create a new element in svg
const SVG_NS = "http://www.w3.org/2000/svg";
const SVG_XLINK = "http://www.w3.org/1999/xlink";
//the total length of the path
let length= thePath.getTotalLength();
//the number of points on the path
let n = 15;
let step = length/n;
for(let i= 1; i<=n; i++){
//creates a new point on the path at a given length
let point = thePath.getPointAtLength(i*step);
//create a new use element (or a circle) and set the center of the circle on the calculated point
let use = document.createElementNS(SVG_NS, 'use');
use.setAttributeNS(SVG_XLINK, 'xlink:href', '#gc');
use.setAttribute("x", point.x);
use.setAttribute("y", point.y);
//create a new text element on the same point. Thr text content is the i number
let text = document.createElementNS(SVG_NS, 'text');
text.setAttribute("x", point.x);
text.setAttribute("y", point.y);
text.textContent = i;
//append the 2 new created elements to the svg
svg.appendChild(use);
svg.appendChild(text);
}
svg {
border: solid;
}
text {
fill: black;
font-size: 4px;
text-anchor: middle;
dominant-baseline: middle;
}
<svg viewBox="0 0 100 80" id="svg">
<defs>
<g id="gc">
<circle fill="silver" r="3" />
</g>
</defs>
<path id="thePath" fill="none" stroke="black" d="M10,10H70A10,10 0 0 1 70,30H20A10,10 0 0 0 20,50H70A10,10 0 0 1 70,70H20" />
</svg>
Please observe that since the svg element has a viewBox attribute and no with it takes all the available width making it responsive.

Is there a well-defined way to compute the x-coordinate of the right edge of an svg text element?

Suppose I have the following text element, and it is left-aligned as below.
<text xml:space="preserve" text-anchor="start" y="134.799806172" x="450" >
Foo Bar</text>
Observe that text-anchor is start, so I am providing the x and y coordinates of the left edge.
Is there a well-defined way to compute the right edge of this text element?
To give some context, this element must be left-aligned because it has to line up with other elements on the left.
However, I want to have a different element that is center-aligned with this element, so I want to compute the right edge and thereby find the center.
You can use getBBox(), example:
var text = document.querySelector("text");
var bbox = text.getBBox();
bbox will be an object with width, height, x and y properties.

SVG Zoom and Pan

I have an SVG layout (from D3) of a tree. I have an SVG element in my HTML that takes up 100% of the width and height of the page. Then, within that SVG element, D3 renders a group element with a bunch of circles and lines in it. For example:
<svg style='width:100%;height:100%;'>
<g>
...stuff...
</g>
</svg>
I want to be able to zoom and pan so that a certain portion of the tree (group element) takes up the screen. I have the exact coordinates of the area I want to zoom in on, so ideally, I want to move the SVG element X pixels up and to the left, then scale the whole element by Y. How can I best do that?
From what I'm reading, the viewBox attribute is best for this, but I just can't figure out how I would be able to zoom in on just one portion. This example seems to get at what I want, but my SVG element is measured in percentages, not pixels. And even though the coordinate system is supposed to be arbitrary, I'm having a hard time converting between the two.
Here, I use this to zoom into certain regions of the SVG image. Change cx to the x coordinate, cy to the y coordinate, width and height are your call. The line you should be interested in is the svgDocument.setAttribute(...)
function zoomTarget1() {
var svgDocument = document.getElementsByTagName('svg')[0];
var cx = 20;
var cy = 20;
var width = 610;
svgDocument.setAttribute("viewBox", cx+" "+cy+" "+width+" 590");
var reShow = svgDocument.getElementById("FloorSelection");
showReturnButton(cx,cy, width, reShow);
}

Best way to move a group of SVG elements

I'm looking for the best way (the faster, the cleaner...) to move a group of SVG elements. I have three ways in mind :
do a loop on all elements and, for each of us, change the x and y attributes
group all elements in a svg element and change its x and y attributes
group all elemnts in a g element and apply the method described here : https://stackoverflow.com/a/14036803/2019761
Do you have an idea please ?
You can move svg groups or elements with javascript
// translate svg element
function translate( _element , _x , _y )
{
var transform = _element.transform.baseVal.getItem(0);
var mat = transform.matrix;
mat = mat.translate( _x, _y );
transform.setMatrix( mat );
}
see it in action:
http://www.janvas.com/illustrators_designers_developers/projects/janvas2D_web/examples_EN.php?exampleName=ufo_animation_EN
I think that the better way is to move a group of elements.
If you look the example you can see that the ufo are translated
and the inner motor rotate inside of it.
(all moved elements are groups)
<g xmlns="http://www.w3.org/2000/svg" transform="matrix(1 0 0 1 -12.5067 69.4101)" id="ufo">
<g transform="matrix(1 0 0 1 0 -2.842170943040401e-14)">
<path transform="matrix(1 0 0 1 21.6 2.8)" width="92.34371368613222" height="91.4899957511011" stroke-width="0.83" stroke-miterlimit="3" stroke="none" fill="url(#_1_)" d="M46.1,0 C71.67,0 92… "/>
</g>
<g transform="matrix(0.5 0.86 -0.86 0.5 74.6 24.1)" id="motor">
<path transform="matrix(1 0 0 1 9.7 -2.2)" width="13.11" height="13.5849" stroke-width="0.88" stroke-miterlimit="3" stroke="none" fill="url(#_4_)" d="M6.55,2.8… "/>
</g>
</g>
Interacting with DOM methods involves JS <-> native code overhead. Browser implementers have been working hard to reduce this overhead, but it's never going to be free. If you're doing a lot of it, such as setting x and y on a lot of elements, you may start to see a significant performance impact. In this case setting positional properties just once on an <svg> or <g> container will likely help.
A more significant source of overhead is likely to be the work to repaint for the changes you make. If these changes are for a transform change, and if the transform's value changes multiple times in a short space of time, then most implementations will paint the content of the transformed SVG element into a cached offscreen surface and composite that surface instead of repainting each time. Recompositing can be a lot faster than repainting if the contents of the element are expensive to paint (say it contains a lot of children, or expensive filter effects), so if you're animating the transform of a <g> then you could well see much better performance.

Cloning an svg group

I have an svg group which contains some elements, I want to clone the group, problem is the function clones only one element of the group.
Here is the function
<script type="text/ecmascript"><![CDATA[
function clone(evt) {
var cloneElement = evt.target.cloneNode(false);
var newx = 100;
var newy = 500;
cloneElement.setAttributeNS(null,"x",newx);
cloneElement.setAttributeNS(null,"y",newy);
document.getElementById("layer1").appendChild(cloneElement);
}
]]></script>
The svg looks something like
<g id="layer1" onclick="clone(evt)">
<rect>
<path>
<circle>
<circle>
</g>
The rectangle is like a container and what happens is that the function clones the rectangle and leaves the other elements.
So what is wrong?
Two things:
cloneNode should be passed true if you want a deeply cloned tree, otherwise it will just clone one element
evt.target will always be the element where the event originated, and g elements are never hit directly, the mouse events just bubble up to there from the children. You can use evt.currentTarget instead if you want the element which is currently handling the event (in your case that would be the g element).

Resources