What is the proper way to get the dimensions of an svg element?
http://jsfiddle.net/langdonx/Xkv3X/
Chrome 28:
style x
client 300x100
offset 300x100
IE 10:
stylex
client300x100
offsetundefinedxundefined
FireFox 23:
"style" "x"
"client" "0x0"
"offset" "undefinedxundefined"
There are width and height properties on svg1, but .width.baseVal.value is only set if I set the width and height attributes on the element.
The fiddle looks like this:
HTML
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="1" fill="red" />
<circle cx="150" cy="50" r="40" stroke="black" stroke-width="1" fill="green" />
<circle cx="250" cy="50" r="40" stroke="black" stroke-width="1" fill="blue" />
</svg>
JS
var svg1 = document.getElementById('svg1');
console.log(svg1);
console.log('style', svg1.style.width + 'x' + svg1.style.height);
console.log('client', svg1.clientWidth + 'x' + svg1.clientHeight);
console.log('offset', svg1.offsetWidth + 'x' + svg1.offsetHeight);
CSS
#svg1 {
width: 300px;
height: 100px;
}
Use the getBBox function:
The SVGGraphicsElement.getBBox() method allows us to determine the coordinates of the smallest rectangle in which the object fits. [...]
http://jsfiddle.net/Xkv3X/1/
var bBox = svg1.getBBox();
console.log('XxY', bBox.x + 'x' + bBox.y);
console.log('size', bBox.width + 'x' + bBox.height);
FireFox have problemes for getBBox(), i need to do this in vanillaJS.
I've a better Way and is the same result as real svg.getBBox() function !
With this good post : Get the real size of a SVG/G element
var el = document.getElementById("yourElement"); // or other selector like querySelector()
var rect = el.getBoundingClientRect(); // get the bounding rectangle
console.log( rect.width );
console.log( rect.height);
I'm using Firefox, and my working solution is very close to obysky. The only difference is that the method you call in an svg element will return multiple rects and you need to select the first one.
var chart = document.getElementsByClassName("chart")[0];
var width = chart.getClientRects()[0].width;
var height = chart.getClientRects()[0].height;
SVG has properties width and height. They return an object SVGAnimatedLength with two properties: animVal and baseVal. This interface is used for animation, where baseVal is the value before animation. From what I can see, this method returns consistent values in both Chrome and Firefox, so I think it can also be used to get calculated size of SVG.
This is the consistent cross-browser way I found:
var heightComponents = ['height', 'paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth'],
widthComponents = ['width', 'paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'];
var svgCalculateSize = function (el) {
var gCS = window.getComputedStyle(el), // using gCS because IE8- has no support for svg anyway
bounds = {
width: 0,
height: 0
};
heightComponents.forEach(function (css) {
bounds.height += parseFloat(gCS[css]);
});
widthComponents.forEach(function (css) {
bounds.width += parseFloat(gCS[css]);
});
return bounds;
};
From Firefox 33 onwards you can call getBoundingClientRect() and it will work normally, i.e. in the question above it will return 300 x 100.
Firefox 33 will be released on 14th October 2014 but the fix is already in Firefox nightlies if you want to try it out.
Use .getAttribute()!
var height = document.getElementById('rect').getAttribute("height");
var width = document.getElementById('rect').getAttribute("width");
var x = document.getElementById('rect').getAttribute("x");
alert("height: " + height + ", width: " + width + ", x: " + x);
<svg width="500" height="500">
<rect width="300" height="100" x="50" y="50" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" id="rect"/>
</svg>
A save method to determine the width and height unit of any element (no padding, no margin) is the following:
let div = document.querySelector("div");
let style = getComputedStyle(div);
let width = parseFloat(style.width.replace("px", ""));
let height = parseFloat(style.height.replace("px", ""));
Related
I'm trying to work on a way to detect if a path of an svg file that is within a canvas is clicked on. My code works in Firefox however it does not work on Chromium browsers. I have surrounded the code with try and catch and I receive no errors on Firefox and everything works however on Chromium browsers I receive the error:
TypeError: Failed to execute 'isPointInFill' on 'SVGGeometryElement': parameter 1 is not of type 'SVGPoint'.
at getIdOfElementAtPoint (main.js:39:27)
at HTMLCanvasElement.<anonymous> (main.js:70:9)
However on the next piece of code no error is thrown.
This is my code:
function getIdOfElementAtPoint(event) {
var paths = svg.getElementsByTagName("path");
//loop through all the path element in svg
for (var i = 0; i < paths.length; i++) {
var path = paths[i];
// Check if point (x, y) is inside the fill of the path
let inFill = false;
try {
inFill = path.isPointInFill(new DOMPoint(event.clientX, event.clientY))
} catch(error) {
console.log(error)
try {
const point = svg.createSVGPoint();
// Get the coordinates of the click
point.x = event.clientX;
point.y = event.clientY;
inFill = path.isPointInFill(point)
} catch(error) {
console.log(error)
}
}
if (inFill) {
console.log("The point is inside the element with id: " + path.getAttribute("id"));
return path.getAttribute("id");
}
}
console.log("The point is outside of any element.");
}
As commented by #Kaiido:
document.elementsFromPoint() or
document.elementFromPoint()
are probably the better options to get SVG elements at certain cursor positions.
However you address several issues:
browser support for DOMPoint() used for SVG methods (firefox vs. chromium vs. webkit)
translation between HTML DOM coordinates (e.g. via mouse inputs) to SVG user units
retrieving all underlying elements at certain coordinates
Example snippet
/**
* static example: is #circleInside within #circle2
*/
let isInfill = false;
let p1 = { x: circleInside.cx.baseVal.value, y: circleInside.cy.baseVal.value };
let inFill1 = isPointInFill1(circle2, p1);
if (inFill1) {
circleInside.setAttribute("fill", "green");
}
function isPointInFill1(el, p) {
let log = [];
let point;
try {
point = new DOMPoint(p.x, p.y);
el.isPointInFill(point);
log.push("DOMPoint");
} catch {
let svg = el.nearestViewportElement;
point = svg.createSVGPoint();
[point.x, point.y] = [p.x, p.y];
log.push("SVGPoint");
}
console.log(log.join(" "));
let inFill = el.isPointInFill(point);
return inFill;
}
document.addEventListener("mousemove", (e) => {
let cursorPos = { x: e.clientX, y: e.clientY };
// move svg cursor
let svgCursor = screenToSVG(svg, cursorPos);
cursor.setAttribute("cx", svgCursor.x);
cursor.setAttribute("cy", svgCursor.y);
/**
* pretty nuts:
* we're just testing the reversal of svg units to HTML DOM units
* just use the initial: e.clientX, e.clientY
*/
// move html cursor
let domCursorPos = SVGToScreen(svg, svgCursor);
cursorDOM.style.left = domCursorPos.x + "px";
cursorDOM.style.top = domCursorPos.y + "px";
// highlight
let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y);
let log = ['elementsFromPoint: '];
elsInPoint.forEach((el) => {
if (el instanceof SVGGeometryElement && el!==cursor) {
log.push(el.id);
}
});
result.textContent = log.join(" | ");
});
/**
* helper function to translate between
* svg and HTML DOM coordinates:
* based on #Paul LeBeau's anser to
* "How to convert svg element coordinates to screen coordinates?"
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates/48354404#48354404
*/
function screenToSVG(svg, p) {
let pSvg = svg.createSVGPoint();
pSvg.x = p.x;
pSvg.y = p.y;
return pSvg.matrixTransform(svg.getScreenCTM().inverse());
}
function SVGToScreen(svg, pSvg) {
let p = svg.createSVGPoint();
p.x = pSvg.x;
p.y = pSvg.y;
return p.matrixTransform(svg.getScreenCTM());
}
body{
margin: 0em
}
svg{
width:25%;
border:1px solid #ccc
}
.highlight{
opacity:0.5
}
.cursorDOM{
display:block;
position:absolute;
top:0;
left:0;
margin-left:-0.5em;
margin-top:-0.5em;
font-size:2em;
width:1em;
height:1em;
outline: 1px solid green;
border-radius:50%;
pointer-events:none;
}
<p id="result"></p>
<p id="resultNext"></p>
<div id="cursorDOM" class="cursorDOM"></div>
<svg id="svg" viewBox="0 0 100 100">
<rect id="rectBg" x="0" y="0" width="100%" height="100%" fill="#eee" />
<circle id="circle0" cx="75" cy="50" r="66" fill="#ccc" />
<circle id="circle1" cx="75" cy="50" r="50" fill="#999" />
<circle id="circle2" cx="75" cy="50" r="33" />
<circle id="circleInside" cx="95" cy="50" r="5" fill="red" />
<circle id="cursor" cx="0" cy="0" r="2" fill="red" />
</svg>
1. DOMPoint() or createSVGPoint() for pointInFill()?
You're right: DOMPoint() is currently (as of 2023) not supported by chromium (blink) for some svg related methods.
createSVGPoint() works well in chromium as well as in firefox – although it's classified as deprecated.
Quite likely chromium will catch up to firefox.
But isPointInFill() or isPointInStroke() are used for checking point intersection for single elements.
2. Translate coordinates
Depending on your layout, you probably need to convert coordinates.
See #Paul LeBeau's answer: "How to convert svg element coordinates to screen coordinates?"
3. Get all underlying elements
document.elementsFromPoint() is probably the best way to go.
However, this method will also return HTML DOM elements.
So you might need some filtering for svg geometry elements like so:
let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y);
elsInPoint.forEach((el) => {
if (el instanceof SVGGeometryElement) {
console.log(el)
}
});
I have a SVG line that triggers on scroll and goes down the page as i scroll. I want the trigger to be at a specific location on the page, but for now, even though the SVG is located further down the page, it starts "growing" as soon as the user starts scrolling (which result in no animation when i arrive at that location, since the SVG is already all scrolled out).
Here's the js of that particular SVG:
var path = document.querySelector('#star-path');
var pathLength = path.getTotalLength();
path.style.strokeDasharray = pathLength + ' ' + pathLength;
path.style.strokeDashoffset = pathLength;
window.addEventListener('scroll', () => {
var scrollPercentage = (document.documentElement.scrollTop + document.body.scrollTop) / (document.documentElement.scrollHeight - document.documentElement.clientHeight);
var drawLength = pathLength * scrollPercentage;
path.style.strokeDashoffset = pathLength - drawLength;
});
And here is the html snippet...
<svg class="svg" width="10" viewBox="0 0 2 150" id="star-svg">
<path id="star-path" fill="none" stroke="black" stroke-width="2" d="M1 0V150" />
</svg>
... as well as the css:
.svg {
position: absolute;
top: 2900px;
left: 1400px;
text-align: center;
overflow: visible;
}
Which element corresponds to that trigger, and what should i do to "delay" it (i am quite new at SVG animations)?
Thanks in advance for your help
I use position() and getBoundingClientRect() to determine if svg object is visible in viewport. Works perfectly well in Chrome, Firefox and EDGE but not in Safari.
obj.position() always returns {top: 0, left: 0} in Safari.
What could be wrong?
options.elements.forEach(function (item) {
let obj = jQuery("#" + item);
if (!!obj && !!obj[0]) {
let position = obj.position();
let box = obj[0].getBoundingClientRect()
if (!!position) {
if (position.left <= left + width &&
position.left + box.width >= left &&
position.top <= top + height &&
position.top + box.height >= top)
onscreen.push(item);
}
}
});
Here is part of HTML. I omitted contents of 'd' of svg
<div id="svgmap">
<svg id="svg">
<g id="R918">
<path d="...." transform="translate(-0.08 0.25)"//>
</g>
<g id="C902J">
<path d="...." transform="translate(-0.08 0.25)"/>
</g>
<g id="C9002I">
<path d="" transform="translate(-0.08 0.25)"/>
</g>
</svg>
Ok, so I didn't figure out why position() is not working in Safari in this particular case but I realized that I don't need to user position() at all. I can get the same data from getBoundingClientRect().left and getBoundingClientRect().top
I am currently using velocity.js to control some animation on an SVG drawing I am creating. It is interactive with the user, so when a click happens in certain parts, the picture might grow to the right or down. So far everything is working perfectly until the picture gets too big for the SVG box.
In those situations, I simply resize the viewBox to scale the image.
svgDoc.setAttribute("viewBox", "0 0 " + svgDocWidth + " " + svgDocHeight);
This works, but it doesn't look great because it isn't animated. It just jumps to the new size. Is there a way to animate with velocity.js a viewBox change?
I've tried this approach:
$viewBox = $(svgDoc.viewBox);
$viewBox.velocity({height: svgDocHeight, width: svgDocWidth});
But that doesn't do anything.
Is this beyond what velocity.js can support?
Solution on 2015-11-21
#Ian gave the solution I eventually used. It ended up looking like this:
var origHeight = this.svgDoc.viewBox.animVal.height;
var origWidth = this.svgDoc.viewBox.animVal.width;
var docHeight = this.SvgHeight();
var docWidth = this.SvgWidth();
if ((origHeight != docHeight) || (origWidth != docWidth))
{
var deltaHeight = docHeight - origHeight;
var deltaWidth = docWidth - origWidth;
$(this.svgDoc).velocity(
{
tween: 1
},
{
progress: function(elements, complete, remaining, start, tweenValue)
{
var newWidth = origWidth + complete * deltaWidth;
var newHeight = origHeight + complete * deltaHeight;
elements[0].setAttribute('viewBox', '0 0 ' + newWidth + ' ' + newHeight);
}
});
}
You can animate a viewBox smoothly via SMIL. Like this...
<svg width="100%" height="100%" viewBox="0 0 200 200">
<animate attributeName="viewBox" to="0 0 400 400" dur="5s" fill="freeze" />
<circle cx="100" cy="100" r="100" fill="green" />
</svg>
I think SMIL is being deprecated in Chrome (please correct me if I'm wrong), so I'm guessing there will be increasing reliance on other methods.
There's no reason you can't use velocity, but with your own calculations and the progress/tweenValue parameter, eg something a bit like this...
$("#mysvg").velocity(
{ tween: 200 },
{ progress: animViewbox }
)
function animViewbox (elements, complete, remaining, start, tweenValue) {
elements[0].setAttribute('viewBox', '0 0 ' + tweenValue + ' ' + tweenValue);
}
jsfiddle
I have an SVG file with an XHTML table. I want to connect portions of the table with SVG drawing (via JavaScript). For example, in the following file I want to place each of the yellow circles centered on the right ends of the red borders:
<?xml version="1.0"?>
<svg viewBox="0 0 1000 650" version="1.1" baseProfile="full"
xmlns="http://www.w3.org/2000/svg">
<title>Connect</title>
<style type="text/css"><![CDATA[
table { border-collapse:collapse; margin:0 auto; }
table td { padding:0; border-bottom:1px solid #c00;}
circle.dot { fill:blue }
]]></style>
<foreignObject x="100" width="800" height="600" y="400"><body xmlns="http://www.w3.org/1999/xhtml"><table><thead><tr>
<th>Space</th>
</tr></thead><tbody>
<tr><td><input name="name" type="text" value="One"/></td></tr>
<tr><td><input name="name" type="text" value="Two"/></td></tr>
</tbody></table></body></foreignObject>
<circle class="dot" id="dot1" cx="100" cy="100" r="5" />
<circle class="dot" id="dot2" cx="200" cy="100" r="5" />
</svg>
How can I best find the location of an arbitrary HTML element in global SVG coordinate space?
For simplicity, feel free to ignore browser resizing, and any transformation stacks that may wrap the <foreignObject>.
Here's what I have come up with, in action: HTML Element Location in SVG (v2)
// Find the four corners (nw/ne/sw/se) of any HTML element embedded in
// an SVG document, transformed into the coordinates of an arbitrary SVG
// element in the document.
var svgCoordsForHTMLElement = (function(svg){
var svgNS= svg.namespaceURI, xhtmlNS = "http://www.w3.org/1999/xhtml";
var doc = svg.ownerDocument;
var wrap = svg.appendChild(doc.createElementNS(svgNS,'foreignObject'));
var body = wrap.appendChild(doc.createElementNS(xhtmlNS,'body'));
if (typeof body.getBoundingClientRect != 'function') return;
body.style.margin = body.style.padding = 0;
wrap.setAttribute('x',100);
var broken = body.getBoundingClientRect().left != 0;
svg.removeChild(wrap);
var pt = svg.createSVGPoint();
return function svgCoordsForHTMLElement(htmlEl,svgEl){
if (!svgEl) svgEl = htmlEl.ownerDocument.documentElement;
for (var o=htmlEl;o&&o.tagName!='foreignObject';o=o.parentNode){}
var xform = o.getTransformToElement(svgEl);
if (broken) xform = o.getScreenCTM().inverse().multiply(xform);
var coords = {};
var rect = htmlEl.getBoundingClientRect();
pt.x = rect.left; pt.y = rect.top;
coords.nw = pt.matrixTransform(xform);
pt.y = rect.bottom; coords.sw = pt.matrixTransform(xform);
pt.x = rect.right; coords.se = pt.matrixTransform(xform);
pt.y = rect.top; coords.ne = pt.matrixTransform(xform);
return coords;
};
})(document.documentElement);
And here's how you use it:
// Setting SVG group origin to bottom right of HTML element
var pts = svgCoordsForHTMLElement( htmlEl );
var x = pts.sw.x, y = pts.sw.y;
svgGroup.setAttribute( 'transform', 'translate('+x+','+y+')' );
Caveats:
It works perfectly in Firefox v7
It works for Chrome v16 and Safari v5, except:
It does not work well if the browser zoom has been adjusted.
It does not work if the <foreignObject> has been rotated or skewed, due to this Webkit bug.
It does not work for IE9 (XHTML isn't showing up, and getTransformToElement does not work).