BBox calculation of svg <g> elements - javascript

I just came across a weired case of bouncing box calculation and it seems I did not grasp the whole truth yet.
First of all, a bounding box is defined as the tightest box, an untransformed element can be enclosed with.
I always was under the impression, that for groups, that means, that it gets basically the union of the bounding box of all children.
However, today I came across this:
<g id="outer">
<g id="inner" transform="translate(100, 100)">
<rect x="0" y="0" width="100" height="100" />
</g>
</g>
The bounding boxes of the elements are as follows:
rect: x: 0, y: 0, w: 100, h: 100
#inner: x: 0, y: 0, w: 100, h: 100
#outer: x: 100, y: 100, w: 100, h: 100
My expectation would have been, that all boxes are the same but as you can see, the outer box is NOT the union of the inner elements (in that case it would equal the #inner's bbox). Instead it takes into account the transformation of the inner elements.
So, is it right to say, that the bbox of a group is the union of the TRANSFORMED bbox's of its children? Or more programatically said, the union of all getBoundingClientRect calls (assuming that scroll is 0 because getCoundingClientRect ignores scroll)?
I would really appreciate a link pointing me to the correct part of the specs.

The bounding box returned by getBBox is the box in the element's transformed coordinate system
Returns the tight bounding box in current user space (i.e., after application of the ‘transform’ attribute, if any) on the geometry of all contained graphics elements, exclusive of stroking, clipping, masking and filter effects)...
The outer SVG element has a different co-ordinate system. I.e. where it places the origin is not the same as the inner <g> element because of the inner element's transform.
getBoundingClientRect operates in the global co-ordinate system however.

In this demo the red polygon represents the #outer BBox during an animation where the rect is rotating.
const SVG_NS = 'http://www.w3.org/2000/svg';
let o = outer.getBBox()
let i = inner.getBBox()
let BBpoly = drawBBox(o);
function drawBBox(bb){
let p = [{x:bb.x,y:bb.y},
{x:bb.x+bb.width,y:bb.y},
{x:bb.x+bb.width,y:bb.y+bb.height},
{x:bb.x,y:bb.y+bb.height}];
let BBpoly = drawPolygon(p, BBoxes);
return BBpoly;
}
function drawPolygon(p, parent) {
let poly = document.createElementNS(SVG_NS, 'polygon');
let ry = [];
for (var i = 0; i < p.length; i++) {
ry.push(String(p[i].x + ", " + p[i].y));
}
var points = ry.join(" ");
poly.setAttributeNS(null, 'points', points);
parent.appendChild(poly);
return poly;
}
function updatePolygon(p,poly){
let ry = [];
for (var i = 0; i < p.length; i++) {
ry.push(String(p[i].x + ", " + p[i].y));
}
var points = ry.join(" ");
poly.setAttributeNS(null, 'points', points);
}
let a = 0;
function Frame(){
requestAnimationFrame(Frame);
inner.setAttributeNS(null,"transform", `rotate(${a}, 120,120)`)
let bb = outer.getBBox()
let p = [{x:bb.x,y:bb.y},
{x:bb.x+bb.width,y:bb.y},
{x:bb.x+bb.width,y:bb.y+bb.height},
{x:bb.x,y:bb.y+bb.height}];
updatePolygon(p,BBpoly);
a++
}
Frame()
svg{border:1px solid; width:300px;}
polygon{fill:none; stroke:red; }
<svg viewBox="0 0 250 250">
<g id="BBoxes"></g>
<g id="outer">
<g id="inner">
<rect x="70" y="70" width="100" height="100" />
</g>
</g>
</svg>

Related

Fill the intersection area between circle and function in JSXGraph

