Zoom towards are mouse is over - javascript

I have found this script online which i though would help me to zoom in on an image, but zoom towards the mouse position. However when the mouse is positioned to the left this isn't the case. I feel like there is a simple change I need to make however I can't find it!
window.addEventListener('load', () => {
new Vue({}).$mount('#app');
});
Vue.component('test', {
template: '#template',
data() {
return {
zoomMin: 1,
zoomMax: 7,
dragEventX: null,
dragEventY: null,
touchEvent: null,
zoomPointX: 0,
zoomPointY: 0,
zoomScale: 1,
zoomStyle: null,
frame: 1,
speed: 1,
zoom: 1,
}
},
mounted() {
this.$refs.image.addEventListener('wheel', this.onWheel);
},
methods: {
onWheel($event) {
$event.preventDefault();
let direction = Math.sign($event.deltaY);
let n = this.zoomScale - direction / (6 / this.speed);
this.setZoomScale($event.clientX, $event.clientY, n)
},
setZoomScale(clientX, clientY, n) {
const bounding = this.$refs.image.getBoundingClientRect();
let mouseLeft = clientX - bounding.left,
mouseTop = clientY - bounding.top,
zoomPointX = this.zoomPointX || 0,
zoomPointY = this.zoomPointY || 0;
let leftPoint = (mouseLeft - zoomPointX) / this.zoomScale,
topPoint = (mouseTop - zoomPointY) / this.zoomScale;
this.zoomScale = Math.min(Math.max(n, this.zoomMin), this.zoomMax);
let leftZoom = -leftPoint * this.zoomScale + mouseLeft,
topZoom = -topPoint * this.zoomScale + mouseTop;
this.setZoomPoint(leftZoom, topZoom);
},
setZoomPoint(leftZoom, topZoom) {
let left = leftZoom || this.zoomPointX || 0,
top = topZoom || this.zoomPointY || 0,
leftOffset = this.$el.clientWidth * (this.zoomScale - 1),
topOffset = this.$el.clientHeight * (this.zoomScale - 1);
this.zoomPointX = Math.min(Math.max(left, -leftOffset), 0);
this.zoomPointY = Math.min(Math.max(top, -topOffset), 0);
this.setZoomStyle();
},
setZoomStyle() {
this.zoomStyle = {
transform: `translate(${this.zoomPointX}px, ${this.zoomPointY}px) scale(${this.zoomScale})`
};
},
}
});
#app {
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 80%;
height: 80%;
overflow: hidden;
border: 1px solid #000;
}
.container img {
width: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<test></test>
</div>
<script type="text/x-template" id="template">
<div class="container">
<img ref="image" :style="zoomStyle" src="https://s3-eu-west-1.amazonaws.com/crash.net/visordown.com/field/image/2020_YAM_YZF1000R1_EU_DPBMC_STA_001-70516%20copy.jpg"></img>
</div>
</script>

You do not zoom towards the mouse on the right either; in fact, if you put your mouse in the top-left corner you zoom in to the center of the image. Besides that I can at least identify that this.zoomPointX = Math.min(Math.max(left, -leftOffset), 0); only allows your translate to go left, and thus only allow zooming towards the right of the center line.
Some debugging
Let's start with taking a step back and figuring out what we are doing here. The final styling is a translate, followed by a scale. The scale happens around the center of what you are seeing on screen right then, and the translate that happens before that is meant to move the image so the point around which you want to zoom is in the middle.
To properly debug this we need a better understanding of what the code is doing, so I first added some debug markers and disabled the zoom styling, so we can visualise what is happening without it zooming all over the place.
window.addEventListener('load', () => {
new Vue({}).$mount('#app');
});
Vue.component('test', {
template: '#template',
data() {
return {
zoomMin: 1,
zoomMax: 7,
dragEventX: null,
dragEventY: null,
touchEvent: null,
zoomPointX: 0,
zoomPointY: 0,
zoomScale: 1,
zoomStyle: null,
frame: 1,
speed: 1,
zoom: 1,
// Debugging
imageHeight: 0,
imageWidth: 0,
}
},
computed: {
markerStyle() {
return {
top: `${this.imageHeight / 2 - this.zoomPointY}px`,
left: `${this.imageWidth / 2 - this.zoomPointX}px`,
};
},
boundaryMarkerStyle() {
const middleY = this.imageHeight / 2 - this.zoomPointY;
const middleX = this.imageWidth / 2 - this.zoomPointX;
const height = this.imageHeight / this.zoomScale;
const width = this.imageWidth / this.zoomScale;
return {
top: `${middleY - height / 2}px`,
left: `${middleX - width / 2}px`,
width: `${width}px`,
height: `${height}px`,
};
},
},
mounted() {
// Moved the listener to the container so we can overlay something over the image
this.$refs.container.addEventListener("wheel", this.onWheel);
// Temporary for debugging; I could also determine it dynamically and should if we
// we want to be able to resize
this.$refs.image.addEventListener("load", () => {
const bounding = this.$refs.image.getBoundingClientRect();
this.imageHeight = bounding.height;
this.imageWidth = bounding.width;
});
},
methods: {
onWheel($event) {
$event.preventDefault();
let direction = Math.sign($event.deltaY);
let n = this.zoomScale - direction / (6 / this.speed);
this.setZoomScale($event.clientX, $event.clientY, n)
},
setZoomScale(clientX, clientY, n) {
const bounding = this.$refs.image.getBoundingClientRect();
// mouseLeft and mouseTop represent the pixel within the container we are targeting
const mouseLeft = clientX - bounding.left;
const mouseTop = clientY - bounding.top;
// zoomPointX and Y represent what point we were zooming towards
const zoomPointX = this.zoomPointX || 0;
const zoomPointY = this.zoomPointY || 0;
// This attempts to modify the point we are targeting based on
// what we are zooming towards before and what we are zooming towards now
// zoomPointX represents something that is calculated with a different zoomScale, so this
// presumably calculates bogus
const leftPoint = (mouseLeft - zoomPointX) / this.zoomScale;
const topPoint = (mouseTop - zoomPointY) / this.zoomScale;
// This normalizes the zoom so we can't zoom out past the full image and not past 7 times the current image
this.zoomScale = Math.min(Math.max(n, this.zoomMin), this.zoomMax);
// This should represent the point we are zooming towards (I think?)
const leftZoom = -leftPoint * this.zoomScale + mouseLeft;
const topZoom = -topPoint * this.zoomScale + mouseTop;
// This function breaks its promise to set only the zoom scale and also sets the zoom point. :(
this.setZoomPoint(leftZoom, topZoom);
},
setZoomPoint(leftZoom, topZoom) {
const left = leftZoom || this.zoomPointX || 0;
const top = topZoom || this.zoomPointY || 0;
const leftOffset = this.$el.clientWidth * (this.zoomScale - 1);
const topOffset = this.$el.clientHeight * (this.zoomScale - 1);
this.zoomPointX = Math.min(Math.max(left, -leftOffset), 0);
this.zoomPointY = Math.min(Math.max(top, -topOffset), 0);
this.setZoomStyle();
},
setZoomStyle() {
// this.zoomStyle = {
// transform: `translate(${this.zoomPointX}px, ${this.zoomPointY}px) scale(${this.zoomScale})`
// };
},
}
});
#app {
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 80%;
height: 80%;
overflow: hidden;
border: 1px solid #000;
position: relative;
}
.container img {
width: 100%;
}
.marker {
position: absolute;
border: 8px solid red;
z-index: 9999;
}
.boundary-marker {
position: absolute;
border: 2px dashed red;
z-index: 9999;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<test></test>
</div>
<script type="text/x-template" id="template">
<div class="container" ref="container">
<img ref="image" :style="zoomStyle" src="https://s3-eu-west-1.amazonaws.com/crash.net/visordown.com/field/image/2020_YAM_YZF1000R1_EU_DPBMC_STA_001-70516%20copy.jpg"></img>
<div class="marker" :style="markerStyle"></div>
<div class="boundary-marker" :style="boundaryMarkerStyle"></div>
</div>
</script>
Fixing the center point
First of all, lets disable the functionality to clamp the zoom point, as we know it to be broken.
this.zoomPointX = left;
this.zoomPointY = top;
Then lets focus on getting the center point right. To get this right, we need to actually determine which pixel of the original image we are targeting, taking into account we might have zoomed in already! We need to keep in mind that the translate function and the scale function always are on the original image.
We can determine the part of the image we are currently viewing with zoomPointX, zoomPointY and zoomScale. (hint: we did that for the debug marker already) zoomPointX and zoomPointY do not really represent a point we zoom towards, but more the translation we made, so I have renamed them to translateX and translateY for convenience.
The x coordinate of the pixel in the original picture we can currently see on the left side of the screen is calculated by finding the x coordinate of the middle point on the original image, then subtracting half of our viewport from it:
const leftSideX = (this.imageWidth / 2 - this.translateX) - (this.imageWidth / this.zoomScale / 2);
The number of pixels from the left side of our viewport to the point, as it would be on the original picture can be calculated by multiplying the amount of pixels this viewport represents with the percentage of pixels from the left border we are
const offsetX = (this.imageWidth / this.zoomScale) * (mouseLeft / this.imageWidth);
And then we get our translateX by calculating from the middle of the image again.
this.translateX = -(leftSideX + offsetX - (this.imageWidth / 2));
window.addEventListener('load', () => {
new Vue({}).$mount('#app');
});
Vue.component('test', {
template: '#template',
data() {
return {
zoomMin: 1,
zoomMax: 7,
dragEventX: null,
dragEventY: null,
touchEvent: null,
translateX: 0,
translateY: 0,
zoomScale: 1,
zoomStyle: null,
frame: 1,
speed: 1,
zoom: 1,
// Debugging
imageHeight: 0,
imageWidth: 0,
}
},
computed: {
markerStyle() {
return {
top: `${this.imageHeight / 2 - this.translateY}px`,
left: `${this.imageWidth / 2 - this.translateX}px`,
};
},
boundaryMarkerStyle() {
const middleY = this.imageHeight / 2 - this.translateY;
const middleX = this.imageWidth / 2 - this.translateX;
const height = this.imageHeight / this.zoomScale;
const width = this.imageWidth / this.zoomScale;
return {
top: `${middleY - height / 2}px`,
left: `${middleX - width / 2}px`,
width: `${width}px`,
height: `${height}px`,
};
},
},
mounted() {
// Moved the listener to the container so we can overlay something over the image
this.$refs.container.addEventListener("wheel", this.onWheel);
// Temporary for debugging; I could also determine it dynamically and should if we
// we want to be able to resize
this.$refs.image.addEventListener("load", () => {
const bounding = this.$refs.image.getBoundingClientRect();
this.imageHeight = bounding.height;
this.imageWidth = bounding.width;
});
},
methods: {
onWheel($event) {
$event.preventDefault();
const direction = Math.sign($event.deltaY);
const scale = this.zoomScale - direction / (6 / this.speed);
this.setZoomScale($event.clientX, $event.clientY, scale);
},
setZoomScale(clientX, clientY, scale) {
const bounding = this.$refs.image.getBoundingClientRect();
// mouseLeft and mouseTop represent the pixel within the container we are targeting
const mouseLeft = clientX - bounding.left;
const mouseTop = clientY - bounding.top;
// translateX and Y represent the translation towards the point we are zooming towards
const leftSideX =
this.imageWidth / 2 -
this.translateX -
this.imageWidth / this.zoomScale / 2;
const offsetX =
(this.imageWidth / this.zoomScale) * (mouseLeft / this.imageWidth);
this.translateX = -(leftSideX + offsetX - this.imageWidth / 2);
const leftSideY =
this.imageHeight / 2 -
this.translateY -
this.imageHeight / this.zoomScale / 2;
const offsetY =
(this.imageHeight / this.zoomScale) * (mouseTop / this.imageHeight);
this.translateY = -(leftSideY + offsetY - this.imageHeight / 2);
// This normalizes the zoom so we can't zoom out past the full image and not past 7 times the current image
this.zoomScale = Math.min(Math.max(scale, this.zoomMin), this.zoomMax);
},
}
});
#app {
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 80%;
height: 80%;
overflow: hidden;
border: 1px solid #000;
position: relative;
}
.container img {
width: 100%;
}
.marker {
position: absolute;
border: 8px solid red;
z-index: 9999;
}
.boundary-marker {
position: absolute;
border: 2px dashed red;
z-index: 9999;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<test></test>
</div>
<script type="text/x-template" id="template">
<div class="container" ref="container">
<img ref="image" :style="zoomStyle" src="https://s3-eu-west-1.amazonaws.com/crash.net/visordown.com/field/image/2020_YAM_YZF1000R1_EU_DPBMC_STA_001-70516%20copy.jpg"></img>
<div class="marker" :style="markerStyle"></div>
<div class="boundary-marker" :style="boundaryMarkerStyle"></div>
</div>
</script>
Fixing the bounds and doing cleanup
The original min-max function that we removed earlier was, I think meant to prevent you from zooming to a point where you see white on the outside of the image (ala what did happen in the original when you zoomed on the bottom right).
We can do this by clamping the value of translateX and translateY to an imaginary rectangle that is half the width/half the height from each of the relevant edges. We start by determining the height/width of our viewport (hint: we already calculated this for the marker). The center is (0, 0), while the edges are variations of (+/- imageWidth / 2, +/- imageHeight / 2).
Afterwards, we just need to clamp the value.
const viewportHeight = this.imageHeight / this.zoomScale;
const viewportWidth = this.imageWidth / this.zoomScale;
const exclusionViewportY = (this.imageHeight / 2) - (viewportHeight / 2);
const exclusionViewportX = (this.imageWidth / 2) - (viewportWidth / 2);
this.translateX = Math.min(Math.max(this.translateX, -exclusionViewportX), exclusionViewportX);
this.translateY = Math.min(Math.max(this.translateY, -exclusionViewportY), exclusionViewportY);
Finally, I took the liberty to move zoomStyle to a computed property. It saves on having to call functions in methods that have nothing to do with that method like you did. I renamed the main function to better represent what it does. I also added a beforeDestroy lifecycle hook, because your code currently leaks memory as the event handler is not removed.
window.addEventListener('load', () => {
new Vue({}).$mount('#app');
});
Vue.component('test', {
template: '#template',
data() {
return {
zoomMin: 1,
zoomMax: 7,
dragEventX: null,
dragEventY: null,
touchEvent: null,
translateX: 0,
translateY: 0,
zoomScale: 1,
frame: 1,
speed: 1,
zoom: 1,
};
},
computed: {
zoomStyle() {
return {
transform: `translate(${this.translateX}px, ${this.translateY}px) scale(${this.zoomScale})`,
};
},
},
mounted() {
// Moved the listener to the container so we can overlay something over the image
this.$refs.container.addEventListener("wheel", this.onWheel);
},
methods: {
onWheel($event) {
$event.preventDefault();
const direction = Math.sign($event.deltaY);
const scale = this.zoomScale - direction / (6 / this.speed);
this.calculateZoom($event.clientX, $event.clientY, scale);
},
calculateZoom(clientX, clientY, scale) {
const bounding = this.$refs.image.getBoundingClientRect();
// mouseLeft and mouseTop represent the pixel within the container we are targeting
const mouseLeft = clientX - bounding.left;
const mouseTop = clientY - bounding.top;
// translateX and Y represent the translation towards the point we are zooming towards
const leftSideX =
bounding.width / 2 -
this.translateX -
bounding.width / this.zoomScale / 2;
const offsetX =
(bounding.width / this.zoomScale) * (mouseLeft / bounding.width);
this.translateX = -(leftSideX + offsetX - bounding.width / 2);
const leftSideY =
bounding.height / 2 -
this.translateY -
bounding.height / this.zoomScale / 2;
const offsetY =
(bounding.height / this.zoomScale) * (mouseTop / bounding.height);
this.translateY = -(leftSideY + offsetY - bounding.height / 2);
// This normalizes the zoom so we can't zoom out past the full image and not past 7 times the current image
this.zoomScale = Math.min(Math.max(scale, this.zoomMin), this.zoomMax);
// Finally, we clamp the center point so we always stay within the image
const viewportHeight = bounding.height / this.zoomScale;
const viewportWidth = bounding.width / this.zoomScale;
const exclusionViewportY = bounding.height / 2 - viewportHeight / 2;
const exclusionViewportX = bounding.width / 2 - viewportWidth / 2;
this.translateX = Math.min(
Math.max(this.translateX, -exclusionViewportX),
exclusionViewportX
);
this.translateY = Math.min(
Math.max(this.translateY, -exclusionViewportY),
exclusionViewportY
);
},
}
});
#app {
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 80%;
height: 80%;
overflow: hidden;
border: 1px solid #000;
position: relative;
padding: 0;
margin: 0;
line-height: 0;
}
.container img {
width: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<test></test>
</div>
<script type="text/x-template" id="template">
<div class="container" ref="container">
<img ref="image" :style="zoomStyle" src="https://s3-eu-west-1.amazonaws.com/crash.net/visordown.com/field/image/2020_YAM_YZF1000R1_EU_DPBMC_STA_001-70516%20copy.jpg"></img>
</div>
</script>

Related

SVG Pan only when zoomed, bigger than viewport

Expected Outcome: Unable to pan SVG if it is not zoomed and SVG remains centered. When zoomed, allow panning to its boundaries.
Problem:
Using this solution How to only allow pan within the bounds of the original SVG, it still allows you to pan when SVG is not zoomed and when zoomed doesn't allow panning to all boundaries
Using this solution How to only allow pan within the bounds of the original SVG, panning while zoomed works as expected but when the SVG is not zoomed, once I try to pan it snaps to the right and to the bottom instead of remaining centered.
let beforePan
beforePan = function (oldPan, newPan) {
let stopHorizontal = false,
stopVertical = false,
gutterWidth = this.getSizes().width,
gutterHeight = this.getSizes().height,
// Computed variables
sizes = this.getSizes(),
leftLimit = -((sizes.viewBox.x + sizes.viewBox.width) * sizes.realZoom) + gutterWidth,
rightLimit = sizes.width - gutterWidth - (sizes.viewBox.x * sizes.realZoom),
topLimit = -((sizes.viewBox.y + sizes.viewBox.height) * sizes.realZoom) + gutterHeight,
bottomLimit = sizes.height - gutterHeight - (sizes.viewBox.y * sizes.realZoom)
customPan = {}
customPan.x = Math.max(leftLimit, Math.min(rightLimit, newPan.x))
customPan.y = Math.max(topLimit, Math.min(bottomLimit, newPan.y))
return customPan
}
let panZoomController = svgPanZoom('#map', {
fit: 1,
center: true,
minZoom: 1,
zoomScaleSensitivity: 0.5,
beforePan: beforePan
});
The SVG (map) is inside a content div which is inside a wrapper div:
.wrapper {
margin: auto;
}
.content {
background-color: silver;
position: absolute;
inset: 0 0 0 0;
}
#map {
width: 100%;
height: 100%;
position: relative;
display: block;
}
After many hours and tries to make it work using only one calculation I decided to use a different calculation when the SVG was smaller than the viewport (no panning allowed). Code below:
beforePan = function (oldPan, newPan) {
let stopHorizontal = false,
stopVertical = false,
sizes = this.getSizes(),
customPan = {}
if (sizes.viewBox.width * sizes.realZoom < sizes.width || sizes.viewBox.height * sizes.realZoom < sizes.height) {
customPan.x = (window.visualViewport.width - (sizes.viewBox.width * sizes.realZoom)) / 2
customPan.y = (window.visualViewport.height - (sizes.viewBox.height * sizes.realZoom)) / 2
console.log(sizes.viewBox.width)
} else {
let gutterWidth = sizes.width,
gutterHeight = sizes.height,
leftLimit = -((sizes.viewBox.x + sizes.viewBox.width) * sizes.realZoom) + gutterWidth,
rightLimit = sizes.width - gutterWidth - (sizes.viewBox.x * sizes.realZoom),
topLimit = -((sizes.viewBox.y + sizes.viewBox.height) * sizes.realZoom) + gutterHeight,
bottomLimit = sizes.height - gutterHeight - (sizes.viewBox.y * sizes.realZoom)
customPan.x = Math.max(leftLimit, Math.min(rightLimit, newPan.x))
customPan.y = Math.max(topLimit, Math.min(bottomLimit, newPan.y))
}
return customPan
}

