Snap SVG scale and centre loaded SVG file - javascript

I have trouble with scaling and centring SVG loaded from file.
It's #svg container to which I load SVG files with Snapsvg:
It's loaded SVG:
And when I'm scaling it:
How browser inspecting it:
My JS:
var s = Snap("#svg");
var g = s.group();
var tux = Snap.load("svg/roulette.svg", function ( loadedFragment ) {
g.append( loadedFragment );
var firstScene = new Snap.Matrix();
firstScene.scale(1.5);
g.animate({ transform: firstScene }, 0);
});
How can I scale my roulette and centre it in svg#svg element?
Working example:
http://plnkr.co/edit/DE1dds8n3ULOLQRATnLY?p=preview

It may depend if you need to scale it and calculate where it should go, or if a responsive type solution could work. I would explore this first, and go for a calculated option otherwise.
As you haven't shown a running example, its hard to be sure though. I would post up a jsbin with the file getting loaded so others could play if the following doesn't work.
In the meantime, you could try something like this inside the load function...it may not work though depending on what Layer_1 and other svg parents though, and setting the viewBox to match the inner SVG.
s.select('#wheel'); // or whatever ID it has, or give it one
.attr({ width: '100%', height: '100%', viewBox: "0 0 600 600" });
As an aside, you don't need to worry about Matrices, just use the Snap transform strings, so you don't need that code...eg
g.animate({ transform: 's1.5,1.5' }, 1000)
is all you would need to animate scale by 1.5

Related

Resizing a canvas element via dragging the edges

I'm trying to make it so users of my site can resize their canvas by simply dragging the sides of it. I'm using Fabric.js which allows me to resize elements from within the canvas but I need to resize the actual canvas itself. I'm fine with using any new libraries you recommend.
This image should help you understand a bit more of what I want.
As a side note, the Fabric.js team have an interactive toolbox here for you to experiment if you need it.
Put your fabric.js canvas in a wrapper div.
Make the wrapper resizable. Here I'm using CSS resize. It only adds a bottom-left corner as a resize control but it's good enough for the sake of the demo. To have all the edges as controls you can try something like this.
Detect the wrapper's size change. Ideally, you would use something like ResizeObserver. However, since browser support is still about 80% at the time of posting this, you might feel the need to use a polyfill or write something specific to your case. A simple setInterval with size check might prove to be sufficient.
When the wrapper's size changes, set new fabric.js canvas dimensions via setWidth() and setHeight().
const canvas = new fabric.Canvas('c')
canvas.add(new fabric.Rect({
width: 100,
height: 100,
fill: 'red',
left: 100,
top: 50,
}))
const canvasWrapper = document.getElementById('wrapper')
// initial dimensions
canvasWrapper.style.width = '300px'
canvasWrapper.style.height = '150px'
let width
let height
setInterval(() => {
const newWidth = canvasWrapper.clientWidth
const newHeight = canvasWrapper.clientHeight
if (newWidth !== width || newHeight !== height) {
width = newWidth
height = newHeight
canvas.setWidth(newWidth)
canvas.setHeight(newHeight)
}
}, 100)
#wrapper {
border: solid 1px black;
resize: both;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<div id="wrapper">
<canvas id="c"></canvas>
</div>
This isn't something that's going to be easy, but it's definitely possible. First, you may wonder about the native browser element resize CSS property and ResizeObserver, however those have poor support right now. Your best option is to:
Create a canvas element
Detect mousemoves on the canvas:
If the mousemove happens around the edge of the canvas, set the cursor to be the resize icon in the proper direction
If the mouse is currently down, resize the canvas based on the mouse's position.
Remember that every time you resize a canvas, the current image on the canvas is wiped.

How to use preserveAspectRatio on SVG <symbol> using SVG.JS

