Centering on a specific point when zooming using SVG viewBox with arbitrary initial state - html

I have a SVG HTML element and I have implemented panning and zooming into it using the mouse. The current implementation of the zooming functionality just multiplies the original width and height of the element by a number that changes when the user scrolls the mouse.
This implementation preserves the origin (0,0) and all other points appear to move closer/further away from it depending on the direction of the zoom.
Intuitively and based o this question. I know, that If I want to zoom in/out on the point the mouse is currently pointing at, I have to pan the viewBox.
I have already looked at the linked questio, as well as two otheres, but I was unable to successfully apply the suggested solutions to my problem. I have also tried to derive the correct formula multiple times, but all my attempts so far have failed.
I am most likely missunderstanding something about the problem and I seem to be unable to generalise the existing answers to my problem.
The following values represent the current state of my viewBox:
offsetX
offsetY
scroll
width
height
I compute the zoomFactor as a function of the scroll variable (Math.exp(scroll/1000)) and set the viewBox property of my SVG as follows: `${offsetX} ${offsetY} ${width * zoomFactor} ${height * zoomFactor}`.
What I am struggling with, is computing the new offsetX and offsetY values based on the previous state and the current position of the mouse inside of the SVG.
processMouseScroll(event: WheelEvent) {
const oldZoomFactor = zoomFactor(this.scroll);
const newZoomFactor = zoomFactor(this.scroll + event.deltaY);
this.scroll = this.scroll + event.deltaY;
this.offsetX = ???;
this.offsetY = ???;
}
How do I compute the new offsets, based on the previous state, so that the when scrolling the mouse, the point bellow it will appear to be stationary?
Thank you for your answers.

I have finally managed to get it working. Turns out, the answer from the first question I found was correct, but my understanding of SVG viewBox was incorrect and I used bad mouse coordinates.
the offset (min-x and min-y; drawn green) of a viewBox is abbsolute and does not depend on the width and height of the viewBox. The mouse coordinates relative to the SVG element (coordinates drawn in black, SVG element drawn in red) are relative to the size of the viewBox. If I enlarge the viewBox, then the part of the picture I can see inside of it shrinks and 100px line drawn by the mouse will cover more of the image.
If we set the size of the dimensions of the viewBox to be the same as the size of the SVG element (initial state), we have a 1:1 scale between the image and the viewBox (the red rectangle would cover the entire image, bordered black). When we make the viewBox smaller we will not fit the entire image into it and therefore the image will appear to be larger.
If we want to compute the absolute position of our mouse in relation to the entire image we can do it like this (same for Y):
position = offsetX + zoomFactor * mouseX (mouseX relative to the SVG element).
When we zoom, we change the factor, but don't change the position of the mouse. If we want the absolute position under the mouse to remain the same, we have to solve the following set of equations:
oldPosition = oldOffsetX + oldZoomFactor * mouseX
newPosition = newOffsetX + newZoomFactor * mouseX
oldPosition = newPosition
we know the mouse position, both zoom factors and the old offset, therefore we solve for the new offset and get:
newOffsetX = oldOffsetX + mouseX * (oldZoomFactor - newZoomFactor)
which is the final formula and very similar to this answer.
Put together we get the final working solution:
processMouseScroll(event: WheelEvent) {
const oldZoomFactor = zoomFactor(this.scroll);
const newZoomFactor = zoomFactor(this.scroll + event.deltaY);
// mouse position relative to the SVG element
const mouseX = event.pageX - (event.target as SVGElement).getBoundingClientRect().x;
const mouseY = event.pageY - (event.target as SVGElement).getBoundingClientRect().y;
this.scroll = this.scroll + event.deltaY;
this.offsetX = this.offsetX + mouseX * (oldZoomFactor - newZoomFactor);
this.offsetY = this.offsetY + mouseY * (oldZoomFactor - newZoomFactor);
}

Related

LibGDX ModelInstance - can't reset scale, setting the transform while scaled deforms the model