Tilt element pane on mousemove

I'm trying to redo the animation that I saw on a site where an image changes it's x and y values with the movement of the mouse. The problem is that the origin of the mouse is in the top left corner and I'd want it to be in the middle.
To understand better, here's how the mouse axis values work :
Now here's how I'd want it to be:
sorry for the bad quality of my drawings, hope you understand my point from those ^^
PS: I'm having a problem while trying to transform the x y values at the same time and I don't know why.
Here's what I wrote in JavaScript :
document.onmousemove = function(e){
var x = e.clientX;
var y = e.clientY;
document.getElementById("img").style.transform = "rotateX("+x*0.005+"deg)";
document.getElementById("img").style.transform = "rotateY("+y*0.005+"deg)";
}
The exact 3D effect you're up to is called "tilting".
Long story short, it uses CSS transform's rotateX() and rotateY() on a child element inside a perspective: 1000px parent. The values passed for the rotation are calculated from the mouse/pointer coordinates inside the parent Element and transformed to a respective degree value.
Here's a quick simplified remake example of the original script:
const el = (sel, par) => (par || document).querySelector(sel);
const elWrap = el("#wrap");
const elTilt = el("#tilt");
const settings = {
reverse: 0, // Reverse tilt: 1, 0
max: 35, // Max tilt: 35
perspective: 1000, // Parent perspective px: 1000
scale: 1, // Tilt element scale factor: 1.0
axis: "", // Limit axis. "y", "x"
};
elWrap.style.perspective = `${settings.perspective}px`;
const tilt = (evt) => {
const bcr = elWrap.getBoundingClientRect();
const x = Math.min(1, Math.max(0, (evt.clientX - bcr.left) / bcr.width));
const y = Math.min(1, Math.max(0, (evt.clientY - bcr.top) / bcr.height));
const reverse = settings.reverse ? -1 : 1;
const tiltX = reverse * (settings.max / 2 - x * settings.max);
const tiltY = reverse * (y * settings.max - settings.max / 2);
elTilt.style.transform = `
rotateX(${settings.axis === "x" ? 0 : tiltY}deg)
rotateY(${settings.axis === "y" ? 0 : tiltX}deg)
scale(${settings.scale})
`;
}
elWrap.addEventListener("pointermove", tilt);
/*QuickReset*/ * {margin:0; box-sizing: border-box;}
html, body { min-height: 100vh; }
#wrap {
height: 100vh;
display: flex;
background: no-repeat url("https://i.stack.imgur.com/AuRxH.jpg") 50% 50% / cover;
}
#tilt {
outline: 1px solid red;
height: 80vh;
width: 80vw;
margin: auto;
background: no-repeat url("https://i.stack.imgur.com/wda9r.png") 50% 50% / contain;
}
<div id="wrap"><div id="tilt"></div></div>
Regarding your code:
Avoid using on* event handlers (like onmousemove). Use EventTarget.addEventListener() instead — unless you're creating brand new Elements from in-memory. Any additionally added on* listener will override the previous one. Bad programming habit and error prone.
You cannot use style.transform twice (or more) on an element, since the latter one will override any previous - and the transforms will not interpolate. Instead, use all the desired transforms in one go, using Transform Matrix or by concatenating the desired transform property functions like : .style.transform = "rotateX() rotateY() scale()" etc.
Disclaimer: The images used in the above example from the original problem's reference website https://cosmicpvp.com might be subject to copyright. Here are used for illustrative and educative purpose only.
You can find out how wide / tall the screen is:
const width = window.innerWidth;
const height = window.innerHeight;
So you can find the centre of the screen:
const windowCenterX = width / 2;
const windowCenterY = height / 2;
And transform your mouse coordinates appropriately:
const transformedX = x - windowCenterX;
const transformedY = y - windowCenterY;
Small demo:
const coords = document.querySelector("#coords");
document.querySelector("#area").addEventListener("mousemove", (event)=>{
const x = event.clientX;
const y = event.clientY;
const width = window.innerWidth;
const height = window.innerHeight;
const windowCenterX = width / 2;
const windowCenterY = height / 2;
const transformedX = x - windowCenterX;
const transformedY = y - windowCenterY;
coords.textContent = `x: ${transformedX}, y: ${transformedY}`;
});
body, html, #area {
margin: 0;
width: 100%;
height: 100%;
}
#area {
background-color: #eee;
}
#coords {
position: absolute;
left: 10px;
top: 10px;
}
<div id="area"></div>
<div id="coords"></div>
I think I would use the bounding rect of the image to determine the center based on the image itself rather than the screen... something like this (using CSSVars to handle the transform)
const img = document.getElementById('fakeimg')
addEventListener('pointermove', handler)
function handler(e) {
const rect = img.getBoundingClientRect()
const x1 = (rect.x + rect.width / 2)
const y1 = (rect.y + rect.height / 2)
const x2 = e.clientX
const y2 = e.clientY
let angle = Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI) + 90
angle = angle < 0 ?
360 + angle :
angle
img.style.setProperty('--rotate', angle);
}
*,
*::before,
*::after {
box-sizeing: border-box;
}
html,
body {
height: 100%;
margin: 0
}
body {
display: grid;
place-items: center;
}
[id=fakeimg] {
width: 80vmin;
background: red;
aspect-ratio: 16 / 9;
--rotation: calc(var(--rotate) * 1deg);
transform: rotate(var(--rotation));
}
<div id="fakeimg"></div>