I recently found out about using 'preserveAspectRatio' on symbols in SVGs, which I will be honest was a gamechanger for they type of designs I have been trying to accomplish. However, I can't seem to get it to work using SVG.JS.
Link to JS Fiddle
/* canvas created */
var draw = SVG('maindiv').viewbox('0 0 500 500').attr({ 'preserveAspectRatio': 'none' })
/* polgon created */
var polyg = draw.polygon('0,150 500,150 500,500 0,500').fill('black')
/* symbol created and inserted, with functioning viewbox but preserveAspect ratio not working */
var txt = draw.symbol().viewbox('0 0 500 500').attr({ 'preserveAspectRatio': 'xMinYMin meet' })
txt.text('Some Text').font({family: 'Arial',weight: 'bold',size: 30,fill: '#f06'})
var title = draw.use(txt)
/* clickable animation */
polyg.click(function() {
this.animate().attr({'points':'0,350 500,350 500,500 0,500'})
})
If you take a look at that code in the link, the black polygon correctly extends the entire viewport, but the text which is in fact a symbol, should NOT scale with the viewport. Go ahead and resize your window and you will see that the text resizes itself.
Any ideas? There's a lack of documentation on how to do this using SVG.JS so I'm sure this will help a few people out there who are trying to acheive this same challenge of having multiple elements in the same SVG canvas, some responsive to the viewport and some static.

retrieving the aspect ratio of an svg <image>

TLDR;
given this svg element:
<image width="30" height="48" x="3.75" y="6" href="http://some/image.jpg">
How can I retrieve the image's actual height and width (seeing as it is defined in part by the image's aspect ratio).
I have a d3js script that draws a bunch of <rect>s and a bunch of <image>s.
Now stuff is laid out so that the images fall inside the rects, as though the rects were borders. There is other stuff inside the rects too.
Now each of these images has it's own unique and special aspect ratio and this bothers me because it means each of the rects then has a different amount of blank space. This is untidy.
To avoid this I want to load the images then get the actual image dimensions and then adjust the positions and sizes of the surrounding goodies. The crux of the matter is getting the actual image sizes. How can I do this?
I've googled around a bit and spent some quality time with the debugging console to no avail. Just nothing comes up. I'll keep hunting but an answer would be really nice.
First, set the width attribute only, keep height unspecified.
Then, call the getBBox for the image element.
Note that image box is available after it's properly rendered by the SVG
const image = parent.append('image').attr('xlink:href', url).attr('width', 100);
setTimeout(() => {
const box = image.node().getBBox();
const ratio = box.width / box.height;
}, 0);
This is the best I can come up with. I would be surprised and horrified if there isn't an easier way. Anyway:
add a normal html <img> element with a suitable src.
use js to fetch the image height and width
remove the extra html
Ugly but it works...
Here's some code:
var oImage = document.createElement("img");
oImage.setAttribute("src",sUrl);
document.body.appendChild(oImage);
var iWidth = oImage.width;
var iHeight = oImage.height;
oImage.remove();

What to do with SVG so it can be resized using Javascript?