Is there any way to reset a modelInstance's scale or set a modelInstances position without using transform.set? I'm making a 3D monster maker, When a part is selected a set of 3D arrows move to the part and become visible allowing the part to be dragged around. It's come to my attention that setting an arrow's position while scaled heavily deforms the arrow for whatever reason. This is a huge problem as I need the arrows to be different sizes depending on the size of the part. Without setting position, with setting position. Using transform.set on a non-scaled ModelInstance (or one scaled by 1) works fine.
I thought a solution would be to reset the size of the arrow before moving it and then re-scaling it after it was moved but there doesn't seem to be any method to do this and using transform.scale(-currentScale, -currentScale, -currentScale) inverts the arrow and increases its size rather than decreasing it.
Here is how I change the arrow's position: arrow[j].transform.set(modelInstance[i].transform.getTranslation(new Vector3(), arrow[j].transform.getRotation(new Quaternion());
Ok, I did some logging and found that scaling changes all values within the matrix4 not just the size of the modelInstance. I didn't find a way to only scale rotation but I figured a way around this: I created an empty Matrix4 and set that to a copy of the arrows matrix before scaling. When the arrows need to be positioned at a part I set the arrow's matrix to the baseMatrix, used transform.set to change the arrows' position, and then rescaled the arrow.
if(Intersector.intersectRayBoundsFast(pickRay, modelInstance[i].boundingBox)) {
modelInstance[i].transform.getTranslation(modelInstance[i].position);//get the part's position and store it
for(int j = 0 ; j < 3 ; j++) {
arrow[j].transform = arrow[j].baseTransform.cpy();//reset the arrows transform
arrow[j].transform.getRotation(arrow[j].rotation);//get the arrow's (unscaled) rotation and store it
arrow[j].transform.set(modelInstance[i].position, arrow[j].rotation);//set the arrow's position and rotation
arrow[j].changeSize('S');//rescale the arrow
}
break;
}
You can use transform.scale(1f/currentScale, 1f/currentScale, 1f/currentScale) to revert the scaling or the function ModelInstance.calculateTransforms()

Edge cutting while rotating an SVG circle

I'm trying to rotate an SVG circle (a group of three 120deg arcs actually), and running into problems where the edges of the arcs are being cut off (at least in Firefox)
http://jsfiddle.net/RedDevil/u9u9rbbw/
var circle;
var root = Snap('#arcs');
circle = root.select('.circle');
Snap.animate(0, 360, function(v) {
return circle.transform("r" + v);
}, 2000);
Here is a render of the static rotated circle to highlight the problem
http://jsfiddle.net/RedDevil/gvbtr2Ly/
circle = root.select('.circle');
circle.transform("r" + 40);
I've inspected every parent of the arcs, and none of them seem to be cutting the arcs off. I can't seem to pinpoint what could be causing the cuts... I thought it could be the viewBox, but adjusting the values doesn't help sadly... I've known SVG in many forms over the past, but am new to using it with HTML...
You've probably already figured it out by now, but just incase someone else comes across this issue...
The issue does relate to the viewBox, but also the objects within that viewBox. Basically you need to provide some padding within your viewBox to allow for the rotation. So, if your object is 400 x 400 and your viewBox is 400 x 400 any minor discrepancy will appear to be cut off (ie, out of the viewBox) therefore you should allow some padding. So your object would be 400 x 400 and positioned center and your viewBox could be 420 x 420.
Hopefully that makes sense.

Scale, Position & Rotate Parent object to make child object take up entire stage

