I have an SVG animation that requires a spinning element. SMIL and css based animations are out of the question as IE does not support them and the beginElement() property is also not supported in IE or Firefox. IE sizing is not the problem as CSS fixes that very well.
The problem is that it is not a standard shape (rect, circle or so on) so the svg animate attributes fail in spectacular fashion as there is not a central point by default - getBBox() is the preferred method.
I have a plunk with an example of the svg within html. The script is necessarily within the svg for ease of portability and I have got it to work in Chrome, Firefox and Opera, but IE fails to rotate the element although no errors are shown. the JS is below:
var svgNS = "http://www.w3.org/2000/svg";
function init(evt) {
addRotateTransform('spinner', 1.5, 1);
}
function addRotateTransform(target_id, dur, dir) {
var my_element = document.getElementById(target_id);
var a = document.createElementNS(svgNS, "animateTransform");
var bb = my_element.getBBox();
var cx = bb.x + bb.width/2;
var cy = bb.y + bb.height/2;
a.setAttributeNS(null, "attributeName", "transform");
a.setAttributeNS(null, "attributeType", "XML");
a.setAttributeNS(null, "type", "rotate");
a.setAttributeNS(null, "dur", dur + "s");
a.setAttributeNS(null, "repeatCount", "indefinite");
a.setAttributeNS(null, "from", "0 "+cx+" "+cy);
a.setAttributeNS(null, "to", 360*dir+" "+cx+" "+cy);
my_element.appendChild(a);
try {
a.beginElement(); // this works in Chrome
}
catch(err) {
window.setTimeout(init, 0); // this works in FF
}
}
Here is a version using javascript. It just useres a timeout() to update a transform on the path.
<h1>SVG spinner</h1>
<p>The javascript should be placed within the svg to maintain portability throughout different projects as we don't want to load all of the libraries required for a simple loading screen.</p>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 251.3 136" enable-background="new 0 0 251.3 136" xml:space="preserve" onload="init(evt)">
<script type="text/ecmascript"><![CDATA[
var svgNS = "http://www.w3.org/2000/svg";
function init(evt) {
addRotateTransform('spinner', 0);
}
function addRotateTransform(target_id, angle) {
var my_element = document.getElementById(target_id);
var bb = my_element.getBBox();
var cx = bb.x + bb.width/2;
var cy = bb.y + bb.height/2;
my_element.setAttribute("transform", "rotate("+angle+","+cx+","+cy+")");
window.setTimeout(function() {
addRotateTransform('spinner', angle+2);
}, 16);
}
]]></script>
<!-- The rest of the svg has been removed from here -->
<g>
<path id="spinner" opacity="0.6" fill="#ed1b1e" d="M228.4,82.8c7.5,1.3,15.1-2.5,18.5-9.3c3.7-7.4,1.2-16.8-5.8-21.2c-0.9-0.6-1.8-1-2.8-1.4
c-0.2-0.1-1-0.2-1.1-0.4c-0.1-0.1,0-1.1,0-1.3c-0.2-1.6-1.2-2.9-2.6-3.6c-1.3-0.7-2.6-0.5-4-0.4c-1.9,0.2-3.7,0.7-5.4,1.4
c-7.4,3.1-11.9,10.3-11.9,18.2C213.1,73.7,220,81.4,228.4,82.8z m-7.9,-15.6c-0.4-2,0.3-4.5,1.2-6.3c1.1-2.2,2.9-4,5.2-5.1
c1.9-0.9,3.7-0.9,5.7-1c2.2-0.1,4-1.6,4.5-3.7c5.8,2.3,9.7,8.3,9.2,14.6c-0.3,3.6-2,7-4.7,9.3c-2.8,2.4-5.9,3.2-9.5,3.2
c0.3,0-0.2,0-0.3-0.1c-0.6-0.1-1.2-0.1-1.8-0.3c-1.8-0.4-3.5-1.3-4.9-2.4C222.4,73.4,220.9,70.5,220.5,67.2
C220.4,66.8,220.5,67.4,220.5,67.2z"/>
</g>
</svg>
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)
}
});
HTML code:
<div class="dots" id="dotsSlider">
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="30" viewBox="0 0 400 60" id="ante">
<rect width="100%" height="100%" fill="none" />
</svg>
</div>
Javascript code:
for (let i = 0; i < 10; i++) {
// targeting the svg itself
const svgns = "http://www.w3.org/2000/svg";
// variable for the namespace
var svg = document.querySelector("svg");
var x = i.toString();
var name = "newCircle" + x;
var name2 = name;
var counter = i + 1;
var position = 36 * counter;
// make a simple rectangle
var name = document.createElementNS(svgns, "circle");
// set attributes of new circle
gsap.set(name, {attr: {
cx: position,
cy: 25,
r: 8,
width: 30,
height: 30,
fill: "black",
id: name2
}});
// append the new rectangle to the svg
svg.appendChild(name);
}
So far so good I created 10 SVGs (dots for a slider). For loop is written inside a function that runs when the page loads. My problem is that I do not know how to access the fill property of these SVGs in order to be able to change it in a dynamic way inside another function. I tried everything related to getElement parsing it to a variable, but propably I am writing something wrong. Any help is much appreciated.
Try this:
var circle = document.getElementById("newCircle3"); // etc
circle.style.fill = "red";
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
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", ""));
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).