I have a circle and simple function Math.cos(x)
I want the circle to be filled when it intersects with that function (fill only the upper side). But it's not working.
Script:
// circle
var point1 = app.board.create('point', [0,0], {size: 2, strokeWidth:2 })
var point2 = app.board.create('point', [6,0], {size: 2, strokeWidth:2 })
var circle = app.board.create('circle', [point1,point2], {strokeColor: "#f00", strokeWidth: 2 })
// function
var func = app.board.create('functiongraph',[function(x){ return Math.cos(x)}]);
// intersection
var curve = app.board.create('curve', [[], []], {strokeWidth: 0, fillColor: "#09f", fillOpacity: 0.8})
curve.updateDataArray = function() {
var a = JXG.Math.Clip.intersection(circle, func, this.board);
this.dataX = a[0];
this.dataY = a[1]
};
app.board.update()
Output
Expected output (I did it on Paint)
Thank you in advance :)
This can easily realized with the next version of JSXGraph which will be released next week: With the inequality element the area above the cosine curve can be marked. The inequality element is a closed curve and can be intersected with a circle. In v1.2.3, the intersection does not work because of a small bug.
For the clipping, the next version contains new elements curveintersection, curveunion, curvedifference which make it easier to use the methods of JXG.Math.Clip, but of course your approach with JXG.Math.Clip will still work.
Here is the code:
var f = board.create('functiongraph', ['cos(x)']);
var ineq = board.create('inequality', [f], {
inverse: true, fillOpacity: 0.1
});
var circ = board.create('circle', [[0,0], 4]);
var clip = board.create('curveintersection', [ineq, circ], {
fillColor: 'yellow', fillOpacity: 0.6
});
Actually, the inequality element does the same as enxaneta does "by hand".
In the next example I'm building the d attribute for the path using Math.cos().
I suppose your function may be different.
Please observe that at the end at the d attribute the path is closing the upper part of the svg canvas.
I'm using the pth inside a clipPath and I'm clipping the circle with it.
let d ="M";
for(let x = -50; x<=50; x+=1){
d+=`${x}, ${5*Math.cos(x/5)} `
}
d+="L50,-50L-50,-50z"
pth.setAttribute("d",d);
<svg viewBox="-50 -50 100 100" width="200">
<clipPath id="clip">
<path id="pth"/>
</clipPath>
<circle r="45" clip-path="url(#clip)" fill="blue"/>
</svg>
In order to better understand how I'm building the path please take a look at the next example:
let d ="M";
for(let x = -50; x<=50; x+=1){
d+=`${x}, ${5*Math.cos(x/5)} `
}
d+="L50,-50L-50,-50z"
pth.setAttribute("d",d);
<svg viewBox="-50 -50 100 100" width="200">
<circle r="45" fill="blue"/>
<path id="pth" fill="rgba(250,0,0,.4)"/>
</svg>

Get the bounding box of the intersection of 2 or more paths