Using the first photo below, let's say:
The red outline is the stage bounds
The gray box is a Sprite on the stage.
The green box is a child of the gray box and has a rotation set.
both display object are anchored at the top-left corner (0,0).
I'd like to rotate, scale, and position the gray box, so the green box fills the stage bounds (the green box and stage have the same aspect ratio).
I can negate the rotation easily enough
parent.rotation = -child.rotation
But the scale and position are proving tricky (because of the rotation). I could use some assistance with the Math involved to calculate the scale and position.
This is what I had tried but didn't produce the results I expected:
gray.scaleX = stage.stageWidth / green.width;
gray.scaleY = gray.scaleX;
gray.x = -green.x;
gray.y = -green.y;
gray.rotation = -green.rotation;
I'm not terribly experienced with Transformation matrices but assume I will need to go that route.
Here is an .fla sample what I'm working with:
SampleFile
You can use this answer: https://stackoverflow.com/a/15789937/1627055 to get some basics. First, you are in need to rotate around the top left corner of the green rectangle, so you use green.x and green.y as center point coordinates. But in between you also need to scale the gray rectangle so that the green rectangle's dimensions get equal to stage. With uniform scaling you don't have to worry about distortion, because if a gray rectangle is scaled uniformly, then a green rectangle will remain a rectangle. If the green rectangle's aspect ratio will be different than what you want it to be, you'd better scale the green rectangle prior to performing this trick. So, you need to first transpose the matrix to offset the center point, then you need to add rotation and scale, then you need to transpose it away. Try this set of code:
var green:Sprite; // your green rect. The code is executed within gray rect
var gr:Number=green.rotation*Math.PI/180; // radians
var gs:Number=stage.stageWidth/green.width; // get scale ratio
var alreadyTurned:Boolean; // if we have already applied the rotation+scale
function turn():void {
if (alreadyTurned) return;
var mat:flash.geom.Matrix=this.transform.matrix;
mat.scale(gs,gs);
mat.translate(-gs*green.x,-gs*green.y);
mat.rotate(-1*gr);
this.transform.matrix=mat;
alreadyTurned=true;
}
Sorry, didn't have time to test, so errors might exist. If yes, try swapping scale, translate and rotate, you pretty much need this set of operations to make it work.
For posterity, here is what I ended up using. I create a sprite/movieClip inside the child (green) box and gave it an instance name of "innerObj" (making it the actually content).
var tmpRectangle:Rectangle = new Rectangle(greenChild.x, greenChild.y, greenChild.innerObj.width * greenChild.scaleX, greenChild.innerObj.height * greenChild.scaleY);
//temporary reset
grayParent.transform.matrix = new Matrix();
var gs:Number=stage.stageHeight/(tmpRectangle.height); // get scale ratio
var mat:Matrix=grayParent.transform.matrix;
mat.scale(gs,gs);
mat.translate(-gs * tmpRectangle.x, -gs * tmpRectangle.y);
mat.rotate( -greenChild.rotation * Math.PI / 180);
grayParent.transform.matrix = mat;
If the registration point of the green box is at one of it's corners (let's say top left), and in order to be displayed this way it has a rotation increased, then the solution is very simple: apply this rotation with negative sign to the parent (if it's 56, add -56 to parent's). This way the child will be with rotation 0 and parent -> -56;
But if there is no rotation applied to the green box, there is almost no solution to your problem, because of wrong registration point. There is no true way to actually determine if the box is somehow rotated or not. And this is why - imagine you have rotated the green box at 90 degrees, but changed it's registration point and thus it has no property for rotation. How could the script understand that this is not it's normal position, but it's flipped? Even if you get the bounds, you will see that it's a regular rectangle, but nobody know which side is it's regular positioned one.
So the short answer is - make the registration point properly, and use rotation in order to display it like in the first image. Then add negative rotation to the parent, and its all good :)
Edit:
I'm uploading an image so I can explain my idea better:
 
As you can see, I've created a green object inside the grey one, and the graphics INSIDE are rotated. The green object itself, has rotation of 0, and origin point - top left.
#Vesper - I don't think that the matrix will fix anything in this situation (remember that the green object has rotation of 0).
Otherwise I agree, that the matrix will do a pretty job, but there are many ways to do it :)

displacementMapfilter floats with object below it