Mousemove event - div position based on mouse position is inverting

Edit - here is a fiddle: http://jsfiddle.net/cLahpqoj/1/ and here is an example of what i'm trying to do: https://www.seventh.tv/
perhaps my math on setting door position is bad and screwing things up. noticed the mouse postion and door position are changing places in values. logging:
{doorX: -1066, x: 396, doorY: -1367, y: 164}
{doorX: 396, x: -1065, doorY: 164, y: -1367}
from:
let mouseX = e.offsetX;
let mouseY = e.offsetY;
let doorX = currentDoorRect.x;
let doorY = currentDoorRect.y;
let x = doorX;
let y = doorY;
x = -( x + (mouseX) );
y = -( y + (mouseY) );
let transform = 'translate(' + x + 'px,' + y + 'px)';
currentDoor.style.transform = transform;
console.log( {
doorX,
x,
doorY,
y
});
I'm trying to move a div with the transform:translate(x,y) css property based on the current div position and the mouse position.
I'm logging the x and y values passed to the transform change and noticing for every value change, there is one proceeding one with something like x: -1, y:0 and then immediately back to what the value should be (i.e x:-563, y:424)
I'm using Vue.js and the function is :
<template>
<div ref="test-container" id="test-container" class="h-full relative"
#mousedown.prevent="disableMiddleClickScroll"
#mousemove="moveDoors"
#wheel="disableWheelScroll">
<div
:ref="`door-${index}`"
v-for="index in numDoors" :key="index"
class="door bg-gray-300 p-8 text-center rounded flex-none">
Door
</div>
</div>
</template>
...
moveDoors(e) {
let mouseX = e.offsetX;
let mouseY = e.offsetY;
let currentDoor = this.$refs[`door-1`];
let currentDoorRect = currentDoor.getBoundingClientRect();
let doorX = currentDoorRect.x;
let doorY = currentDoorRect.y;
let x = -( doorX + (mouseX) );
let y = -( doorY + (mouseY) );
console.log( {
x,
y
});
let transform = 'translate(' + x + 'px,' + y + 'px)';
currentDoor.style.transform = transform;
},
the event is called like this:
example of console log:
{x: 1, y: -0}
{x: -547, y: -426}
{x: 1, y: -0}
{x: -545, y: -426}
{x: -0, y: 1}
{x: -544, y: -425}
{x: -0, y: 1}
{x: -546, y: -424}
{x: -1, y: -0}
The container css is:
#test-container {
position: fixed;
left: 0%;
top: 0%;
right: 0%;
bottom: 0%;
}
Please let me know if there's not enough information to debug. I think this is all that is relevant.
If what you want is what this website is doing, then yes, your math is not the ones you need, and your strategy is also a bit off.
What they do there is a simple mapping from the viewport position to the content's one. You don't need to move each element on their own, but only the main container.
Basically, if you move your cursor to the center of the viewport, the content will move so that its center is at the center of the viewport. If you move it to the top left corner of the screen, then the content will move so that you see the top left corner of the content.
To do this, there are multiple strategies. One is to map the mouse position to a [0, 1] range, and then multiply this ratio by the content's size. You then just have to adjust for half the viewport size for the whole to be correctly centered.
const content = document.querySelector(".content");
document.onmousemove = (evt) => {
// the mouse position (in the viewport)
const viewport_x = evt.clientX;
const viewport_y = evt.clientY;
// the viewport size
const viewport_width = window.innerWidth;
const viewport_height = window.innerHeight;
// the mouse position, still relative to the viewport
// but in a range [0, 1]
const ratio_x = viewport_x / viewport_width;
const ratio_y = viewport_y / viewport_height;
// our content's size
const content_rect = content.getBoundingClientRect();
const content_width = content_rect.width;
const content_height = content_rect.height;
// transform the mouse position to the content's position
const content_x = content_width * ratio_x;
const content_y = content_height * ratio_y;
// remove it from half the size of the viewport
// so that aiming at the center of the viewport
// shows the center of our content
const translate_x = viewport_width / 2 - content_x;
const translate_y = viewport_height / 2 - content_y;
content.style.setProperty("--translate-x", translate_x + "px");
content.style.setProperty("--translate-y", translate_y + "px");
};
.content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
transition: transform .5s ease-out;
width: 150vw;
height: 250vh;
transform: translate(var(--translate-x), var(--translate-y));
--translate-x: -25vw; /* (150vw - 100vw) / 2 */
--translate-y: -75vh; /* (250vh - 100vw) / 2 */
}
.frame {
border: 1px solid;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
counter-increment: frame;
}
.frame::after {
content: counter(frame);
}
body, :root {
overflow: auto;
border: 0px;
}
<div class="content">
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
<div class="frame"></div>
</div>