Let's say I loaded SVG, displayed it in browser, and so far it is OK.
Now, I would like to resize it. All methods I found googling failed to give me the desired effect.
It is all variations of:
$svg.removeAttr('width')
.removeAttr('height')
//.attr('preserveAspectRatio','xMinYMin meet')
//.css('width',width+'px')
//.css('height',height+'px')
.css('width','100%')
.css('height','100%')
.attr('viewBox','0 0 '+width+' '+height)
;
Nothing. I get the view of desired size, but the image (SVG) is clipped to that view instead of being resized. Setting size via attributes does not change a thing. Like the size of that SVG is carved in stone.
Thanks to frenchie answer (see below) it appears JS tries hard to resize SVG and for basic SVG it just works. So the real question is -- what to do with SVG (real-world SVG, not just a rectangle) so Javascript would be able to resize it?
SVG I am testing: http://jsfiddle.net/D6599/
I created SVG with Inkscape, and this is how I load SVG in JS:
$.get(svg_url, function(data) {
// Get the SVG tag, ignore the rest
svg = $(data).find('svg')
.attr('id', 'SVG')
// Remove any invalid XML tags as per http://validator.w3.org
.removeAttr('xmlns:a')
[0];
on_load();
}, 'xml');
The code comes from How to change color of SVG image using CSS (jQuery SVG image replacement)?. What I have later on is svg element (I wrapped it using jQuery).
This is not the question how to load SVG, or what to do with cross-domain issue. SVG loads fine, it is the same domain as the script (my disk).
Your SVG markup should look like this:
<svg id="SomeID" height="20" width="20">......</svg>
So all you need to do is reset the css properties. Something like this should work:
$('#SomeID').css({'width':40, 'height' :40});
And if you can't change the id of the SVG markup then you can simply wrap it around a div like this:
<div id="SomeID"><svg>....</svg></div>
$('#SomeID').find('svg').css({'width': 40, 'height': 40});
Here's a jsFiddle:
function Start() {
$('#TheButton').click(ResizeSVG);
};
function ResizeSVG() {
$('#TheSVG').css({'width':40, 'height' :40});
}
$(Start);
Finally I found it. I hope it will change in future, but for now there is a crucial difference if you change attribute by editing actual SVG file, and changing SVG on-fly using Javascript.
In order JS could resize SVG you have to manually edit SVG file and change height and width attributes to following lines.
Before:
width="600px"
height="560px"
After:
viewBox="0 0 600 560"
width="100%"
height="100%"
Of course in your case the values will be different. Please note that you can change those attributes programmatically with Javascript but it does not work (at least not for me).
Now, you can load such file as usual and compute, let's say width/height ratio.
var vbox = $svg.attr('viewBox').split(' ');
var w_ratio = vbox[2]/vbox[3]; // width/height
Such fixed file is still readable by Inkscape, I don't know how about other programs.

How to get the MouseEvent coordinates for an element that has CSS3 Transform?