I've been creating a magnifier class using a standard displacementMapfilter similar to Adobe's and numerous other examples: http://help.adobe.com/en_US/ActionScript/3.0_ProgrammingAS3/WS5b3ccc516d4fbf351e63e3d118a9b90204-7da2.html
The filter is being used at the application level so it filters everything, and there's a large number of image elements that can be dragged, rotated, and scaled around the screen, and when passed below this filter it magnifies them. I also intend to add some more functionality to allow dragging the filter around.
The filter works as intended, magnifying everything that passes by it. The weird thing is whenever an image is being dragged outside of the top or left edge of the screen, the filter floats in that direction the distance it takes before the image is entirely offscreen (ie: the filter floats 500 pixels left if a picture 500 pixels wide is pulled offscreen to the left.)
I'm using an enterFrame listener to constantly update its position, and it works as follows:
private function onEnterFrame(e:Event):void {
dPoint.x = stage.stageWidth - radius;
dPoint.y = stage.stageHeight - radius;
dFilter = new DisplacementMapFilter(map, dPoint, BitmapDataChannel.RED, BitmapDataChannel.BLUE, 100, 100, DisplacementMapFilterMode.IGNORE, 0x000000, 0);
// The application is a displayObject itself, so just apply filters to "this"
this.filters = [dFilter];
}
So this code should anchor the filter at the bottom right of the screen, yet somehow whenever an image is dragged off, the filter drifts with it. Is there any reason a filter would do that, and any way I could stop it?
Dragging an image off the stage changes the size and shape of the rectangle the filter is being applied to (picture the filter as if it's taking a snapshot of everything on the stage). When the image moves off the top left, it means that (0, 0) on the filter is actually at the top left corner of the image.
If you check the bounds of the stage (in the stage's own coordinate space), you should see top and left become negative numbers when you drag an image off:
stage.getBounds(stage).top;
stage.getBounds(stage).left;
Cancelling out any negative bounds should keep your filter in the correct position:
var stageBounds:Rectangle = stage.getBounds(stage);
if (stageBounds.left < 0) {
dPoint.x -= stageBounds.left;
}
if (stageBounds.top < 0) {
dPoint.y -= stageBounds.top;
}

Difference between defining sprite's x, y coordinates, and painting object in some location

I have a task:
I need to place about 100 sprites on one canvas (with prepared grid on it). I need to place them as invisible (circles) stones, on the board, and make visible only on mouseover.
The problem I come across is following, I can't place those objects accurately into the nodes on the grid.
E.g.
if I define stones (it's just a sprite, as I said earlier) this way:
var stone:StoneSprite = new StoneSprite();
stone.x = this.x + 2*cellWidth;
stone.graphics.beginFill( 0x000000 );
stone.graphics.drawCircle(stone.x , this.y + cellWidth, cellWidth/3 );
stone.graphics.endFill();
rawChildren.addChild(stone);
They don't sit on the node...
See image:
http://img.skitch.com/20091014-kuhfyjeg1g5qmrbyxbcerp4aya.png
And if I do it this way:
var stone:StoneSprite = new StoneSprite();
stone.graphics.beginFill( 0x000000 );
  stone.graphics.drawCircle(this.x + 2*cellWidth , this.y + cellWidth, cellWidth/3 );
  stone.graphics.endFill();
rawChildren.addChild(stone);
The stone is displayed correctly in the grid node... See image 2:
http://img.skitch.com/20091014-f595tksjxramt98s7yfye591bh.png
So I wonder what is the difference between these 2 approaches.
Also, I think I need to pass correct coordinates to the stone class... In case I would like to change some properties of the stone object. E.g. visibility, or radius.
Could you please suggest, what's wrong in defining coordinates as stone.x, stone.y
How to fix the problem with incorrect positioning.
Would really appreciate ideas about the problem, I am trying to solve for so long :(
Assume x & y are 30 and cellWidth is 30.
First Example:
stone.x = 30 + 60; //90
drawCircle(90, 60, 10);
This means if you were to draw a rectangle around your circle, it would be at [170,50]. (x,y).
Second Example:
stone.x = 0;
drawCircle(90, 60, 10)
This means the rectangle around your circle is at [80,50];
In the first example, you are moving the sprite to position x==90. Then drawing a circle whose center is at x==90 inside the sprite. So relative to this, you're at x==180. But because a circle's x,y coords are the center, subtract 10 for the radius to get the boundary x position.
In the second example, the sprite defaults to position x==0 relative to this and you're drawing the circle inside the sprite at position x==90. (therefore it begins at x==80).
I am not sure what's causing the issue - might be some padding induced by the container - can't say without testing. But I believe that adding a Sprite (say board) to canvas.rawChildren and using it as the parent for the grid and stones would fix the issue.