how to animate a div within a boundry

To preface, this is my first time using JQuery so i dont really know the syntax and i'm a beginning programmer in javascript and I'm new to stackoverflow too. So apologies in advance.
Also apologies in advance if this has been asked before. This should be simple, yet somehow i can't figure it out and it's a little hard to search for.
Problem: I need my animation to be moving within a div boundry that it's in.
i tried changing the window in var h and var w to the id of my container, it doesn't work:
var h = $(#ghosts).height() - 75;
var w = $(#ghosts).height() - 75;
// html
<div id="playBoxProperties">
<div id="playBox"> // play area
<div id="ghosts"> // container for all 8 ghosts
<div id='g1' class="boo"> // one of the moving ghosts
</div>
</div>
</div>
</div>
// to randomize the movement
function randomPos() {
var h = $(window).height() - 75; // i need to change the window, to something else.
var w = $(window).width() - 75; // But i dont know what.
var newH = Math.floor(Math.random() * h);
var newW = Math.floor(Math.random() * w);
return [newH, newW];
}
// animation to move around
function animateDiv(divID) {
var newPos = randomPos();
$(divID).animate({ top: newPos[0], left: newPos[1] }, 4000, function () {
animateDiv(divID);
});
I expect it to be inside the black box
Subtract the currently iterating ghost element size from the random coordinate relative to the parent wrapper:
pass the parent and the animating child like randomPos($chi, $par)
Use Strings as your selectors. $(#ghosts); should be $('#ghosts');
Create a small jQuery plugin if you want: $.fn.animateGhosts. Use it like $ghosts.animateGhosts();
const rand = (min, max) => Math.random() * (max - min) + min;
const $ghostsWrapper = $('#ghosts');
const randomPos = ($chi, $par) => ({ // Randomize position
x: ~~(Math.random() * ($par.width() - $chi.width())),
y: ~~(Math.random() * ($par.height() - $chi.height()))
});
$.fn.animateGhosts = function() {
function anim() {
const pos = randomPos($(this), $ghostsWrapper);
$(this).stop().delay(rand(100, 500)).animate({
left: pos.x,
top: pos.y,
}, rand(1000, 4000), anim.bind(this));
}
return this.each(anim);
};
$('.boo').animateGhosts();
#ghosts {
position: relative;
height: 180px;
outline: 2px solid #000;
}
.boo {
position: absolute;
background: fuchsia;
width: 50px;
height: 50px;
}
<div id="ghosts">
<div class="boo">1</div>
<div class="boo">2</div>
<div class="boo">3</div>
<div class="boo">4</div>
<div class="boo">5</div>
<div class="boo">6</div>
</div>
<script src="//code.jquery.com/jquery-3.4.1.js"></script>
Better performance using CSS transition and translate
Here's an example that will use the power of CSS3 to move our elements in a hardware (GPU) accelerated fashion. Notice how, when slowing down, the elements are not zigzagging to the round pixel value (since jQuery animates top and left).
Instead we'll use transition for the CSS3 animation timing and translate(x, y) for the positions:
const rand = (min, max) => Math.random() * (max - min) + min;
const $ghostsWrapper = $('#ghosts');
const randomPos = ($chi, $par) => ({ // Randomize position
x: ~~(Math.random() * ($par.width() - $chi.width())),
y: ~~(Math.random() * ($par.height() - $chi.height()))
});
$.fn.animateGhosts = function() {
function anim() {
const pos = randomPos($(this), $ghostsWrapper);
$(this).css({
transition: `${rand(1, 4)}s ${rand(0.1, 0.4)}s ease`, // Speed(s) Pause(s)
transform: `translate(${pos.x}px, ${pos.y}px)`
}).one('transitionend', anim.bind(this));
}
return this.each(anim);
};
$('.boo').animateGhosts();
#ghosts {
position: relative;
height: 180px;
outline: 2px solid #000;
}
.boo {
position: absolute;
background: fuchsia;
width: 50px;
height: 50px;
}
<div id="ghosts">
<div class="boo">1</div>
<div class="boo">2</div>
<div class="boo">3</div>
<div class="boo">4</div>
<div class="boo">5</div>
<div class="boo">6</div>
</div>
<script src="//code.jquery.com/jquery-3.4.1.js"></script>

JS Canvas Zoom in and Zoom out translate doesn't settle to center

I'm working on my own canvas drawer project, and just stuck in the zoom in/out function. In my project I'm using scale and translate to make the zoom, as I want to keep all the canvas and its elements in the center.
After sketching a little bit(not a math genius), I succeeded to draw out the following formula to use in the translate process, so the canvas will be kept in the middle of its view port after zooming: Old width and height / 2 - New width and height(which are old width and height multiply by scale step, which is 1.1 in my case) / 2.
Logically speaking, that should make it work. But after trying few times the zoom in and zoom out, I can clearly see that the canvas has a little offset and it's not being centered to the middle of the viewport(by viewport I mean the stroked square representing the canvas).
I took my code out of my project and put it in fiddle, right here:
https://jsfiddle.net/s82qambx/3/
index.html
<div id="letse-canvas-container">
<canvas id="letse-canvas" width="300px" height="300px"></canvas>
<canvas id="letse-upper-canvas" width="300px" height="300px"></canvas>
</div>
<div id="buttons">
<button id="zoomin">
Zoom-in
</button>
<button id="zoomout">
Zoom-out
</button>
</div>
main.js
const canvas = {
canvas: document.getElementById('letse-canvas'),
upperCanvas: document.getElementById('letse-upper-canvas')
};
canvas.canvas.ctx = canvas.canvas.getContext('2d');
canvas.upperCanvas.ctx = canvas.upperCanvas.getContext('2d');
const CANVAS_STATE = {
canvas: {
zoom: 1,
width: 300,
height: 300
}
}
const Elements = [
{
x: 20,
y: 20,
width: 30,
height: 40
},
{
x:170,
y:30,
width: 100,
height: 100
}
];
const button = {
zoomin: document.getElementById('zoomin'),
zoomout: document.getElementById('zoomout')
}
button.zoomin.addEventListener('click', (e) => {
canvasZoomIn(e, canvas);
});
button.zoomout.addEventListener('click', (e) => {
canvasZoomOut(e, canvas);
});
function canvasZoomIn(e, canvas) {
const zoomData = getZoomData('in');
canvas.upperCanvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.upperCanvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.upperCanvas.ctx.clearRect(0, 0, 300, 300);
canvas.canvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.canvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.canvas.ctx.clearRect(0, 0, 300, 300);
Elements.forEach((element) => {
canvas.canvas.ctx.strokeRect(element.x, element.y, element.width, element.height);
});
CANVAS_STATE.canvas.zoom = zoomData.scale;
CANVAS_STATE.canvas.width = zoomData.docWidth;
CANVAS_STATE.canvas.height = zoomData.docHeight;
console.log(CANVAS_STATE.canvas.zoom, 'zoom');
console.log(CANVAS_STATE.canvas.width, 'width');
console.log(CANVAS_STATE.canvas.height, 'height');
canvas.canvas.ctx.strokeRect(0, 0, 300, 300);
canvas.canvas.ctx.beginPath();
canvas.canvas.ctx.moveTo(0, 150);
canvas.canvas.ctx.lineTo(300, 150);
canvas.canvas.ctx.stroke();
CANVAS_STATE.canvas.draggable = canvas.canvas.width < CANVAS_STATE.canvas.width || canvas.canvas.height < CANVAS_STATE.canvas.height;
}
function canvasZoomOut(e, canvas) {
const zoomData = getZoomData('out');
canvas.upperCanvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.upperCanvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.upperCanvas.ctx.clearRect(0, 0, canvas.canvas.width, canvas.canvas.height);
canvas.canvas.ctx.scale(zoomData.zoomStep, zoomData.zoomStep);
canvas.canvas.ctx.translate(zoomData.translateX, zoomData.translateY);
canvas.canvas.ctx.clearRect(0, 0, canvas.canvas.width, canvas.canvas.height);
Elements.forEach((element) => {
canvas.canvas.ctx.strokeRect(element.x, element.y, element.width, element.height);
});
CANVAS_STATE.canvas.zoom = zoomData.scale;
CANVAS_STATE.canvas.width = zoomData.docWidth;
CANVAS_STATE.canvas.height = zoomData.docHeight;
console.log(CANVAS_STATE.canvas.zoom, 'zoom');
console.log(CANVAS_STATE.canvas.width, 'width');
console.log(CANVAS_STATE.canvas.height, 'height');
canvas.canvas.ctx.strokeRect(0, 0, 300, 300);
canvas.canvas.ctx.beginPath();
canvas.canvas.ctx.moveTo(0, 150);
canvas.canvas.ctx.lineTo(300, 150);
canvas.canvas.ctx.stroke();
CANVAS_STATE.canvas.draggable = canvas.canvas.width < CANVAS_STATE.canvas.width || canvas.canvas.height < CANVAS_STATE.canvas.height;
}
function getZoomData(zoom) {
const zoomStep = zoom === 'in' ? 1.1 : 1 / 1.1;
const scale = CANVAS_STATE.canvas.zoom * zoomStep;
const docWidth = CANVAS_STATE.canvas.width * zoomStep;
const docHeight = CANVAS_STATE.canvas.height * zoomStep;
const translateX = CANVAS_STATE.canvas.width / 2 - docWidth / 2;
const translateY = CANVAS_STATE.canvas.height / 2 - docHeight / 2;
console.log(zoomStep);
console.log(scale, 'check');
console.log(docWidth);
console.log(docHeight);
console.log(translateX, 'check');
console.log(translateY, 'check');
return {
zoomStep,
scale,
docWidth,
docHeight,
translateX,
translateY
};
}
main.css
#letse-canvas-container {
position: relative;
float: left;
}
#letse-canvas {
border: 1px solid rgb(0, 0, 0);
/* visibility: hidden; */
}
#letse-upper-canvas {
/* position: absolute; */
/* top: 0px; */
left: 0px;
border: 1px solid;
/* visibility: hidden; */
}
Can someone suggest a reason? What am I missing here?
OK! So I managed to derive the right formula after searching in the net and testing few options. I used:
function getZoomData(zoom) {
const zoomStep = zoom === 'in' ? 1.1 : 1 / 1.1;
const oldZoom = CANVAS_STATE.canvas.zoom;
const newZoom = oldZoom * zoomStep;
const zoomDifference = newZoom - oldZoom;
const docWidth = CANVAS_STATE.canvas.width * newZoom;
const docHeight = CANVAS_STATE.canvas.height * newZoom;
const translateX = (-(canvas.canvas.width / 2 * zoomDifference / newZoom));
const translateY = (-(canvas.canvas.height / 2 * zoomDifference / newZoom));

Categories