I have a simple SVG file like this :
<svg id="mySVG" /*other attributes"*/>
<group id="mixUps">
<ellipse /*ellipse atributtes blabla*/ />
<path /*this is a star-shaped path*/ />
<rect /*rect attributes*/ />
/*
Basically this is just a simple SVG group containing an ellipse, a path and a rectangle
*/
</g>
</svg>
and then, in the script file, I want this group to be scaled twice it's original size at mouseover event, and return to it's original size at mouseout :
<script>
var mySvg = Snap("#mySvg");
var mixUps = mySvg.select("#mixUps");
function mixCursor(evt){
if(evt.type==="mouseover"){
mixUps.animate({transform:"s2"}, 250);
}else if(evt.type==="mouseout"){
mixUps.animate({transform:"s1"}, 250);
}
}
mixUps.mouseover(mixCursor);
mixUps.mouseout(mixCursor);
</script>
However, at the first mouseover event, the group somehow translated (moved) to upper-left corner, and stays there, why is this? How to make this group stays in place when scaled?
I put the file here.
You need to include the initial transform that's in place, otherwise it will get overwritten, so you want original transform THEN new transform, so instead of
mixUps.animate({transform: 's2'}, 250);
use
mixUps.animate({transform: this.transform() + 's2'}, 250);
jsfiddle
Or better is to store the original transform, so we can revert back to it later...
jsfiddle
mixUps.data('originalTransform', mixUps.transform() )
...
mixUps.animate({transform: this.data('originalTransform') + 's2'}, 250);
...
mixUps.animate({transform: this.data('originalTransform') }, 250);
Related
Is there a way to get the screen/window coordinates from a svg element ?
I have seen solutions for the other way around like:
function transformPoint(screenX, screenY) {
var p = this.node.createSVGPoint()
p.x = screenX
p.y = screenY
return p.matrixTransform(this.node.getScreenCTM().inverse())
}
But what i need in my case are the screen coordinates.
Sory if it's an obvious question, but i'm new to svg.
Thanks.
The code you included in your question converts screen coordinates to SVG coordinates. To go the other way, you have to do the opposite of what that function does.
getScreenCTM() returns the matrix that you need to convert the coordinates. Notice that the code calls inverse()? That is inverting the matrix so it does the conversion in the other direction.
So all you need to do is remove the inverse() call from that code.
var svg = document.getElementById("mysvg");
function screenToSVG(screenX, screenY) {
var p = svg.createSVGPoint()
p.x = screenX
p.y = screenY
return p.matrixTransform(svg.getScreenCTM().inverse());
}
function SVGToScreen(svgX, svgY) {
var p = svg.createSVGPoint()
p.x = svgX
p.y = svgY
return p.matrixTransform(svg.getScreenCTM());
}
var pt = screenToSVG(20, 30);
console.log("screenToSVG: ", pt);
var pt = SVGToScreen(pt.x, pt.y);
console.log("SVGToScreen: ", pt);
<svg id="mysvg" viewBox="42 100 36 40" width="100%">
</svg>
I was playing around with this snippet below when I wanted to do the same (learn which screen coordinates correspond to the SVG coordinates). I think in short this is what you need:
Learn current transformation matrix of the SVG element (which coordinates you are interested in), roughly: matrix = element.getCTM();
Then get screen position by doing, roughly: position = point.matrixTransform(matrix), where "point" is a SVGPoint.
See the snippet below. I was playing with this by changing browser window size and was altering svg coordinates to match those of the div element
// main SVG:
var rootSVG = document.getElementById("rootSVG");
// SVG element (group with rectangle inside):
var rect = document.getElementById("rect");
// SVGPoint that we create to use transformation methods:
var point = rootSVG.createSVGPoint();
// declare vars we will use below:
var matrix, position;
// this method is called by rootSVG after load:
function init() {
// first we learn current transform matrix (CTM) of the element' whose screen (not SVG) coordinates we want to learn:
matrix = rect.getCTM();
// then we "load" SVG coordinates in question into SVGPoint here:
point.x = 100; // replace this with the x co-ordinate of the path segment
point.y = 300; // replace this with the y co-ordinate of the path segment
// now position var will contain screen coordinates:
position = point.matrixTransform(matrix);
console.log(position)
// to validate that the coordinates are correct - take these x,y screen coordinates and apply to CSS #htmlRect to change left, top pixel position. You will see that the HTML div element will get placed into the top left corner of the current svg element position.
}
html, body {
margin: 0;
padding: 0;
border: 0;
overflow:hidden;
background-color: #fff;
}
svg {
position: fixed;
top:0%;
left:0%;
width:100%;
height:100%;
background:#fff;
}
#htmlRect {
width: 10px;
height: 10px;
background: green;
position: fixed;
left: 44px;
top: 132px;
}
<body>
<svg id="rootSVG" width="100%" height="100%" viewbox="0 0 480 800" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init()">
<g id="rect">
<rect id="rectangle" x="100" y="300" width="400" height="150"/>
</g>
</svg>
<div id="htmlRect"></div>
</body>
Not sure why it hasn't been suggested before, but `Element.getBoundingClientRect() should be enough:
const {
top, // x position on viewport (window)
left, // y position on viewport (window)
} = document.querySelector('rect').getBoundingClientRect()
I think other answers might be derived from a method promoted by Craig Buckler on SitePoint, where he explains using the SVGElement API (instead of getBoudingClientRect, from the - DOM - Element API) to convert DOM to SVG coordinates and vice-versa.
But 1. only DOM coordinates are required here 2. he claims that using getBoundingClientRect when transformations (via CSS or SVG) are applied will return incorrect values to translate to SVG coordinates, but the current specification for getBoundingClientRect takes those transformations into account.
The getClientRects() method, when invoked, must return the result of the following algorithm: [...]
If the element has an associated SVG layout box return a DOMRectList object containing a single DOMRect object that describes the bounding box of the element as defined by the SVG specification, applying the transforms that apply to the element and its ancestors.
Specification: https://drafts.csswg.org/cssom-view/#extension-to-the-element-interface
Support: https://caniuse.com/#feat=getboundingclientrect
2020
⚠️ Safari currently has several bugs that make this pretty difficult if you're working with SVGs (or SVG containers) that are transitioning, rotated, or scaled.
getScreenCTM() does not include ancestor scale and rotation transforms in the returned matrix. (If your svgs are neither rotated or scaled, then this is the way to go though.)
However, if you know the ancestor elements that are being scaled and/or rotated, and their transformation values, you can manually fix the matrix provided by getScreenCTM(). The workaround will look something like this:
let ctm = ele.getScreenCTM();
// -- adjust
let ancestorScale = // track yourself or derive from window.getComputedStyle()
let ancestorRotation = // track yourself or derive from window.getComputedStyle()
ctm = ctm.scale(ancestorScale)
ctm = ctm.rotate(ancestorRotation)
// !! Note: avoid ctm.scaleSelf etc. On some systems the matrix is a true blue SVGMatrix (opposed to a DOMMatrix) and may not support these transform-in-place methods
// --
// repeat 'adjust' for each ancestor, in order from closest to furthest from ele. Mind the order of the scale/rotation transformations on each ancestor.
If you don't know the ancestors... the best I've come up with is a trek up the tree looking for transformations via getComputedStyle, which could be incredibly slow depending on the depth of the tree...
getBoundingClientRect() may return incorrect values when transitioning. If you're not animating things but you are transforming things, then this may be the way to go, though I'm pretty sure it's notably less performant than getScreenCTM. Ideally, insert a very small element into the SVG such that its bounding rect will effectively be a point.
window.getComputedStyles().transform has the same issue as above.
Playing with innerWidth, screenX, clientX etc...
I'm not sure about what you are searching for, but as you question is arround screenX, screenY and SVG, I would let you play with snippet editor and some little tries.
Note that SVG bounding box is fixed to [0, 0, 500, 200] and show with width="100%" height="100%".
The last line of tspan with print x and y of pointer when circle is clicked.
function test(e) {
var sctm=new DOMMatrix();
var invs=new DOMMatrix();
sctm=e.target.getScreenCTM();
invs=sctm.inverse();
document.getElementById("txt1").innerHTML=
sctm.a+", "+sctm.b+", "+sctm.c+", "+sctm.d+", "+sctm.e+", "+sctm.f;
document.getElementById("txt2").innerHTML=
invs.a+", "+invs.b+", "+invs.c+", "+invs.d+", "+invs.e+", "+invs.f;
document.getElementById("txt3").innerHTML=
e.screenX+", "+e.screenY+", "+e.clientX+", "+e.clientY;
var vbox=document.getElementById("svg").getAttribute('viewBox').split(" ");
var sx=1.0*innerWidth/(1.0*vbox[2]-1.0*vbox[0]);
var sy=1.0*innerHeight/(1.0*vbox[3]-1.0*vbox[0]);
var scale;
if (sy>sx) scale=sx;else scale= sy;
document.getElementById("txt4").innerHTML=
e.clientX/scale+", "+e.clientY/scale;
}
<svg id="svg" viewBox="0 0 500 200" width="100%" height="100%" >
<circle cx="25" cy="25" r="15" onclick="javascript:test(evt);" />
<text>
<tspan x="10" y="60" id="txt1">test</tspan>
<tspan x="10" y="90" id="txt2">test</tspan>
<tspan x="10" y="120" id="txt3">test</tspan>
<tspan x="10" y="150" id="txt4">test</tspan>
</text>
</svg>
I am trying to figure out how to do the same zooming behavior as shown in the example below, but with a normal polygon instead of the geo paths.
https://bl.ocks.org/mbostock/4699541
I have seen some answers here on SO that kind of address this, but the animation is choppy or jumps around strangely.
The html I have is
<div id="map-container">
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
id="canvas"
viewBox="0 0 4328 2880">
<defs>
<pattern id="mapPattern"
patternUnits="userSpaceOnUse"
x="0"
y="0"
width="4328"
height="2880">
<image xlink:href="/development/data/masterplan.png"
x="0"
y="0"
width="4328"
height="2880"></image>
</pattern>
</defs>
<g id="masterGroup">
<rect fill="url(#mapPattern)"
x="0"
y="0"
width="4328"
height="2880" />
</g>
</svg>
I would like to be able to add some polygons in the same group as the map rectangle and then zoom on the polygon's boundary. Can anyone please show me a fiddle of such behaviour?
I should also add that I do not want to use the scroll wheel or panning. Just zooming in on a clicked polygon and then zooming out on another click.
Maybe this will help you. I answered a question here earlier today : D3js outer limits
Here is the fiddle I put together : http://jsfiddle.net/thatOneGuy/JnNwu/921/
I have added a transition : svg.transition().duration(1000).attr('transform',function(d){
Notice if you click one of the nodes the area moves to cater for the size of the new layout.
The basics are explained in the link to the question, but basically I got the bounding box and translated the SVG accordingly. So I translated and scaled to the size of the new rectangle.
Take a look, quite easy to understand. Here is the main part of the transition :
svg.transition().duration(1000).attr('transform',function(d){
var testScale = Math.max(rectAttr[0].width,rectAttr[0].height)
var widthScale = width/testScale
var heightScale = height/testScale
var scale = Math.max(widthScale,heightScale);
var transX = -(parseInt(d3.select('#invisRect').attr("x")) + parseInt(d3.select('#invisRect').attr("width"))/2) *scale + width/2;
var transY = -(parseInt(d3.select('#invisRect').attr("y")) + parseInt(d3.select('#invisRect').attr("height"))/2) *scale + height/2;
return 'translate(' + transX + ','+ transY + ')scale('+scale+')' ;
})
So with your code, your rectAttr values as seen in the snippet above would be the values retrieved from the getBoundingClientRect() of your polygon : x, y, width and height.
Where I have used d3.select('#invisRect'), this should be your boundingBoxRect() also. And the rest should just work.
EDIT
Here are the edits I made with the fiddle provided : http://jsfiddle.net/thatOneGuy/nzt39dym/3/
I used this function to get the bounding box of the polygon and set the rectangles values accordingly :
var bbox = d3.select(polygon).node().getBBox();
var rectAttr = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};
I am using the VivaGraph.js library to render a graph in SVG. I am trying to display an image cropped to a circle, for which I am using a clipPath element - as recommended in this post.
However, when I create a new SVG element of type that has a capital letter in it, e.g. clipPath in my case, the element that is inserted into the DOM is lowercase, i.e. clippath, even though the string I pass in to the constructor is camelCase. Since SVG is case sensitive, this element is ignored. Everything else seems to be okay.
I also tried to change the order in which I append the child elements, in hopes of changing the 'z-index', but it didn't have an impact on this.
I am using the following code inside of the function that creates the visual representation of the node in the graph (the 'addNode' callback) to create the node:
var clipPhotoId = 'clipPhoto';
var clipPath = Viva.Graph.svg('clipPath').attr('id', clipPhotoId);
var ui = Viva.Graph.svg('g');
var photo = Viva.Graph.svg('image').attr('width', 20).attr('height', 20).link(url).attr('clip-path', 'url(#' + clipPhotoId + ')');
var photoShape = Viva.Graph.svg('circle').attr('r', 10).attr('cx', 10).attr('cy', 10);
clipPath.append(photoShape);
ui.append(clipPath);
ui.append(photo);
return ui;
Thank you!
There is a bit of tweaking needed on top of the post you provided.
General idea to solve your issue is this one:
We create a VivaGraph svg graphics (which will create an svg element in the dom)
Into this svg graphic we create only once a clip path with relative coordinates
When we create a node we refer to the clip path
Code is:
var graph = Viva.Graph.graph();
graph.addNode('a', { img : 'a.jpg' });
graph.addNode('b', { img : 'b.jpg' });
graph.addLink('a', 'b');
var graphics = Viva.Graph.View.svgGraphics();
// Create the clipPath node
var clipPath = Viva.Graph.svg('clipPath').attr('id', 'clipCircle').attr('clipPathUnits', 'objectBoundingBox');
var circle = Viva.Graph.svg('circle').attr('r', .5).attr('cx', .5).attr('cy', .5);
clipPath.appendChild(circle);
// Add the clipPath to the svg root
graphics.getSvgRoot().appendChild(clipPath);
graphics.node(function(node) {
return Viva.Graph.svg('image')
.attr('width', 30)
.attr('height', 30)
// I refer to the same clip path for each node
.attr('clip-path', 'url(#clipCircle)')
.link(node.data.img);
})
.placeNode(function(nodeUI, pos){
nodeUI.attr('x', pos.x - 15).attr('y', pos.y - 15);
});
var renderer = Viva.Graph.View.renderer(graph, { graphics : graphics });
renderer.run();
The result in the dom will be like this:
<svg>
<g buffered-rendering="dynamic" transform="matrix(1, 0, 0,1,720,230.5)">
<line stroke="#999" x1="-77.49251279562495" y1="-44.795726056131116" x2="6.447213894549255" y2="-56.29464520347651"></line>
<image width="30" height="30" clip-path="url(#clipCircle)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="a.jpg" x="-92.49251279562495" y="-59.795726056131116"></image>
<image width="30" height="30" clip-path="url(#clipCircle)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="b.jpg" x="-8.552786105450746" y="-71.2946452034765"></image>
</g>
<clipPath id="clipCircle" clipPathUnits="objectBoundingBox">
<circle r="0.5" cx="0.5" cy="0.5"></circle>
</clipPath>
</svg>
Notice the clipPathUnits="objectBoundingBox", since it's the main trick for this solution.
I want to rotate a polygon on mouseover, but all I am able to do is a single rotate, I think because the angle is sort of static.
also, the polygon should rotate around itself, what it doesn't. My code looks like:
<polygon id="stern"
points="
350,370.5
370.9,460.1
460.9,460.1
390.7,510.5
420.3,600.1
350 ,550
270.7,600.1
300.3,510.5
230.1,460.1
320.1,460.1"
style="fill:#FACC2E"
onmouseover="rotieren()"/>
<use x="365" y="-380" xlink:href="#stern" transform="scale(0.7)"/>
<use x="1060" y="400" xlink:href="#stern" transform="scale(0.4)"/>
<use x="500" y="700" xlink:href="#stern" transform="scale(0.5)"/>
I tried many functions, for example to use a variable, but I can't use it in .setAttribute("transform", "rotate(variable,0,0)). Right now, I do
document.getElementByID("stern").setAttribut("transform","rotate(5,1060,400)")
Can you do something like this?
var stern = document.getElementByID("stern");
var i = 0;
var interval = setInterval(function(){
stern.setAttribute("transform","rotate("+(++i)+",1060,400)")
},50)
I'm developing a map, in Javascript using SVG to draw the lines.
I would like to add a feature where you can search for a road, and if the road is found, a circle appears on the map.
I know i can draw a circle in SVG, but my problem is that, the size of the circle should not change depending on the zoom-level. In other words the circle must have the same size at all times.
The roads on my map have this feature, all i had to do was add
vector-effect="non-scaling-stroke"
to the line attributes..
A line looks like this.
<line vector-effect="non-scaling-stroke" stroke-width="3" id = 'line1' x1 = '0' y1 = '0' x2 = '0' y2 = '0' style = 'stroke:rgb(255,215,0);'/>
The circle looks like this.
<circle id = "pointCircle" cx="0" cy="0" r="10" stroke="red" stroke-width="1" fill = "red"/>
Is it possible to define the circle as "non-scaling" somehow?
It took me a while, but I finally got the math clean. This solution requires three things:
Include this script in your page (along with the SVGPan.js script), e.g.
<script xlink:href="SVGPanUnscale.js"></script>
Identify the items you want not to scale (e.g. place them in a group with a special class or ID, or put a particular class on each element) and then tell the script how to find those items, e.g.
unscaleEach("g.non-scaling > *, circle.non-scaling");
Use transform="translate(…,…)" to place each element on the diagram, not cx="…" cy="…".
With just those steps, zooming and panning using SVGPan will not affect the scale (or rotation, or skew) of marked elements.
Demo: http://phrogz.net/svg/scale-independent-elements.svg
Library
// Copyright 2012 © Gavin Kistner, !#phrogz.net
// License: http://phrogz.net/JS/_ReuseLicense.txt
// Undo the scaling to selected elements inside an SVGPan viewport
function unscaleEach(selector){
if (!selector) selector = "g.non-scaling > *";
window.addEventListener('mousewheel', unzoom, false);
window.addEventListener('DOMMouseScroll', unzoom, false);
function unzoom(evt){
// getRoot is a global function exposed by SVGPan
var r = getRoot(evt.target.ownerDocument);
[].forEach.call(r.querySelectorAll(selector), unscale);
}
}
// Counteract all transforms applied above an element.
// Apply a translation to the element to have it remain at a local position
function unscale(el){
var svg = el.ownerSVGElement;
var xf = el.scaleIndependentXForm;
if (!xf){
// Keep a single transform matrix in the stack for fighting transformations
// Be sure to apply this transform after existing transforms (translate)
xf = el.scaleIndependentXForm = svg.createSVGTransform();
el.transform.baseVal.appendItem(xf);
}
var m = svg.getTransformToElement(el.parentNode);
m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
xf.setMatrix(m);
}
Demo Code
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Scale-Independent Elements</title>
<style>
polyline { fill:none; stroke:#000; vector-effect:non-scaling-stroke; }
circle, polygon { fill:#ff9; stroke:#f00; opacity:0.5 }
</style>
<g id="viewport" transform="translate(500,300)">
<polyline points="-100,-50 50,75 100,50" />
<g class="non-scaling">
<circle transform="translate(-100,-50)" r="10" />
<polygon transform="translate(100,50)" points="0,-10 10,0 0,10 -10,0" />
</g>
<circle class="non-scaling" transform="translate(50,75)" r="10" />
</g>
<script xlink:href="SVGPan.js"></script>
<script xlink:href="SVGPanUnscale.js"></script>
<script>
unscaleEach("g.non-scaling > *, circle.non-scaling");
</script>
</svg>
If you are looking for a fully static way of doing this, you might be able to combine non-scaling-stroke with markers to get this, since the markers can be relative to the stroke-width.
In other words, you could wrap the circles in a <marker> element and then use those markers where you need them.
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000">
<marker id="Triangle"
viewBox="0 0 10 10" refX="0" refY="5"
markerUnits="strokeWidth"
markerWidth="4" markerHeight="3"
orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<path d="M 100 100 l 200 0" vector-effect="non-scaling-stroke"
fill="none" stroke="black" stroke-width="10"
marker-end="url(#Triangle)" />
<path d="M 100 200 l 200 0"
fill="none" stroke="black" stroke-width="10"
marker-end="url(#Triangle)" />
</svg>
The same can also be viewed and tweaked here. The svg spec isn't fully explicit about what should happen in this case (since markers are not in SVG Tiny 1.2, and vector-effect isn't in SVG 1.1). My current line of thinking was that it should probably affect the size of the marker, but it seems no viewers do that at the moment (try in a viewer that supports vector-effect, e.g Opera or Chrome).
Looks like some work was done in webkit (maybe related to this bug: 320635) and the new transform doesn't stick around when simply appended like that
transform.baseVal.appendItem
This seems to work better. Even works in IE 10.
EDIT: Fixed the code for more general case of multiple translate transformations in the front and possible other transformations after. First matrix transformation after all translates must be reserved for unscale though.
translate(1718.07 839.711) translate(0 0) matrix(0.287175 0 0 0.287175 0 0) rotate(45 100 100)
function unscale()
{
var xf = this.ownerSVGElement.createSVGTransform();
var m = this.ownerSVGElement.getTransformToElement(this.parentNode);
m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
xf.setMatrix(m);
// Keep a single transform matrix in the stack for fighting transformations
// Be sure to apply this transform after existing transforms (translate)
var SVG_TRANSFORM_MATRIX = 1;
var SVG_TRANSFORM_TRANSLATE = 2;
var baseVal = this.transform.baseVal;
if(baseVal.numberOfItems == 0)
baseVal.appendItem(xf);
else
{
for(var i = 0; i < baseVal.numberOfItems; ++i)
{
if(baseVal.getItem(i).type == SVG_TRANSFORM_TRANSLATE && i == baseVal.numberOfItems - 1)
{
baseVal.appendItem(xf);
}
if(baseVal.getItem(i).type != SVG_TRANSFORM_TRANSLATE)
{
if(baseVal.getItem(i).type == SVG_TRANSFORM_MATRIX)
baseVal.replaceItem(xf, i);
else
baseVal.insertItemBefore(xf, i);
break;
}
}
}
}
EDIT2:
Chrome killed getTransformToElement for some reason, so the matrix needs to be retrieved manually:
var m = this.parentNode.getScreenCTM().inverse().multiply(this.ownerSVGElement.getScreenCTM());
It's discussed here and here
It looks like current browsers don't do the expected thing, so one needs to apply the inverse transform of the zoom (scale) on the contents of the <marker>, eg. transorm: scaleX(5) on the user of the <marker> etc. will need to be accompanied by a transform: translate(...) scaleX(0.2) inside the <pattern>, also factoring in possible x/y/width/height/transform-origin values inside the pattern if needed