I'm performing maintenance on some javascript which makes use of the .offsetParent property. Recent changes now have the application using SVG elements, and they are breaking the JavaScript, as mySvgElement.offsetParent is always undefined.
Is .offsetParent standard, and does it not work with SVG elements? If so, what is an alternative to .offsetParent when working with HTML5 SVG elements?
offsetParent does not exist in SVG.
To get the bounding box coordinates of an SVG node, you would typically use the getBBox method on the SVG element. This returns a bbox in the local coordinate system of that element. To determine the location of the SVG element in screen coordinates, then, you use getScreenCTM on the element to get a transformation matrix that will transform that element's local coordinates to screen coordinates. You then transform the returned bbox by the returned transformation matrix. Here's some code to do this:
function getBoundingBoxInArbitrarySpace(element,mat){
var svgRoot = element.ownerSVGElement;
var bbox = element.getBBox();
var cPt1 = svgRoot.createSVGPoint();
cPt1.x = bbox.x;
cPt1.y = bbox.y;
cPt1 = cPt1.matrixTransform(mat);
// repeat for other corner points and the new bbox is
// simply the minX/minY to maxX/maxY of the four points.
var cPt2 = svgRoot.createSVGPoint();
cPt2.x = bbox.x + bbox.width;
cPt2.y = bbox.y;
cPt2 = cPt2.matrixTransform(mat);
var cPt3 = svgRoot.createSVGPoint();
cPt3.x = bbox.x;
cPt3.y = bbox.y + bbox.height;
cPt3 = cPt3.matrixTransform(mat);
var cPt4 = svgRoot.createSVGPoint();
cPt4.x = bbox.x + bbox.width;
cPt4.y = bbox.y + bbox.height;
cPt4 = cPt4.matrixTransform(mat);
var points = [cPt1,cPt2,cPt3,cPt4]
//find minX,minY,maxX,maxY
var minX=Number.MAX_VALUE;
var minY=Number.MAX_VALUE;
var maxX=0
var maxY=0
for(i=0;i<points.length;i++)
{
if (points[i].x < minX)
{
minX = points[i].x
}
if (points[i].y < minY)
{
minY = points[i].y
}
if (points[i].x > maxX)
{
maxX = points[i].x
}
if (points[i].y > maxY)
{
maxY = points[i].y
}
}
//instantiate new object that is like an SVGRect
var newBBox = {"x":minX,"y":minY,"width":maxX-minX,"height":maxY-minY}
return newBBox;
}
function getBBoxInScreenSpace(element){
return getBoundingBoxInArbitrarySpace(element,element.getScreenCTM());
}
This code was taken from here, and is Apache-licensed. getBoundingBoxInArbitrarySpace has been tested, but getBBoxInScreenSpace hasn't (but I think it should work).
offsetParent is not a standard property of SVG elements, although some browsers may provide one anyway.
Depending on what you want to do with the information, using getScreenCTM or getCTM will probably work for you. For example, here's how you might calculate the position in pixels of (0, 0) relative to the element:
var
matrix = element.getScreenCTM(),
point = element.createSVGPoint();
point.x = 0;
point.y = 0;
point = point.matrixTransform(matrix.inverse());
"SVGElement.offsetParent' is deprecated and will be removed in M50" = april 2016 offsetParent will be removed
You can use "getBoundingClientRect()"
for more info : https://www.chromestatus.com/features/5724912467574784
Related
I have (1) :
a big container : <div id="container"> which has absolute positionning
inside this container, lots of <div class="txt"> which have absolute positionning as well
I would like to find the coordinates of a bounding box that contains all the <div class="txt"> elements. I have this code :
minX = 1000000, maxX = - 1000000;
minY = 1000000, maxY = - 1000000;
$(".txt").each(function(index) {
minX = Math.min(minX, parseFloat($(this).css("left")));
maxX = Math.max(maxX, parseFloat($(this).css("left")));
minY = Math.min(minY, parseFloat($(this).css("top")));
maxY = Math.max(maxY, parseFloat($(this).css("top")));
});
I don't think it's really correct because maxX will contain the max of each element's left , thus the max of the top-left-corner of each element. So the bouding-box will not contain the <div> which is the most on the left.
Then, in order to take this into account, I could compute
maxX = Math.max(maxX, parseFloat($(this).css("left")) + parseFloat($(this).css("width")))
but it begins to be dirty.
Is there a more elegant way to know the bounding box of lots of elements ?
Notes : (1) : It is for my project named BigPicture, the div name are a little different on the website.
(2) : would this fit more in codereview.SE ?
If your using jQuery, with .position() it is probably easier to get the coordinates of the elements, use .width() and .height() to get the other needed values. The values are already numbers so there is no need for the parsing functions.
$(".txt").each(function(index) {
var position = $(this).position();
var width = $(this).width();
var height = $(this).height();
minX = Math.min(minX, position.left));
maxX = Math.max(maxX, position.left + width);
minY = Math.min(minY, position.top);
maxY = Math.max(maxY, position.top + height);
});
You could also consider using vanilla JavaScript, here a link that could help you port your codebase.
In case IE < 9 compatibility is not needed:
var textElements = document.getElementsByClassName('txt');
for(var i = 0; i < textElements.length; ++i){
minX = Math.min(minX, textElements[i].offsetLeft);
maxX = Math.max(maxX, textElements[i].offsetLeft + textElements[i].offsetWidth);
minY = Math.min(minY, textElements[i].offsetTop);
maxY = Math.max(maxY, textElements[i].offsetTop + textElements[i].offsetHeight);
}
I tried to generate multiple canvases on the fly and when I create a new canvas, the previous one disappears. See here for an example:
http://jsfiddle.net/adrianh/5jspv/4/
Here is the javascript code:
var circleCount = 0;
function circleRect(rect)
{
var diameter = Math.sqrt(rect.width*rect.width+rect.height*rect.height);
var cx = (rect.right + rect.left)/2;
var cy = (rect.top + rect.bottom)/2;
var left = Math.floor(cx - diameter/2);
var top = Math.floor(cy - diameter/2);
diameter = Math.floor(diameter);
var html = "<canvas id='circleCanvas"+circleCount+"' "+
"width='"+(diameter+2)+"' "+
"height='"+(diameter+2)+"' "+
"style='"+
"position:absolute;"+
"z-index:0;"+
"left:"+(left-1)+"px;"+
"top:"+(top-1)+"px;"+
//"border:1px solid;"+
"' />";
alert(html);
var container = document.getElementById("circles");
container.innerHTML += html;
var c=document.getElementById("circleCanvas"+circleCount);
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.arc(diameter/2+1,diameter/2+1,diameter/2,0,2*Math.PI);
ctx.stroke();
++circleCount;
}
$(".circled").each(function(i, obj) {
var rect = obj.getBoundingClientRect();
circleRect(rect);
});
Why is only one canvas showing up?
It will be more reliable to manipulate the DOM rather than trying to inline things with innerHTML. This code uses jQuery's DOM manipulation methods:
var circleCount = 0;
function circleRect(rect)
{
var diameter = Math.sqrt(rect.width*rect.width+rect.height*rect.height);
var cx = (rect.right + rect.left)/2;
var cy = (rect.top + rect.bottom)/2;
var left = Math.floor(cx - diameter/2);
var top = Math.floor(cy - diameter/2);
diameter = Math.floor(diameter);
var html = $("<canvas id='circleCanvas"+circleCount+"' "+
"width='"+(diameter+2)+"' "+
"height='"+(diameter+2)+"' "+
"style='"+
"position:absolute;"+
"z-index:0;"+
"left:"+(left-1)+"px;"+
"top:"+(top-1)+"px;"+
"' />");
$("#circles").append(html);
var ctx=html[0].getContext("2d");
ctx.beginPath();
ctx.arc(diameter/2+1,diameter/2+1,diameter/2,0,2*Math.PI);
ctx.stroke();
++circleCount;
}
You could also use the standard createElement and appendChild if you don't really need jQuery.
The innerHTML property has a number of drawbacks, although there is nothing specific I can find about not using += with it, the fact that insertAdjacentHTML exists would seem to indicate that you shouldn't really expect it to work well. (forgot this bit earlier) In this case, as you correctly surmised in your comment, the canvas you've drawn on is replaced by a new one when the assignment to innerHTML happens.
I am trying to position a set of elements including text, path and circle in raphael. Is there a way to position the entire set in the svg? The SVG itself covers the entire height and width of the window and the set needs to be positioned in the center of the SVG.
The code is as follows:
var r = Raphael("holder", windowWidth, windowHeight);
var set = r.set(
(r.text(100,250,"ab").attr({...})),
(r.text(295,250,"c").attr({..})),
(r.path("M400,217L400,317").attr({...})),
(r.circle(190, 290, 13).attr({...})));
for (var i=0; i<set.length; i++) {
set[i].node.setAttribute("class", "set");
}
I've added the class in case the set could be manipulated using css, but I havent found any method to do so.
Any ideas on how to solve this problem?
You can do a transform on a set, like http://jsbin.com/EzUFiD/1/edit?html,js,output (fiddle isn't working for me)
var svgWidth = 600;
var svgHeight = 600;
var r = Raphael("holder", svgWidth, svgHeight);
var set = r.set(
(r.text(50,50,"ab")),
(r.text(50,60,"c")),
(r.path("M100,17L40,31")),
(r.circle(80, 80, 13))
);
var bbox = set.getBBox(); //find surrounding rect of the set
var posx = svgWidth / 2 - bbox.width / 2 - bbox.x; // find new pos depending on where it is initially
var posy = svgHeight / 2 - bbox.height / 2 - bbox.y;
set.transform('t' + posx + ',' + posy);
I am using Kinetic.js for this, but I figure this is not Kinetic specific. The problem is as follows:
I am loading an image in a canvas, and I then use Kinetic to rotate this image. How do I get the x and y of, for example, leftmost point of this image?
Try doing the calculations manually:
var theta = image.getRotation*Math.PI/180.;
// Find the middle rotating point
var midX = image.getX() + image.getWidth()/2;
var midY = image.getY() + image.getHeight()/2;
// Find all the corners relative to the center
var cornersX = [image.getX()-midX, image.getX()-midX, image.getX()+image.getWidth()-midX, image.getX()+image.getWidth()-midX];
var cornersY = [image.getY()-midY, image.getY()+image.getHeight()-midY, midY-image.getY(), image.getY()+image.getHeight()-midY];
// Find new the minimum corner X and Y by taking the minimum of the bounding box
var newX = 1e10;
var newY = 1e10;
for (var i=0; i<4; i=i+1) {
newX = min(newX, cornersX[i]*Math.cos(theta) - cornersY[i]*Math.sin(theta) + midX);
newY = min(newY, cornersX[i]*Math.sin(theta) + cornersY[i]*Math.cos(theta) + midY);
}
// new width and height
newWidth = midX - newX;
newHeight = midY - newY;
The extension I'm talking about is the Raphael-zpd: http://pohjoisespoo.net84.net/src/raphael-zpd.js
/* EDIT The script is added to a Raphael document with this command var zpd = new RaphaelZPD(paper, { zoom: true, pan: true, drag: false}); where paper is your canvas */
The script was originally released at the authors github http://www.github.com/somnidea which no longer exists.
What I wanted to do was run the mousewheel zoom out to the threshold as soon as the raphael is loaded. The zoomthreshold is set at the beginning of the script zoomThreshold: [-37, 20]. In the mousewheel scroll function it is compared to zoomCurrent which is by default 0 me.zoomCurrent = 0;
This is the whole mousewheel event part
me.handleMouseWheel = function(evt) {
if (!me.opts.zoom) return;
if (evt.preventDefault)
evt.preventDefault();
evt.returnValue = false;
var svgDoc = evt.target.ownerDocument;
var delta;
if (evt.wheelDelta)
delta = evt.wheelDelta / 3600; // Chrome/Safari
else
delta = evt.detail / -90; // Mozilla
if (delta > 0) {
if (me.opts.zoomThreshold)
if (me.opts.zoomThreshold[1] <= me.zoomCurrent) return;
me.zoomCurrent++;
} else {
if (me.opts.zoomThreshold)
if (me.opts.zoomThreshold[0] >= me.zoomCurrent) return;
me.zoomCurrent--;
}
var z = 1 + delta; // Zoom factor: 0.9/1.1
var g = svgDoc.getElementById("viewport"+me.id);
var p = me.getEventPoint(evt);
p = p.matrixTransform(g.getCTM().inverse());
// Compute new scale matrix in current mouse position
var k = me.root.createSVGMatrix().translate(p.x, p.y).scale(z).translate(-p.x, -p.y);
me.setCTM(g, g.getCTM().multiply(k));
if (!me.stateTf)
me.stateTf = g.getCTM().inverse();
me.stateTf = me.stateTf.multiply(k.inverse());
}
The reason I can't just draw a smaller SVG to begin with is that I'm using raster images as the background and need them to be higher resolution. I would still like to start at the furthest point I've set at the threshold. Is it possible for me to somehow use this script to do this? I'm naturally using it otherwise to handle mouse zoom/pan.
//EDIT
There is also this function at the end of the script, but so far I've been unable to work it.
Raphael.fn.ZPDPanTo = function(x, y) {
var me = this;
if (me.gelem.getCTM() == null) {
alert('failed');
return null;
}
var stateTf = me.gelem.getCTM().inverse();
var svg = document.getElementsByTagName("svg")[0];
if (!svg.createSVGPoint) alert("no svg");
var p = svg.createSVGPoint();
p.x = x;
p.y = y;
p = p.matrixTransform(stateTf);
var element = me.gelem;
var matrix = stateTf.inverse().translate(p.x, p.y);
var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")";
element.setAttribute("transform", s);
return me;
}
Seems like it's used for panning through the document through say click events so that a click would execute the function with the given coordinates. However, as said I've been unable to work it. I don't know how it's supposed to function. I tried paper.ZPDPanTo(100, 100); as well as just ZPDPanTo(100,100) but nothing happens.
You may also want to check out the working branch for Raphaël 2.0, which supposedly adds support for viewBox and transforms, see https://github.com/DmitryBaranovskiy/raphael/tree/2.0.
This doesn't answer your question fully, but it seems quite possible that Raphaël 2.0 will address your use-case.
If you're using pure svg then you can manipulate the zoom&pan positions via the SVG DOM properties currentTranslate and currentScale, see this example.
An example using RAPHAEL ZPD:
var paper = Raphael("container",800,760);
window.paper = paper;
zpd = new RaphaelZPD(paper, { zoom: true, pan: true, drag: false });
paper.circle(100,100, 50).attr({fill:randomRGB(),opacity:0.95});
paper.rect(100,100, 250, 300).attr({fill:randomRGB(),opacity:0.65});
paper.circle(200,100, 50).attr({fill:randomRGB(),opacity:0.95});
paper.circle(100,200, 50).attr({fill:randomRGB(),opacity:0.95});
http://jsfiddle.net/4PkRm/1/