Split groups in SVG into sub-images positioned around the contents - javascript

I'm having issues with individual elements with an imported SVG and was hoping somehow could advise.
I have an SVG created in Illustrator, with several layers, which end up being group elements.
When I retrieve the SVG from my server, I get something similar to.
<svg>
<g>
<g>
</svg>
I do not want to place the image as one, so I break it up by the groups, and surround each by their own svg tag and then place on the page.
<svg>
<g>
</svg>
<svg>
<g>
</svg>
This is great, works like I want.
My issue lies, where the paths of each of these items is drawn in the original file. They are all drawn off (0,0) from the Illustrator file, so when I try to place them, they all have a ton of white space area on the left, where the other elements once existed.
I've tried using transform="translate(-50,-50)" or whatever, which does shift the elements, but since they do not have x,y properties, I do not know where to shift them.
Does anyone know of a way to offset a path drawn? Or if there is a way to read the SVG and break it up into each individual element and work with?
When using firebug or chrome, they show me the individual element with correct sizes, but they are placed with lots of white space because of position of the paths drawn in Illustrator.
I've tried contentDocument, documentElement and both of these come up as null. Maybe I'm using them wrong?
I'm a heavy Actionscript developer, and now working with Javascript and jQuery so I'm use to x,y coord system and placing elements, but this is not seeming the way it should work :/

If you really want to get the XY parts of a path I have written a function to convert paths to use all-absolute commands. With this you could then run through the path commands (using the pathSegList DOM interface, not the raw string attribute) and pull out all the X/Y values and do with them what you will.
However, far easier is to simply calculate the bounding box of the path and set the viewBox on your <svg> element to fit around that directly:
// In case you want to leave the old SVG document unchanged
var newGroup = oldGroup.cloneNode(true);
var newSVG = document.createElementNS('http://www.w3.org/2000/svg','svg');
newSVG.appendChild(newGroup);
var bbox = newGroup.getBBox();
newSVG.setAttribute(
'viewBox',
[bbox.x,bbox.y,bbox.width,bbox.height].join(' ')
);
The above answer does not properly account for the case where a transform has already been applied to the group you want to move. (The bounding box is returned in the untransformed space of the element.) I've created a demo that accounts for this, calculating the correct bounding box for transformed elements.
Demo: http://phrogz.net/SVG/explode_svg_components.xhtml
// Find all the root groups in the original SVG file
var rootGroups = document.querySelectorAll('svg > g');
for (var i=rootGroups.length;i--;){
var newSVG = elementToSVG(rootGroups[i]);
document.body.appendChild(newSVG);
}
// Create a new SVG wrapping around a copy of the element
// with the viewBox set to encompass the element exactly
function elementToSVG(el){
var old = el.ownerSVGElement,
svg = document.createElementNS(old.namespaceURI,'svg'),
css = old.querySelectorAll('style,defs');
// Preserve elements likely needed for correct appearance
[].forEach.call(css,copyToNewSVG);
copyToNewSVG(el);
var bb = globalBoundingBox(el);
svg.setAttribute('viewBox',[bb.x,bb.y,bb.width,bb.height].join(' '));
return svg;
function copyToNewSVG(e){
svg.appendChild(e.cloneNode(true));
}
}
// Calculate the bounding box of an element in global SVG space
// accounting for transforms applied to the element
function globalBoundingBox(el){
var bb = el.getBBox(),
svg = el.ownerSVGElement,
m = el.getTransformToElement(svg);
var pts = [
svg.createSVGPoint(), svg.createSVGPoint(),
svg.createSVGPoint(), svg.createSVGPoint()
];
pts[0].x=bb.x; pts[0].y=bb.y;
pts[1].x=bb.x+bb.width; pts[1].y=bb.y;
pts[2].x=bb.x+bb.width; pts[2].y=bb.y+bb.height;
pts[3].x=bb.x; pts[3].y=bb.y+bb.height;
var xMin=Infinity,xMax=-Infinity,yMin=Infinity,yMax=-Infinity;
pts.forEach(function(pt){
pt = pt.matrixTransform(m);
xMin = Math.min(xMin,pt.x);
xMax = Math.max(xMax,pt.x);
yMin = Math.min(yMin,pt.y);
yMax = Math.max(yMax,pt.y);
});
bb = {}; //IE9 disallows mutation of the original bbox
bb.x = xMin; bb.width = xMax-xMin;
bb.y = yMin; bb.height = yMax-yMin;
return bb;
}

Related

What does it determine the placedItem.width and placedItem.height , when adding one without specifying those properties?

I'm in Adobe Illustrator 2022 26.064 and I'm placing an item. It is the thirdone I place in the same document.
var placedItem3 = doc.placedItems.add();
placedItem3.file = new File("/c/files/dotfiles-win/illustrator-scripts/lorem-picsum/3to2-1080x720.jpg");
placedItem3.name = "placedItem3";
For some reason it gets placedItem.width=3061,41 ,placedItem.height=2040,94
When the file is 1080x720
The previous items were placed keeping the same dimensions of the file.
You can find out ppi of a image with the formula:
72 / image.matrix.mValueA
For example here is the script that finds images with ppi less than 250 and selects them:
var min = 250; // target ppi
var rasters = app.activeDocument.rasterItems;
var i = rasters.length;
while (i--) if (72/rasters[i].matrix.mValueA < min) rasters[i].selected = true;
Illustrator does it in a rather uncommon way via the transformation matrix
Matrix
I'm not sure if there are another ways as well. And probably it might require additional steps for rotated images.

