zooming an image results in issues with pageX/pageY to SVG conversions - javascript

I am trying to move a SVG circle that sits in the center of an HTML image. If you mouse down on the image and drag the circle it works great.
However, if I zoom the image (tap on + button in the codepen), pageX and pageY to SVG co-ordinate translation messes up.
How should I be handling this correctly ? (My complete code handles both touch and mouse events for SVG - I've simplified it to just mouse for this example)
My codepen: http://codepen.io/pliablepixels/pen/EZxyRN
Here is how I am getting co-ordinates (please see codepen for a runnable example):
// map to SVG view so I can move the circle
function recompute(ax,ay)
{
// alert ("Here");
var svg=document.getElementById('zsvg');
var pt = svg.createSVGPoint();
pt.x = ax;
pt.y = ay;
var svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
$scope.cx = Math.round(svgP.x);
$scope.cy = Math.round(svgP.y);
}
function moveit(event)
{
if (!isDrag) return;
var status = "Dragging with X="+event.pageX+" Y="+event.pageY;
$timeout (function(){$scope.status = status; recompute(event.pageX, event.pageY)});
}
The relevant SVG:
<svg id = "zsvg" class="zonelayer" viewBox="0 0 400 200" width="400" height="300" >
<circle id="c1" ng-attr-cx="{{cx}}" ng-attr-cy="{{cy}}" r="20" stroke="blue" fill="purple" />
</svg>

Firstly, you would normally use clientX and clientY rather than pageX and pageY.
Secondly, the Ionic(?) zoomTo() function you are using is applying a 3D transform to the container div. Ie.
style="transform: translate3d(-791.5px, -173px, 0px) scale(2);"
I don't expect getScreenCTM() handles 3D transforms. Even ones that are effectively 2D because they do nothing in the Z axis.
You'll need to either:
do the zoom a different way. IOW handle it yourself so you can do it in a getScreenCTM()-friendly way. Or multiply the zoom factor in directly. or
find a way of getting the details of the transform that Ionic has applied and updating your transformed mouse coords appropriately.
Update:
The definition of getScreenCTM() has changed in SVG 2. In SVG 1.1 it was fairly loosely defined. In SVG 2 the definition has been updated to explicitly define how the result is calculated. Although it does not specify how 3D transforms on ancestor elements should be handled.
I have experimented a little on Chrome and Firefox. It appears that Chrome has implemented the SVG2 definition, but it has a bug. It is not returning the correct transform matrix. However Firefox has not yet been updated. It is not including any transforms on ancestor elements.
I now believe the bug in Chrome is the reason your sample is not working there. However if you want to be cross-browser, my advice to handle zoom yourself - and adjust the transform (or coords) - still holds.

Related

Framer motion and transform matrix

I am making app with framer motion and I need to drag svg inside another svg but my problem is that viewbox size is not equal to window size so when I drag element my 1px movement of mouse on the screen is like 100+ px. I know in JavaScript we can calculate x and y with screenX, sceenY and CTM (current transform matrix). Is possible to make somehow framer motion drag function to calculate that?
<svg viewBox="0 0 40 20">
<motion.circle drag cx="5" cy="5" r="0.5"strokeWidth="0.1"/>
</svg>
P.S. I cannot change viewbox size and its 100% of width and height of screen and probably we need to transform current matrix with useTransform or useMotionValue hooks from framer motion but I am not sure how to do it.
this is current state of the app where you can see the problem when you try to drag player object. https://waterpolo.klaktech.com
I was having issues understanding properly the question, but I think this is what you were looking for.
There's a couple of catches:
I am capturing the with and height of each of the boxes to know if they are overlapping at the first useEffect.
The problem is that when the items change position the REF isn't updated. which means that we need and onDrag hook to update the X and Y positions of the boxes.
The second catch is that the X and Y from there are exposed from the mouse, which means that it's not the one of the origin of the object, so the overlap might not happen till the mouse actually enters the other object
I set this up so you can have a look at the code
https://codesandbox.io/s/paths-forked-0iqtdj?file=/src/Example.tsx

zoom in on a particular area in a line chart (using d3 and svelte)

I am trying to zoom in on parts of a line chart using viewBox. I managed to get it working, however, not sure how to deal with the fact that everything scales accordingly (line path also becomes larger).
What I am looking for is for the line to stay constant while zoom is happening. I researched that there is also a way with translation and scale, but not sure how to implement it.
Also, not sure how to make this behavior more automatic. For example, I am zooming in on a range between two years: 2010 and 2011. Currently, I specify the y value manually. But ideally, this would be specified in using yScale function.
Any help would be appriciated!
Here is my repl: https://svelte.dev/repl/be0df7e353334108b3d432d5c82a6cd3?version=3.38.2
One way to accomplish this is by scaling the viewBox of the <svg> element.
You can do this by binding the x, y, width and height of the viewBox.
<svg ... viewBox="{x} {y} {width} {height}">
You can also tween the viewbox changes:
By using Svelte's tweened() store for x, y, width, height. ie <svg viewBox="{$x} {$y} {$width} {$height}">
Or by using SVG's <animate> element. ie <animate attributeName="viewBox" values="..." dur="2s"/>

Understanding coordinate scaling (and viewBox) in a PyQt/SVG/JavaScript project

I am working on a PyQt application that has an SVG viewer based on QWebKit. Initially the SVG file is generated by an external application (LilyPond), but we want to edit the SVG in the viewer using JavaScript.
Basically it's working pretty well, but we experience a strange rounding/conversion/rastering issue we don't have a clue about: When dragging an object, onmousemove outputs only rasterized values for clientX/clientY or pageX/pageY. That is we can see that the event fires a number of times, and (say) clientX produces a series of values like 100 100 100 100 101 101 101 101 102 102 102 102 etc., the number of repetition being dependent of the zoom factor of the view.
I assume these values are not screen pixels but numbers in a unit of the SVG document and this is the source of our issue.
The svg element of the file looks like this:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" width="210.00mm" height="297.00mm" viewBox="0 0 119.5016 169.0094">(remember, this is generated from the external program).
I assume that the viewBox attribute is used to ensure that SVG coordinates are consistent with the coordinate system of the originating application (LilyPond thinks in "staff spaces", and obviously a translation of 1 in the SVG file does exactly the same as translating by 1 staff space in LilyPond. If I change the "global staff size" in LilyPond the viewBox attribute will get different values.)
So what I would basically need is that clientX(etc.) gives me floating point results instead of rounded integers.
Is there a way to make JavaScript do that?
Or would I have to do some calculations - somehow translating clientX (etc.) coordinates to screen pixels and then back again (taking zooming and/or hardware into account)?
I'm at a loss when trying to understand the different scaling options at the different stages...
EDIT
Here's some more code...
As #uli_1973 mentioned, mouse position is sustained by the clientX/-Y property:
svgPoint.x = event.clientX;
svgPoint.y = event.clientY;
To get the mouse position translated to the coordinates of the SVG we use this line of code:
svgPoint = svgPoint.matrixTransform(svg.getScreenCTM().inverse());
I guess the main question is, could we get a higher resolution out of this somehow? Currently the smallest possible difference in mouse movement translates into 0.15 in SVG coordinates (mm).
EDIT2
The whole file can be seen at
https://github.com/PeterBjuhr/frescobaldi/blob/svg-edit/frescobaldi_app/svgview/editsvg.js
The implementation of the dragging can be found in the three event handlers at the bottom of the file.
Initially (onmousedown) the constructor function DraggableObject(elem, event) (currently starting at line 219) is called. onmousemove the event object is passed to the updatePositions(e) method in line 319.
Both use the function mousePos() at the top of the file. And this function is where we pinpoint the problem.
When we call event.clientX or any of screenX or pageX we get output of the wrong resolution.
A more detailed discussion of all our observations and thoughts may be found at https://github.com/PeterBjuhr/frescobaldi/issues/21

How to make a svg element (e.g. rectangle) scrollIntoView?

I have a svg in graph panel. All nodes in the svg are listed in another panel. I hope that by clicking the node in node list, svg can scroll to that node. Each node is a rectangle. But I found that only the upper border is in view, while the rest part of the node are still out of the view. Is there any way to fix this problem? (either Javascript or Extjs)
This is my code:
function selectRectangle(Id){
var ele = Ext.get(Id);
ele.scrollIntoView(Ext.get('graph-panel-body'), true);}
By whatever reason scrollIntoView seems not to work for SVG elements. This is what I do
suppose the svg is in a
<div id="container">
<svg ...>
...
<path id> ...</path>
</svg>
</div>
then suppose in the variable 'element' you have the element you want to scrollIntoView
var bbox = ele.getBBox()
var top = bbox.y + bbox.y2
top = 50 * Math.floor(top/50)
$("#container").get(0).scrollTop=top
I am not sure, but I observe that getBBox is pretty slow. So take care.
The problem is that the SVG element you are trying to scroll the view to probably has a y or dy attribute which offsets it from the top, and the scrollIntoView method in Chrome doesn't take that into account (in Firefox it does), therefor it will scroll to the top, because it thinks the element is there, because this is how SVG elements positioning works.. elements are all rendered from the very top left and then kind of "transformed" to their positions via x, y, dx, dy attributes.
There is an open bug which you can (and should) star:
https://bugs.chromium.org/p/chromium/issues/detail?id=803440

drawImage on canvas has weird aspect ratio in firefox and other problems

I am running firefox 3.5.6.
I want to display an image on a canvas and draw a couple lines on it. It needs to display properly in firefox and internet explorer (using excanvas).
Here is what I am getting:
The top image is what I see in IE8, the bottom is what I see in firefox.
IE seems to be a bit messed up as far as the canvas is the wrong size but firefox is going crazy! What gives with this aspect ratio? Why does the second half of my arc not appear?
Also, some times firefox just flat out doesn't show anything.
Here is my code by the way.
Aspect ratio problem
If you don't set a width on the canvas element, it defaults to 300x150. In your CSS, you set the style to 94x120, so it scales the image to that size. To fix it, you need to either set the width and height in the HTML, or with JavaScript.
In HTML:
<canvas id="c" width="94" height="120">Ugh, this just ain't gonna work</canvas>
In JavaScript (with jQuery):
$('canvas').attr('width', '94').attr('height', '120');
Internet Explorer's incorrect size
Adding the size to the canvas element should fix this problem too. Since IE is using VML instead of a canvas to render the image, the CSS rule for canvas won't apply. excanvas should see the specified size and apply it in IE.
Missing the second half of the arc
The simpleArc function doesn't work in Firefox when the amplitude is negative. The problem is that a negative amplitude results in a negative radius for the arc, which is illegal according to the canvas spec. It should actually throw an INDEX_SIZE_ERR exception, but Firefox just seems to ignore the call.
There are two possible solutions (basically; there are several ways you could accomplish either): when you pass a negative amplitude, either calculate the parameters for the arc taking into account the negative radius (with a different center point and angles, etc.), or change the sign and use transformations to rotate the arc. I implemented the second solution like this:
ctx.simpleArc = function(x,y, length, amplitude) {
var rotate = false;
// Check whether we need to rotate the image
if (amplitude < 0) {
rotate = true;
amplitude = -amplitude;
}
var radius = amplitude/2+ length*length/(8*amplitude);
var outerAngle = Math.asin((radius-amplitude)/radius);
var innerAngle = Math.PI - 2*outerAngle;
// The translate/rotate/translate steps could be combined into one matrix
// transformation, but I think this is clearer and less error-prone.
if (rotate) {
this.save(); // So we can easily undo the transformation
this.translate(x + length, y);
this.rotate(Math.PI);
this.translate(-length, -y);
}
this.arc(x+length/2, y+(radius-amplitude), radius, -(outerAngle+innerAngle), -outerAngle, false);
// Reset the transformation matrix to its original value
if (rotate) {
this.restore();
}
return this;
}
Firefox not showing anything
In your code, you create the image and set the source, but it may not be loaded before the rest of the code get's executed. The image loads asynchronously, and when you draw the image onto the canvas, it doesn't wait for it to finish. You will need to call the code that uses the image from an onload event.
var img = $('<img>');
img[0].onload = function() {
ctx.drawImage(img[0], 0, 0);
ctx.strokeStyle = "blue";
ctx.simpleStroke(function(ctx) { ctx.simpleArc(0, 70, img_w/2, 3)});
ctx.simpleStroke(function(ctx) { ctx.simpleArc(img_w / 2, 70, img_w/2, -3)});
};
// I moved this so it happens after you set the `onload` event, because I
// think IE won't call `onload` if it happens to already be loaded.
img.attr('src', 'shortcylinder.png');
You could also pre-load all the images you will need instead of creating them when you need them. You would still need to prevent the code from running until all the images are loaded.
I've recently noticed that using style to define width & height for canvas elements caused an issue like this. Taking from an earlier example
This works in FF 9.0.1 Mac
<canvas id="c" width="94" height="120">Ugh, this just ain't gonna work</canvas>
vs.
This had similar display issues as your example, in FF 9.0.1 Mac
<canvas id="c" style="width:94;height:120;">Ugh, this just ain't gonna work</canvas>
Maybe that's it?

Categories