I want to detect where a MouseEvent has occurred, in coordinates relative to the clicked element. Why? Because I want to add an absolutely positioned child element at the clicked location.
I know how to detect it when no CSS3 transformations exist (see description below). However, when I add a CSS3 Transform, then my algorithm breaks, and I don't know how to fix it.
I'm not using any JavaScript library, and I want to understand how things work in plain JavaScript. So, please, don't answer with "just use jQuery".
By the way, I want a solution that works for all MouseEvents, not just "click". Not that it matters, because I believe all mouse events share the same properties, thus the same solution should work for all of them.
Background information
According to DOM Level 2 specification, a MouseEvent has few properties related to getting the event coordinates:
screenX and screenY return the screen coordinates (the origin is the top-left corner of user's monitor)
clientX and clientY return the coordinates relative the document viewport.
Thus, in order to find the position of the MouseEvent relative to the clicked element content, I must do this math:
ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
ev.clientX is the coordinate relative to the document viewport
this.getBoundingClientRect().left is the position of the element relative to the document viewport
this.clientLeft is the amount of border (and scrollbar) between the element boundary and the inner coordinates
this.scrollLeft is the amount of scrolling inside the element
getBoundingClientRect(), clientLeft and scrollLeft are specified at CSSOM View Module.
Experiment without CSS Transform (it works)
Confusing? Try the following piece of JavaScript and HTML. Upon clicking, a red dot should appear exactly where the click has happened. This version is "quite simple" and works as expected.
function click_handler(ev) {
var rect = this.getBoundingClientRect();
var left = ev.clientX - rect.left - this.clientLeft + this.scrollLeft;
var top = ev.clientY - rect.top - this.clientTop + this.scrollTop;
var dot = document.createElement('div');
dot.setAttribute('style', 'position:absolute; width: 2px; height: 2px; top: '+top+'px; left: '+left+'px; background: red;');
this.appendChild(dot);
}
document.getElementById("experiment").addEventListener('click', click_handler, false);
<div id="experiment" style="border: 5px inset #AAA; background: #CCC; height: 400px; position: relative; overflow: auto;">
<div style="width: 900px; height: 2px;"></div>
<div style="height: 900px; width: 2px;"></div>
</div>
Experiment adding a CSS Transform (it fails)
Now, try adding a CSS transform:
#experiment {
transform: scale(0.5);
-moz-transform: scale(0.5);
-o-transform: scale(0.5);
-webkit-transform: scale(0.5);
/* Note that this is a very simple transformation. */
/* Remember to also think about more complex ones, as described below. */
}
The algorithm doesn't know about the transformations, and thus calculates a wrong position. What's more, the results are different between Firefox 3.6 and Chrome 12. Opera 11.50 behaves just like Chrome.
In this example, the only transformation was scaling, so I could multiply the scaling factor to calculate the correct coordinate. However, if we think about arbitrary transformations (scale, rotate, skew, translate, matrix), and even nested transformations (a transformed element inside another transformed element), then we really need a better way to calculate the coordinates.
The behaviour you are experiencing is correct, and your algorithm isn't breaking. Firstly CSS3 Transforms are designed not to interfere with the box model.
To try and explain...
When you apply a CSS3 Transform on an element. the Element assumes a kind of relative positioning. In that the surrounding elements are not effected by the transformed element.
e.g. imagine three div's in a horizontal row. If you apply a scale transform to decrease the size of the centre div. The surrounding div's will not move inwards to occupy the space that was once occupied the transformed element.
example: http://jsfiddle.net/AshMokhberi/bWwkC/
So in the box model, the element does not actually change size. Only it's rendered size changes.
You also have to keep in mind that you are applying a scale Transform, so your elements "real" size is actually the same as it's original size. You are only changing it's perceived size.
To explain..
Imagine you create a div with a width of 1000px and scale it down to 1/2 the size. The internal size of the div is still 1000px, not 500px.
So the position of your dots are correct relative to the div's "real" size.
I modified your example to illustrate.
Instructions
Click the div and keep you mouse in the same position.
Find the dot in the wrong position.
Press Q, the div will become the correct size.
Move your mouse to find the dot in the correct position to where you clicked.
http://jsfiddle.net/AshMokhberi/EwQLX/
So in order to make the mouse clicks co-ordinates match the visible location on the div, you need to understand that the mouse is giving back co-ordinates based on the window, and your div offsets are also based on its "real" size.
As your object size is relative to the window the only solution is to scale the offset co-ordinates by the same scale value as your div.
However this can get tricky based on where you set the Transform-origin property of your div. As that is going to effect the offsets.
See here.
http://jsfiddle.net/AshMokhberi/KmDxj/
Hope this helps.
if element is container and positioned absolute or relative,
you can place inside of it element,
position it relative to parent and
width = 1px, height = 1px, and move to inside of container,
and after each move use document.elementFromPoint(event.clientX, event.clientY) =))))
You can use binary search to make it faster.
looks terrible, but it works
http://jsfiddle.net/3VT5N/3/ - demo
BY FAR the fastest. The accepted answer takes about 40-70 ms on my 3d transforms site, this usually takes less than 20 (fiddle):
function getOffset(event,elt){
var st=new Date().getTime();
var iterations=0;
//if we have webkit, then use webkitConvertPointFromPageToNode instead
if(webkitConvertPointFromPageToNode){
var webkitPoint=webkitConvertPointFromPageToNode(elt,new WebKitPoint(event.clientX,event.clientY));
//if it is off-element, return null
if(webkitPoint.x<0||webkitPoint.y<0)
return null;
return {
x: webkitPoint.x,
y: webkitPoint.y,
time: new Date().getTime()-st
}
}
//make full-size element on top of specified element
var cover=document.createElement('div');
//add styling
cover.style.cssText='height:100%;width:100%;opacity:0;position:absolute;z-index:5000;';
//and add it to the document
elt.appendChild(cover);
//make sure the event is in the element given
if(document.elementFromPoint(event.clientX,event.clientY)!==cover){
//remove the cover
cover.parentNode.removeChild(cover);
//we've got nothing to show, so return null
return null;
}
//array of all places for rects
var rectPlaces=['topleft','topcenter','topright','centerleft','centercenter','centerright','bottomleft','bottomcenter','bottomright'];
//function that adds 9 rects to element
function addChildren(elt){
iterations++;
//loop through all places for rects
rectPlaces.forEach(function(curRect){
//create the element for this rect
var curElt=document.createElement('div');
//add class and id
curElt.setAttribute('class','offsetrect');
curElt.setAttribute('id',curRect+'offset');
//add it to element
elt.appendChild(curElt);
});
//get the element form point and its styling
var eltFromPoint=document.elementFromPoint(event.clientX,event.clientY);
var eltFromPointStyle=getComputedStyle(eltFromPoint);
//Either return the element smaller than 1 pixel that the event was in, or recurse until we do find it, and return the result of the recursement
return Math.max(parseFloat(eltFromPointStyle.getPropertyValue('height')),parseFloat(eltFromPointStyle.getPropertyValue('width')))<=1?eltFromPoint:addChildren(eltFromPoint);
}
//this is the innermost element
var correctElt=addChildren(cover);
//find the element's top and left value by going through all of its parents and adding up the values, as top and left are relative to the parent but we want relative to teh wall
for(var curElt=correctElt,correctTop=0,correctLeft=0;curElt!==cover;curElt=curElt.parentNode){
//get the style for the current element
var curEltStyle=getComputedStyle(curElt);
//add the top and left for the current element to the total
correctTop+=parseFloat(curEltStyle.getPropertyValue('top'));
correctLeft+=parseFloat(curEltStyle.getPropertyValue('left'));
}
//remove all of the elements used for testing
cover.parentNode.removeChild(cover);
//the returned object
var returnObj={
x: correctLeft,
y: correctTop,
time: new Date().getTime()-st,
iterations: iterations
}
return returnObj;
}
and also include the following CSS in the same page:
.offsetrect{
position: absolute;
opacity: 0;
height: 33.333%;
width: 33.333%;
}
#topleftoffset{
top: 0;
left: 0;
}
#topcenteroffset{
top: 0;
left: 33.333%;
}
#toprightoffset{
top: 0;
left: 66.666%;
}
#centerleftoffset{
top: 33.333%;
left: 0;
}
#centercenteroffset{
top: 33.333%;
left: 33.333%;
}
#centerrightoffset{
top: 33.333%;
left: 66.666%;
}
#bottomleftoffset{
top: 66.666%;
left: 0;
}
#bottomcenteroffset{
top: 66.666%;
left: 33.333%;
}
#bottomrightoffset{
top: 66.666%;
left: 66.666%;
}
It essentially splits the element into 9 squares, determines which one the click was in via document.elementFromPoint. It then splits that into 9 smaller squares, etc until it is accurate to within a pixel. I know I over-commented it. The accepted answer is several times slower than this.
EDIT: It is now even faster, and if the user is in Chrome or Safari it will use a native function designed for this instead of the 9 sectors thingy and can do it consistently in LESS THAN 2 MILLISECONDS!
another way is place 3 divs in corners of that element,
than find transform matrix ... but is also works only for positioned containerable
elements – 4esn0k
demo: http://jsfiddle.net/dAwfF/3/
Also, for Webkit webkitConvertPointFromPageToNode method can be used:
var div = document.createElement('div'), scale, point;
div.style.cssText = 'position:absolute;left:-1000px;top:-1000px';
document.body.appendChild(div);
scale = webkitConvertPointFromNodeToPage(div, new WebKitPoint(0, 0));
div.parentNode.removeChild(div);
scale.x = -scale.x / 1000;
scale.y = -scale.y / 1000;
point = webkitConvertPointFromPageToNode(element, new WebKitPoint(event.pageX * scale.x, event.pageY * scale.y));
point.x = point.x / scale.x;
point.y = point.y / scale.x;
To get the coordinates of a MouseEvent relative to the clicked element, use offsetX / layerX.
Have you tried using ev.layerX or ev.offsetX?
var offsetX = (typeof ev.offsetX == "number") ? ev.offsetX : ev.layerX || 0;
See also:
CSSOM View Module: 9 Extensions to the MouseEvent Interface
IE 8 measures clientX from the element's padding edge instead of the content edge: GTalbot MSIE 8 Browser Bugs: event.offsetX, event.offsetY as mouse coordinates inside element target's padding-box
MSDN: offsetX Property
This seems to work really well for me
var elementNewXPosition = (event.offsetX != null) ? event.offsetX : event.originalEvent.layerX;
var elementNewYPosition = (event.offsetY != null) ? event.offsetY : event.originalEvent.layerY;
EDIT: my answer is untested, WIP, I will update when I get it working.
I'm implementing a polyfill of the geomtetry-interfaces. The DOMPoint.matrixTransform method I will make next, which means we should be able to write something like the following in order to map a click coordinate onto a transformed (possiblly nested) DOM element:
// target is the element nested somewhere inside the scene.
function multiply(target) {
let result = new DOMMatrix;
while (target && /* insert your criteria for knowing when you arrive at the root node of the 3D scene*/) {
const m = new DOMMatrix(target.style.transform)
result.preMultiplySelf(m) // see w3c DOMMatrix (geometry-interfaces)
target = target.parentNode
}
return result
}
// inside click handler
// traverse from nested node to root node and multiply on the way
const matrix = multiply(node)
const relativePoint = DOMPoint(clickX, clickY, 0, 800).matrixTransform(matrix)
relativePoint will be the point relative to the element's surface that you clicked on.
A w3c DOMMatrix can be constructed with a CSS transform string argument, which makes it super easy to use in JavaScript.
Unfortunately, this isn't working yet (only Firefox has a geometry-interfaces implementation, and my polyfill does not yet accept a CSS transform string). See: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18
I will update this once I implement that and have a working example. Pull requests welcome!
EDIT: the value 800 is the scene's perspective, I'm not sure if this is what the fourth value for the DOMPoint constructor should be when we intend to do something like this. Also, I'm not sure if I should use preMultiplySelf or postMultiplySelf. I'll find out once I get it at least working (values may be incorrect at first) and will update my answer.
I am working on a polyfill to transfrom DOM coordinates. The GeometryUtils api is not available yet (#see https://drafts.csswg.org/cssom-view/). I created a "simple" code in 2014 to transform coordinates, like localToGlobal, globalToLocal and localToLocal. Its not finished yet, but its working :) I think I will finish it in the coming months (05.07.2017), so if you still need a API to accomplish coordinate transformation give it a try: https://github.com/jsidea/jsidea jsidea core library. Its not stable yet (pre alpha).
You can use it like that:
Create your transform instance:
var transformer = jsidea.geom.Transform.create(yourElement);
The box model you want to transform to (default:"border", will be replaced by ENUM's later on):
var toBoxModel = "border";
The box model where your input coordinates coming from (default:"border"):
var fromBoxModel = "border";
Transform your global coordinates (here {x:50, y:100, z: 0}) to local space. The resulting point has 4 components: x, y, z and w.
var local = transformer.globalToLocal(50, 100, 0, toBoxModel, fromBoxModel);
I have implemented some other functions like localToGlobal and localToLocal.
If you want to give a try, just download the release build and use the jsidea.min.js.
Download the first release here: Download TypeScript code
Feel free to change the code, I never put it under any license :)
I have this issue and started trying to compute the matrix.
I started a library around it: https://github.com/ombr/referentiel
$('.referentiel').each ->
ref = new Referentiel(this)
$(this).on 'click', (e)->
input = [e.pageX, e.pageY]
p = ref.global_to_local(input)
$pointer = $('.pointer', this)
$pointer.css('left', p[0])
$pointer.css('top', p[1])
What do you think ?
Works fine whether relative or absolute :) simple solution
var p = $( '.divName' );
var position = p.position();
var left = (position.left / 0.5);
var top = (position.top / 0.5);

Categories

Resources