CreateJS : Get the ACTUAL bound area after the object has been rotated

I'm making an animation editor entirely with HTML and Javascript. For the Canvas manipulation operations I'm using CreateJS.
At this point, I want to render a small UI inside the canvas, that shows off what item is selected.
The problem starts when I rotate the Image, so far I know that I can get the Boundaries of a Bitmap after its transformation, but this operation gives me the data that I'm not Looking for.
The Code below sets the coordinates for the UI drawn in the canvas:
update : function(){
//Big rectangle
this.ui[0].x = this.object.x;
this.ui[0].y = this.object.y;
this.ui[0].rotation = this.object.rotation;
this.ui[0].scaleX = this.object.scaleX;
this.ui[0].scaleY = this.object.scaleY;
//North West rectangle coordinates
this.ui[1].x = this.object.getTransformedBounds().x;
this.ui[1].y = this.object.getTransformedBounds().y;
//North East rectangle coordinates
this.ui[2].x = this.object.getTransformedBounds().x + this.object.getTransformedBounds().width;
this.ui[2].y = this.object.getTransformedBounds().y;
//South West rectangle coordinates
this.ui[3].x = this.object.getTransformedBounds().x;
this.ui[3].y = this.object.getTransformedBounds().y + this.object.getTransformedBounds().height;
//South East rectangle coordinates
this.ui[4].x = this.object.getTransformedBounds().x + this.object.getTransformedBounds().width;
this.ui[4].y = this.object.getTransformedBounds().y + this.object.getTransformedBounds().height;
}
The method getTransformedBounds() returns the whole rectangular area that the image occupies after it's transformation. Is there a way to get the Actual rectangular area that the object occupies in the canvas so I can achieve something like this:
http://postimg.org/image/5wr32wt7j/
and not this:
http://postimg.org/image/dqroob10f/
I'm kinda new with createJS so please bear with me.
You can use getBounds() instead of getTransformedBounds(). It returns the untransformed local bounds. Then just draw a rect in a Shape using those bounds, and transform the Shape to match the transformation of the "object".
Alternately, put both the "ui" and the "object" in a Container together, and apply the transformations to the Container instead of to the "object".

What's the idiomatic way to sync transitions so that adjacent shapes move together?