In the following example I have in gold the intersection of 3 shapes (in this case I'm using circles but those 3 shapes can be anything) The golden intersection is the result of clipping with clip-path.
I would like to use the intersection as a symbol and for this I would need to know the bounding box of the intersection, i.e the red stroked rectangle.
If I'm using intersection.getBBox() I'm getting the bounding box before clipping.
How can I get the bounding box of the intersection?
console.log(intersection.getBBox())
svg{border:solid}
.circles{fill:none;stroke:black}
<svg id="svg" viewBox="-150 -150 300 300" width="300">
<defs>
<circle id="c1" cx="0" cy="-50" r="80"></circle>
<circle id="c2" cx="43.3" cy="25" r="80"></circle>
<circle id="c3" cx="-43.3" cy="25" r="80"></circle>
<clipPath id="clipC2"><use xlink:href="#c2"/></clipPath>
<clipPath id="clipC3"><use xlink:href="#c3"/></clipPath>
</defs>
<g class="circles">
<use xlink:href="#c1"/>
<use xlink:href="#c2"/>
<use xlink:href="#c3"/>
</g>
<g id="intersection">
<g clip-path="url(#clipC3)">
<use fill="gold" xlink:href="#c1" clip-path="url(#clipC2)"/>
</g>
</g>
<rect x="-38" y="-42" width="75" height="74" stroke="red" fill="none"/>
</svg>
The main idea is this:
I'm taking the svg element, make it base64 and use it as the src attribute of an image.
I'm painting the svg element on a canvas with the same size as the svg element.
I get the image data from the canvas
loop through the image data and get:
the smallest x value of a black pixel
the smallest y value of a black pixel
the biggest x value of a black pixel
the biggest y value of a black pixel
I'm using using those values to build the new viewBox value for the intersection.
//the svg's viewBox
let vB = { x: -100, y: -100, w: 200, h: 200 };
//canvas
let ctx = c.getContext("2d");
//set the size of the canvas equal to the size of the svg element
c.width = vB.w;
c.height = vB.h;
// draw the svg element on the canvas
let xml = new XMLSerializer().serializeToString(svg);
// make it base64 and use it as the src attribute of the image
let img=new Image()
img.src = "data:image/svg+xml;base64," + btoa(xml);
img.onload = function() {
//paint the image on the canvas
ctx.drawImage(this, 0, 0);
//get the image data from the canvas
let imgData = ctx.getImageData(0, 0, vB.w, vB.h).data;
// x the smallest x value of a black pixel
// y the smallest y value of a black pixel
// X the biggest x value of a black pixel
// Y the biggest y value of a black pixel
let x = vB.w,
y = vB.h,
X = 0,
Y = 0;
let n = 0;
for (let i = 0; i < imgData.length; i += 4) {
n++
if (imgData[i + 3] != 0) {
//if the alpha (i+3) value of the pixel is not 0
let _y = Math.ceil(i / (4 * vB.w));
let _x = (i / 4) % vB.w;
if (_x < x) { x = _x; }
if (_y < y) { y = _y; }
if (_x > X) { X = _x; }
if (_y > Y) { Y = _y; }
}
if(n==imgData.length/4){
let newViewBox = `${x + vB.x} ${y + vB.y} ${X - x + 1} ${Y - y}`;
reuleaux.setAttribute("viewBox", newViewBox);
console.log(`viewBox="${newViewBox}"`);
}
}
}
svg,
canvas {
outline: 1px solid;
}
<svg id="svg" viewBox="-100 -100 200 200" width="200">
<defs>
<circle id="c1" cx="0" cy="-50" r="80"></circle>
<circle id="c2" cx="43.3" cy="25" r="80"></circle>
<circle id="c3" cx="-43.3" cy="25" r="80"></circle>
<clipPath id="clipC2"><use xlink:href="#c2"/></clipPath>
<clipPath id="clipC3"><use xlink:href="#c3"/></clipPath>
</defs>
<g id="intersection">
<g clip-path="url(#clipC3)">
<use xlink:href="#c1" clip-path="url(#clipC2)"/>
</g>
</g>
</svg>
<!--<img id="img" width="200" height="200"/>-->
<canvas id="c"></canvas>
<svg id="reuleaux" viewBox="-100 -100 200 200" width="200" style="background:#dfdfdf">
<use xlink:href="#intersection"/>
</svg>
Unsure if this was the type of thing you were after.
let myBB = {
x: c2.getBBox().x,
get y() {
return c1.getBBox().y + this.width
},
get width() {
// return (posNum(c3.getBBox().x)) - (posNum(myBB.x));
let leftPointOfWidth = c2.getBBox().x;
let rightPointofWidth = c3.getBBox().x + c3.getBBox().width;
// 10000 to guarantee both positive numbers. very hacky
let mywidth = (rightPointofWidth + 10000) - (leftPointOfWidth + 10000);
return mywidth;
},
get height() {
return this.width;
}
}
I'm quite sure there's a better way to put it. And need to call the getters; They won't appear in the console.log(myBB)
Inputting the obtained coordinates and width gives the rect in yellow. (Pink rects are shoing centres of circles)

How to apply rotation to graphic elements and get new path data?

How do you apply path rotation to graphic elements such as a rectangle or a path?
For example, applying rotation to a path:
<svg width="200px" height="200px" viewbox="0 0 200 200">
<rect x="100" y="0" width="20" height="20" fill="red" transform="rotate(45)"/>
</svg>
IMPORTANT NOTE:
I'm not running an browser. I remember seeing solutions that use browser or canvas to do the calculations.
I only have the markup. For a path I have the path data, for a rectangle the position and width and height, and the line the x1 y1 and x2 y2 data.
UPDATE:
It's important to know the transform origin. That would be rotated from the element center.
I would use an array of points to draw the path. For the rotation I would rotate the points and draw the rotated shape.Please read the comments in my code. I hope this is what you were asking.
const SVG_NS = svg.namespaceURI;
// the rotation
let angle = Math.PI/4;
// the points used to rotate the initial rect
let theRect = [
{ x: 100, y: 0 },
{ x: 100 + 20, y: 0 },
{ x: 100 + 20, y: 0 + 20 },
{ x: 100, y: 0 + 20 }
];
// calculating the rotated points
let rotatedRect = [];
theRect.forEach(p => {
rotatedRect.push(rotatePoint(p, angle));
});
drawRect(theRect);
drawRect(rotatedRect);
// a function to draw the rect. For this I'm using the points array
function drawRect(ry) {
let d = `M${ry[0].x},${ry[0].y}L${ry[1].x},${ry[1].y} ${ry[2].x},${ry[2].y} ${
ry[3].x
},${ry[3].y}`;
drawSVGelmt({ d: d }, "path", svg);
}
// a function used to rotate a point around the origin {0,0}
function rotatePoint(p, rot) {
let cos = Math.cos(rot);
let sin = Math.sin(rot);
return {
x: p.x * cos - p.y * sin,
y: p.x * sin + p.y * cos
};
}
// a function to draw an svg element
function drawSVGelmt(o, tag, parent) {
let elmt = document.createElementNS(SVG_NS, tag);
for (let name in o) {
if (o.hasOwnProperty(name)) {
elmt.setAttributeNS(null, name, o[name]);
}
}
parent.appendChild(elmt);
return elmt;
}
svg{border:1px solid; max-width:90vh;}
<svg id="svg" viewbox="0 0 200 200">
<rect x="100" y="0" width="20" height="20" fill="red" transform="rotate(45)"/>
</svg>

svg : find x,y coordinates of rect vertices

I have various svg rects on a web page on which a transform is applied in the form :
transform="translate(418 258) rotate(-45.2033 15 18) scale(2.5 2.5)"
I need to get the x,y coordinates of the 4 vertices of each rect after the transform is applied.
Here is an exemple of code :
<g transform="translate(418 258) rotate(-45.25 15 18) scale(2.5 2.5)">
<rect id="box" x="0" y="0" width="31" height="37" style="fill:none;stroke:rgb(102, 102, 102);stroke-width:1.5px;">
</rect>
</g>
I have tried the following formula in plain js :
x' = x * cos(angle) + y * sin(angle)
y' = -x * sin(angle) + y * cos(angle)
but the results are slightly different from the svg display in various browsers.
I guess this can be done using js/svg primitives, but I don't know how, and didn't find any code example. Perhaps changing the rects into paths after transform would do the trick...
Last but not least, I'm using jquery but not d3.
Thanks in advance.
You can read the transform attribute and convert it to a matrix.
Then for each of the rectangle's four corners, you can use that matrix to calculate the transformed corner locations.
See the following demo.
This demo assumes that there is an element with id of "box", and that the transform you care about is just the one on the parent <g> element. If your circumstances are more complex than that, then you will need to do some more work on this code.
// Get a reference to the "box" rect element
var box = document.getElementById("box");
// Get its x, y, width and height
var bx = box.x.baseVal.value;
var by = box.y.baseVal.value;
var bw = box.width.baseVal.value;
var bh = box.height.baseVal.value;
// Get the box's parent element (the <g>)
var parentGroup = box.parentNode;
// Read the transform attribute and convert it to a transform matrix object
var transformMatrix = parentGroup.transform.baseVal.consolidate().matrix;
// For each of the rectangle's four corners, use the transform matrix to calculate its new coordinates
console.log("point 1 = ", doPoint(bx, by));
console.log("point 2 = ", doPoint(bx + bw, by));
console.log("point 3 = ", doPoint(bx + bw, by + bh));
console.log("point 4 = ", doPoint(bx, by + bh));
function doPoint(x, y)
{
// We need a reference to the <svg> element for the next step
var svg = box.ownerSVGElement;
// Create an SVGPoint object with the correct x and y values
var pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
// Use the "matrixTransform()" method on SVGPoint to apply the transform matrix to the coordinate values
pt = pt.matrixTransform(transformMatrix);
// Return the updated x and y values
return {x: pt.x, y: pt.y};
}
<svg>
<g transform="translate(418 258) rotate(-45.25 15 18) scale(2.5 2.5)">
<rect id="box" x="0" y="0" width="31" height="37" style="fill:none;stroke:rgb(102, 102, 102);stroke-width:1.5px;">
</rect>
</g>
</svg>

Raphael Js : How to re-arrange the elements inside a raphael set in a curve path

I was working in Raphael Js to create a layout in svg. I have created some circles that are arranged in rows and column. Say there are 15 circles in columns and 5 in rows like in the image attached.
Here all the elements are first drawn in a straight line. This is good and as wished. But in some sets drawn, I would want to skew the whole set and also arrange them in a curve (horizontal/vertical). I was trying to use a range slider to determine the curve of the elements.
How can I achieve it?
draw arch (or any complex path)
get path's (x,y) value
plot circles using path. QED.
NOTE: In Raphael, use paper.circle(..) instead of svgdoc.appendChild() with getCircle().
function getCircle(x,y, width, height) {
var xmlns = "http://www.w3.org/2000/svg";
var xlink = "http://www.w3.org/1999/xlink";
var Elem; // ouput SVG element
Elem = document.createElementNS(xmlns,"use");
Elem.setAttributeNS(null, 'x', x);
Elem.setAttributeNS(null, 'y', y);
Elem.setAttributeNS(null, 'width', width);
Elem.setAttributeNS(null, 'height', height);
Elem.setAttributeNS(xlink, 'xlink:href', '#sym01');
return Elem;
}
var svgdoc = document.getElementById('mySVG');
var curve1 = document.getElementById('curve1');
var len = curve1.getTotalLength();
for (var y_pos = 0; y_pos < 10; ++y_pos) {
for (var i = 0; i <= 20; ++i) {
var pt = curve1.getPointAtLength( (i*5/100)*len);
svgdoc.appendChild(getCircle(pt.x-5,pt.y+y_pos*20, 10,10));
}
}
<svg id='mySVG' width="400" height="400">
<!-- symbol definition NEVER draw -->
<symbol id="sym01" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="17" stroke-width="2" stroke="red" fill="pink"/>
</symbol>
<path id="curve1" d="M10 100 Q 200 -10 390 100" stroke="red" fill="transparent"/>
<!-- actual drawing with "use" element -->
</svg>

Categories

Resources