I have an <img> that is zoomed upon mousewheel scrolling, by adjusting transform: scale(). I want the zooming to be like in Google Maps, where you zoom to where the mouse cursor is, not to the center of the image. I'd like to not use canvas though, just for the learning experience (that's also why the other questions I found did not really help).
I set up a JSFiddle to demonstrate the problem. My thought process was as follows: when zooming in by 10%, the image expands in all directions, from the center of the image, by 10%. That means that e.g., the left and right edge will travel 5% of the original width in each direction. I therefore tried to solve the problem like so:
Calculate mouse offset from image center
Calculate new image offset (top and left) by multiplying mouse offset with zoom factor and divide by two
Apply offset and watch it all blow up it my face with the power of a million burning suns
It seems that I just can't find a formula or algorithm that fits.
Eventually I figured it out myself, although only by looking at existing solutions. Here is the JSFiddle that contains only the essentials.
The idea is to first set transform-origin: 0 0. This makes sure that, upon zooming, the image expands down and right, instead of distributing the increase in width over all four sides. Note that it does not reposition the image, it just changes the origin for all transformations.
Additionally, this JSFiddle assumes that the top and left margins of the image are aligned with the top and left margins of the container element. If the image should be repositioned before zooming occurs, this should be done through transform: translate() and the translateX and translateY values need to be updated accordingly.
The heart of the logic is this:
// Track the percentage change between the old
// and the new scale of the image
const ratio = 1 - nextScale / currentScale
// get the current mouse offset
const {
clientX,
clientY
} = event
// The += here is extremely important!
// The new 2D translation values are derived from the difference
// between mouse cursor position and current (!) 2D translation.
// So where is the mouse cursor relative to the translated image
// This difference is then adjusted by the % change of the scaling
translateX += (clientX - translateX) * ratio
translateY += (clientY - translateY) * ratio
/*
This would work for the first wheel scroll. But afterwards, the
image will not be translated enough to offset the zooming because
we're not taking into account the existing translation
translateX += (clientX - translateX) * ratio
translateY += (clientY - translateY) * ratio
*/
So to summarize the required steps:
Calculate the next scale
Calculate the current mouse offset relative to the translated image
Adjust the mouse offset for the change in scaling, e.g., const percentChange = 1 - nextScale / currentScale
Add the adjusted mouse offset to the existing values for translate()
Apply the transformation (scaling and the translation)
The linked JSFiddle also includes Lodash and transition: transform 330ms ease-in-out; to make the scrolling a little smoother and not affect browser performance too much.
You could use transform-origin : <position of your mouse pointer> :
transform-origin : 0% 0% points on the top left corner.
transform-origin : 100% 100% points on the bottom right corner.
Here's an example I made : https://jsfiddle.net/zez538L8/4/
The javaScript :
var currentzoom = 1;
function zoom(delta, e) {
var img = document.getElementById("test");
var width = img.offsetWidth; //calculating the size of the img (in px)
var height = img.offsetHeight;
var x = event.offsetX; //calculating the position of the mouse pointer on the picture (in px)
var y = event.offsetY;
var xpercent = x*100/width; //calculating the position of the mouse pointer on the picture (in %)
var ypercent = y*100/height;
img.style.transform = "scale("+currentzoom+")"; //scaling the picture
img.style.transformOrigin = xpercent + "% "+ ypercent +"%"; //transform-origin
currentzoom += delta;
}
Related
I am new to css and learning different type of css styles. I want to learn how the effect used in official MongoDb website. The effect which tracks the mouse position and transforms the boxes. I know how to do the transform in css. But, how can it be done with the mouse position. Thanks for the help.
General overview of how to do it:
Register a mousemove-handler and track your mouse-screen location (see link)
translate mouse screenlocation, to mouse location relative to rectangle:
e.target in mousemove event gives you the rectangle (or some descendent which allows you to get to the rectangle.
given the target element get it's position (top + left using getBoundingClientRect) as well as width and height. These should be easy to lookup
Notice that the mouse at the center of the rectangle doesn't rotate the rectangle. Only when moving to the edges, the rotation starts to get going. This rotational rate-of-change seems to be linear. So:
determine the max rotation that seems nice to have in degrees. Simply test with different numbers in the chrome dev tools or similar: transform: rotateY(0.01deg) rotateX(0.01deg); Say you want to have a max rotation of 25 degrees.
say the rectangle is 100px in width. It's clear to see that each pixel movement from the center to the edge (50 px in total) adds 0.5 degree to the rotation due to the linear rate of change: 25 deg / 50px. So for example moving 20px to the left of the center translates to rotateY(10deg);. Moving 20px to the right results in the mirror rotation (rotateY(-10deg);). NOTE: the positive and negative may need to be flipped.
similarly, moving along the Y-axis changes the rotateX-property.
Once calculated, set the css-property and you're done
I believe this must be done with Javascript. The general idea is when the mouse enter/move on the element, you compare it's coordinate with the position and width/height of the element to decide the rotation values. When the mouse leave the element, you reset the values of the rotation back to normal.
You can get the coordinate of the mouse from event by using:
const mouseX = event.clientX; // Get the horizontal coordinate
const mouseY = event.clientY; // Get the vertical coordinate
And the position of the element:
const eleLoc = event.target.getBoundingClientRect();
From there you calculate the center and the width/height of the element:
const centerX = (eleLoc.right + eleLoc.left) / 2
const centerY = (eleLoc.bottom + eleLoc.top) / 2
const halfWidth = (eleLoc.right - eleLoc.left) / 2
const halfHeight = (eleLoc.bottom- eleLoc.top) / 2
Then you calculate the distance between the mouse and the center in percent. In the center, the distance is 0, at the border, it's 1 (or -1).
const percentX = (mouseX - centerX) / halfWidth
const percentY = (mouseY - centerY) / halfHeight
Now you only need to rotate X/Y based on the distance percent:
const degX = percentX * maxDegX
const defY = percentY * maxDegY
event.target.style.transform = `perspective(${yourPerspective}px) rotateX(${degX}deg) rotateY(${degY}deg)`
Remember to reset the transform when your mouse move out.
There are some libraries for this, ie: tilt.js
So I have a bunch of divs that are absolute to an overlay. The user creates a square div by dragging on the overlay. If you were to create a div and then zoom in and out, the div stays positioned in the same spot since it is absolute to the overlay as mentioned before.
However here's where the problem is. You are able to save the div location (top, left, height, width) to a .json and load the .json to show all of your previously saved divs. This works fine.... if you save and load on the same browser zoom percentage.
If you were to draw divs on 150% zoom, for example, and try to load the div's position on 100% zoom, the position is altered (as to make up for the zoom I presume).
I tried forcing the window zoom to 100% on save, but that didn't work, and I am kind of stuck now. Does anyone have any suggestions?
Following Alex's advice to use percentages, I was able to come up with a solution that works perfectly.
In the method that sets the top and left of the div - I changed that from pixels relative to the parent to percentage relative to the parent.
So this
{
width = endX - startX;
left = startX;
height = endY - startY;
top = startY;
}
Became this
{
width = endX - startX;
var percentageLeft = startX / parent.offsetWidth * 100;
left = percentageLeft;
height = endY - startY;
var percentageTop = startY / parent.offsetHeight * 100;
top = percentageTop;
}
and then by changing the saving/loading to look for percentages instead of pixels, I was able to make this work!
I'm trying to create a zoom able image upon click of a button, however the image should be zoomed centered on the view able area, as the image may be bigger than the container.
I've created a fiddle here to illustrate what I want, I'd obviously usually hide the image outside the container, I'm not bothered about that part just now, also put in a border overlay to show the bounds of the container.
I've done the zoom part based on the image ratio, I just need to work out the new top and left css values and apply it to the image after the zoom. Also the image is draggable, so once you move the image it must take into account the position of the image.
Basically, the centrol point of the image within the red container must remain the same after the zoom, so you are effectly zooming in on whatever is at the middle of the container.
http://jsfiddle.net/wFaFg/1/
why do you we need code to link to jsfiddle?
Thanks
edit:
http://jsfiddle.net/FU55w/
getting close with the above fiddle, but still not zooming completely on central point
So I found the solution,
It is slightly more complex than just finding out how much the image increased in size.
I work out the x & y value of the center point of the image within the container, by taking the left and top value, turning it positive then adding half the container width and height.
var x = Math.abs(image.position().left) + container.width() / 2
var y = Math.abs(image.position().top) + container.height() / 2
I work out the ratio of the image scale by dividing the new width by the old width, then I can multiply the x & y value by the ratio.
Then take the difference between the new x and y away from the current left and top.
image.position().left - (newX - x)
image.position().top - (newY - y)
Complete fiddle:
http://jsfiddle.net/fbd2mk5q/
Try the new fiddle based on your comment:
http://jsfiddle.net/wFaFg/6/
$("#zoom").on("click", function(e)
{
var container = $("#container");
var image = $("#container img");
var curLeft = image.position().left;
var curTop = image.position().top;
var ratio = image.height() / image.width();
image.css({
height: image.height() + (20 * ratio),
width: image.width() + (20 * ratio),
left: curLeft - ((20 * ratio)/2),
top: curTop - ((20 * ratio)/2)
});
});
I have an <img> within a <div> which can be moved around using four directional buttons, for example:
The image is obviously larger than its container, hence the directional buttons to move it in different directions.
There is also a zoom control where you can zoom in and out. I set up the scaling method with ease, by just applying a zoom factor as a percentage to the base width and height:
scale: function(zoom)
{
image.width = baseWidth * zoom;
image.height = baseHeight * zoom;
}
// Zoom in 50%.
Scene.scale(1.5);
This is fine however the image scales from top-left, meaning that the image looks like it's getting sucked out towards the top left during a zoom in and spat back out when zooming out.
I'm trying to have the zoom effect apply from the centre of the container, like this:
But I'm finding it hard to get my head around the mathematics required to move the image after scaling applies to give this effect.
The closest I've gotten is to move the image based on the difference between the current zoom and the new zoom level, but it's still slightly off and gives a 'curved' effect when zooming.
Is there a common formula used to reposition an image so that it scales around a different origin (i.e. not top-left (0,0)).
This is what it looks like currently.
You have to take your original coordinates and calculate the center of your original image, that is x_center = x_orig + width_orig / 2; Then you can calculate the new x coordinate of your scaled image: x_new = x_center - width_new / 2. The same applies for y. Then move your scaled image to these new coordinates. If you do it after each time you scale the image, it will look as though it is scaled around its center.
I have a sprite animation, a small cannon rendered using a 3D app. I have exactly 360 frames for a 360 degree turn. Each image has a 100x100 pixel size.
So basically what I am trying todo is when I click anywhere in the page, the barrel of the cannon needs to rotate to point at the mouse cursor, sound simple maybe but I can't really get it to work very well, perhaps cause my math skills is lacking :P
What I currently have is something like this
/* This is my div with the cannon background image (360 images stitched into one) each "image area" is 100x100px */
obj.cannon = $('#cannon');
/* Get the X/Y of the cannon loc in the dom */
var cannonX = $(obj.cannon).offset().left;
var cannonY = $(obj.cannon).offset().top;
/* Get radians using atan2 */
var radians = Math.atan2(e.pageY-cannonY, e.pageX-cannonX);
/* Convert to degrees */
var degrees = radians * (180/Math.PI);
And this is where I am, I mean since the image width is 100px and I need to move the background-position by 100px to move the cannon one degree right, because 360 images * 100px = 36000px in total. So the stitched sprite is like 36000px wide.
So
Insert weird calculation here based on the current backgroundPosition of the image-sprite and apply new backgroundPosition based on where you click with the mouse cursor, then use some sort of setTimeout(animateIt, speed); to "animate" the background position to the new position.
function animateIt(){
if(newpos!=targetpos) { //Use < > here if we need to add or remove
newpos+=100; //Until we arrive at the new backgroundPosition
$(obj.cannon).css({backgroundPosition: newpos+'px' });
setTimeout(animateIt, speed);
}
}
Am I at all on the right track here, am I thinking correctly about this? I feel stupid, this should be a simple thing but right now I am having a brain meltdown I think =P. My problem is I don't know how to properly arrive at the "new target backgroundposition" and then animate it ++ or -- based on the current background position :/
Well, here is a simplified working example with 10 images.
I'll post the code and jsFiddle now, and I might come back later to cover it in depth. But basically you just order your images correctly, and then you pick the segment by using (Segments - Math.floor(degree / (360 / segments))). You may have to adjust your 0 degree. For example, I made my 0 equal to what would normal by 90.
Pay attention to the fact that the screen coordinates, x and y, increase right and down. This makes the degrees of atan work clockwise instead of the usual counter clockwise in coordinate systems where x and y increase right and up.
I added in some text output that shows the degrees and image segment being shown.
jQuery handles normalizing the x and y position nicely. Just take care that your CSS setup is cross browser.
Working jsFiddle example
Here's our image:
Here's our HTML:
<div id="main"><div id="img"></div></div>
<div id="info">
<span></span><br/>
<span></span>
</div>
CSS:
div#main {
width:500px;
height:500px;
border:2px #000 solid; }
div#img {
width:94px;
height:119px;
overflow:hidden;
top:50%;
left:50%;
margin-left:-45px;
margin-top:-60px;
position:relative;
background-image:url('http://imgur.com/3UPki.png');
background-position:0;}
div#info {
position: absolute;
bottom:0;
left:0; }
Javascript / jQuery:
$(function() {
var position = $("div#img").position(),
mouseX, mouseY, imgX, imgY, degree;
imgX = position.left;
imgY = position.top;
$("#main").mousemove(function(e) {
// degree is arctan y over x (soh,cah,toa)
degree = Math.atan2((e.pageY - imgY),(e.pageX - imgX))*(180 / Math.PI);
degree = (degree - 90) % 360;
// jQuery normalizes pageX and pageY
// transfrom from -180 to 180 ==> 0 to 360
if (degree < 0) degree = 180 + (180 - (Math.abs(degree)));
rotate(degree);
$("span:first").html("Segment: " + (9 - Math.floor(degree / 36)));
$("span:last").html("Degree: " + Math.floor(degree));
});
function rotate(degree) {
var off = 9 - Math.floor(degree / 36);
$("div#img").css("background-position",-off*94);
}
});
Working jsFiddle example
Keep in mind that the degrees you get from atan will start pointing right for zero degrees and go clockwise from there (-90 is up, 90 is down).
Each position of your image should correspond to a specific angle. Once you have the degrees measured (it looks like you have that part right), use some type of mapping to translate your degrees to the proper image offset. I don't know what your image looks like so I can't help with that. Assuming your image starts pointing right, and goes around clockwise from there, the degrees will correspond directly the the offset for the right image. (I suggest you arrange your frames like this for ease...)