Say I have an arbitrary path like this:
[##########]
I also have a circle like this: o
I want to keep o at the tip of the arbitrary path, so it looks like this:
[##########]o
(Assume o is centered vertically between the top and bottom of the path object) And when the path grows or shrinks, the o should always stay at the tip.
[###############]o
Most importantly, when a transform is applied to the path, the transform should also be applied accordingly to the circle -- they should be in sync when in motion.
I've tried making the circle a path marker, but run into trouble
(a) getting it to only move through the center of the path
(b) getting it to "stick" in the final position
(all examples have it infinitely rotating around the path, like this and this and this)
Calling two transition functions (one for each set of shapes) one after another is usually sufficient, because the time it takes the browser to run through the code is much less than the delay between animation frames.
However, if your animation is sufficiently complex that there is a noticeable lag between the two, or if you are doing a lot of complex calculations that will affect both elements, you could use a custom tween function on one selection, and within it select the other shape and update it (you'll want to select it in your "outer" function, so that your inner function which gets called at every update can just quickly reposition it to match the new value).
Regarding transformations, the easiest way to keep things coordinated is to put both elements in a <g> and transform the group instead of the individual elements.
Putting the ideas together, you could get a transition process something like this:
d3.selectAll("g.groups").transition().delay(time)
.attr("transform", function(d,i){ /* Calculate new transform */ })
.tween("stretch", function(d,i){
/* Select the sub-elements, do all the calculations
then create interpolators for both objects */
var g = d3.select(this);
var path = g.select("path");
var dot = g.select("circle");
var newEndPoint = /*** Calculate final position ***/;
var offset = /*** distance from end point to center of circle ***/;
var pathInterpolator = d3.interpolateString(
path.attr(d),
/*** new path including new end point ***/
);
var dotInterpolator = d3.interpolateObject(
{cx=dot.attr("cx"), cy=dot.attr("cy")},
{cx=newEndPoint.x + offset, cy=newEndPoint.y}
);
return function(t){
/* the function that updates both objects at each tick */
path.attr("d", pathInterpolator(t) );
dot.attr( dotInterpolator(t) );
};
});
How complex your real calculations are will depend on how arbitrary your "arbitrary path" is, of course. Maybe you'll need to calculate both x and y offsets to keep the circle positioned correctly. But that becomes an issue of geometry, not of synchronization. Regardless of what else your path shape does as it transitions, if the end point is an actual point in the path data, it will transition in a direct line, the same as the transition of the circle's coordinates.

How do I get resulted pathString after transformation is applied?

Consider I have the following HTML5 path:
var myPath = paper.path([
'M', 0, 0
'L', 100, 100,
'L', 150, 50,
'Z']
]);
myPath.transform(['s', 0.5, 0.5, 0, 0]);
After tranformation (scaling) my path resizes accordingly in half, but inspecting the element is the same path string but with transformation matrix applied. Is there any way to retrieve the pathString resulted (lets say M,0,0,L,50,50,L,75,24,z).
I think you need transformPath method: http://raphaeljs.com/reference.html#Raphael.transformPath
The only solution would be using Raphael 1.x which used to modify paths instead of applying transformations. Otherwise you'd need to write your own routines to convert apply matrix transformations to paths (really difficult).
Please check my answer HERE and the testbed HERE.
There is a function flatten_transformations() which can bake (or apply) transforms to paths, so that transform attribute can be removed. It can handle all path segments (also arcs).
OLD ANSWER (not so complete implementation):
Of course there is a way (example in JSBIN) to get a resulted path data after transformations applied. And even very easy way.
Let's suppose we have a SVG path pathDOM and it's root element svgDOM. We can get a transformation matrix of path's coordinate space to root element's coordinate space using native getTransformToElement() -function. It is used this way:
var matrix = pathDOM.getTransformToElement(svgDOM);
When we apply this matrix to all points in path, we get a new path data, where all coordinates are relative to root element. It can be done this way:
var pt = svgDOM.createSVGPoint();
pt.x = some_x_coordinate_of_path_data;
pt.y = some_y_coordinate_of_path_data;
var new_point = pt.matrixTransform(matrix); // <- matrix object, which we created earlier
var new_x = new_point.x;
var new_y = new_point.y;
And that's it! After conversion the transform attribute can be emptied.
Of course all coordinates in path have to be converted to absolute and eg. Arc segments have to be converted to Line or Cubic Segments, which both can be achieved with Raphaƫl's path2curve() function.
I made a full functional example of using this "Flattening transformations" functionality in JSBIN. There is a ready made function flatten_transformations(), which is the only one needed (the rest is needed for UI purposes). The example has got a path that is nested inside two g elements. Path has own transformations applied, as well as both g elements. Purpose is to test that also nested transformations are flattened.
The code works in newest main browsers and even in IE9. My code that modifies transformations is rather interesting mix of jQuery, Raphael and native code that it may be cause of some problems in IE9 when clicking buttons, but fortunately those essential native functions getTransformToElement(), createSVGPoint() and matrixTransform() work as expected also in IE. I wanted to test simultaneously how those different coding bases plays together. Because it's the fact that Raphael itself is not perfect enough to handle all possible coding needs (lack of styles and groups and lack of possibility to append svg elements as textual xml-data are just ones to note).

Calculate SVG Path Centroid with D3.js

I'm using the SVG located at http://upload.wikimedia.org/wikipedia/commons/3/32/Blank_US_Map.svg in a project and interacting with it with d3.js. I'd like to create a click to zoom effect like http://bl.ocks.org/2206590, however that example relies on path data stored in a JSON object to calculate the centroid. Is there any way to load path data in d3 from an existing SVG to get the centroid?
My (hackish) attempt so far:
function get_centroid(sel){
var coords = d3.select(sel).attr('d');
coords = coords.replace(/ *[LC] */g,'],[').replace(/ *M */g,'[[[').replace(/ *z */g,']]]').replace(/ /g,'],[');
return d3.geo.path().centroid({
"type":"Feature",
"geometry":{"type":"Polygon","coordinates":JSON.parse(coords)}
});
}
This seems to work on some states, such as Missouri, but others like Washington fail because my SVG data parsing is so rudimentary. Does d3 support something like this natively?
The D3 functions all seem to assume you're starting with GeoJSON. However, I don't actually think you need the centroid for this - what you really need is the bounding box, and fortunately this is available directly from the SVG DOM interface:
function getBoundingBoxCenter (selection) {
// get the DOM element from a D3 selection
// you could also use "this" inside .each()
var element = selection.node();
// use the native SVG interface to get the bounding box
var bbox = element.getBBox();
// return the center of the bounding box
return [bbox.x + bbox.width/2, bbox.y + bbox.height/2];
}
This is actually slightly better than the true centroid for the purpose of zooming, as it avoids some projection issues you might otherwise run into.
The accepted answer was working great for me until I tested in Edge. I can't comment since I don't have enough karma or whatever but was using this solution and found an issue with Microsoft Edge, which does not use x or y, just top/left/bottom/right, etc.
So the above code should be:
function getBoundingBoxCenter (selection) {
// get the DOM element from a D3 selection
// you could also use "this" inside .each()
var element = selection.node();
// use the native SVG interface to get the bounding box
var bbox = element.getBBox();
// return the center of the bounding box
return [bbox.left + bbox.width/2, bbox.top + bbox.height/2];
}
From here
The solution is to use the .datum() method on the selection.
var element = d3.select("#element");
var centroid = path.centroid(element.datum());

Categories