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>
CONTEXT :
My requirement is to do image transformations like scale, rotate and translate with user actions on an image.
Rotation and translation of image is done in its center using transform origin 50% 50% ( default ).
For scaling the image on mouse point using mouse wheel, i was able to make it work with transform origin 0 0
Just to accomodate the scale behaviour if i change the origin, i am required to recalculate other transformations relative to the new origin.
PROBLEM :
How to scale the image on mouse point if the transform origin is defaulted to 50% 50% by default ?
The following fiddle works with transform origin 0 0 for reference. I needhelp in optimizing this calculations using transform origin 50% 50%.
Can someone assist in optimizing this calculation further so that it works with default transform origin ?
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.container {
background-color: lightgrey;
}
.stage {
height: 100%;
width: 100%;
overflow: hidden;
}
#image {
transform-origin: 0 0;
height: auto;
width: 80%;
cursor: grab;
}
.actions {
display: flex;
position: absolute;
bottom: 0;
left: 0;
height: 1.5rem;
width: 100%;
background-color: lightgrey;
}
.action {
margin-right: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="stage">
<img id="image" src="https://cdn.pixabay.com/photo/2018/01/14/23/12/nature-3082832__480.jpg" />
</div>
<div class="actions">
<div class="action">
<label for="rotate">Rotate </label>
<input type="range" id="rotate" name="rotate" min="0" max="360">
</div>
</div>
</div>
<script>
const img = document.getElementById('image');
const rotate = document.getElementById('rotate');
let mouseX;
let mouseY;
let mouseTX;
let mouseTY;
let startXOffset = 222.6665;
let startYOffset = 224.713;
let startX = 0;
let startY = 0;
let panning = false;
const ts = {
scale: 1,
rotate: 0,
translate: {
x: 0,
y: 0
}
};
rotate.oninput = function(event) {
event.preventDefault();
ts.rotate = event.target.value;
setTransform();
};
img.onwheel = function(event) {
event.preventDefault();
let xs = (event.clientX - ts.translate.x) / ts.scale;
let ys = (event.clientY - ts.translate.y) / ts.scale;
let delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
ts.scale = (delta > 0) ? (ts.scale * 1.2) : (ts.scale / 1.2);
ts.translate.x = event.clientX - xs * ts.scale;
ts.translate.y = event.clientY - ys * ts.scale;
setTransform();
};
img.onmousedown = function(event) {
event.preventDefault();
panning = true;
img.style.cursor = 'grabbing';
mouseX = event.clientX;
mouseY = event.clientY;
mouseTX = ts.translate.x;
mouseTY = ts.translate.y;
};
img.onmouseup = function(event) {
panning = false;
img.style.cursor = 'grab';
};
img.onmousemove = function(event) {
event.preventDefault();
const x = event.clientX;
const y = event.clientY;
pointX = (x - startX);
pointY = (y - startY);
if (!panning) {
return;
}
ts.translate.x =
mouseTX + (x - mouseX);
ts.translate.y =
mouseTY + (y - mouseY);
setTransform();
};
function setTransform() {
const steps = `translate(${ts.translate.x}px,${ts.translate.y}px) scale(${ts.scale}) rotate(${ts.rotate}deg)`;
img.style.transform = steps;
}
</script>
</body>
</html>
How to scale the image on mouse point if the transform origin is defaulted to 50% 50% by default ?
To shift origin to 50% 50% we need x,y position of mouse, relative to the image i.e. image top left as origin till image bottom right. Then we compensate image position by using the relative coordinates. We need to consider image dimensions as well.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<style>
.container {
background-color: lightgrey;
}
.stage {
height: 90vh;
width: 100%;
overflow: hidden;
}
#image {
transform-origin: 50% 50%;
height: auto;
width: 40%;
cursor: grab;
}
.actions {
display: flex;
position: absolute;
bottom: 0;
left: 0;
height: 1.5rem;
width: 100%;
background-color: lightgrey;
}
.action {
margin-right: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="stage">
<img id="image" src="https://cdn.pixabay.com/photo/2018/01/14/23/12/nature-3082832__480.jpg" />
</div>
<div class="actions">
<div class="action">
<label for="rotate">Rotate </label>
<input type="range" id="rotate" name="rotate" min="0" max="360">
<button onclick="reset()">Reset All</button>
</div>
</div>
</div>
<script>
const img = document.getElementById('image');
const rotate = document.getElementById('rotate');
let mouseX;
let mouseY;
let mouseTX;
let mouseTY;
let startXOffset = 222.6665;
let startYOffset = 224.713;
let startX = 0;
let startY = 0;
let panning = false;
const ts = {
scale: 1,
rotate: 0,
translate: {
x: 0,
y: 0
}
};
rotate.oninput = function(event) {
event.preventDefault();
ts.rotate = event.target.value;
setTransform();
};
img.onwheel = function(event) {
event.preventDefault();
//need more handling to avoid fast scrolls
var func = img.onwheel;
img.onwheel = null;
let rec = img.getBoundingClientRect();
let x = (event.clientX - rec.x) / ts.scale;
let y = (event.clientY - rec.y) / ts.scale;
let delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
ts.scale = (delta > 0) ? (ts.scale + 0.2) : (ts.scale - 0.2);
//let m = (ts.scale - 1) / 2;
let m = (delta > 0) ? 0.1 : -0.1;
ts.translate.x += (-x * m * 2) + (img.offsetWidth * m);
ts.translate.y += (-y * m * 2) + (img.offsetHeight * m);
setTransform();
img.onwheel = func;
};
img.onmousedown = function(event) {
event.preventDefault();
panning = true;
img.style.cursor = 'grabbing';
mouseX = event.clientX;
mouseY = event.clientY;
mouseTX = ts.translate.x;
mouseTY = ts.translate.y;
};
img.onmouseup = function(event) {
panning = false;
img.style.cursor = 'grab';
};
img.onmousemove = function(event) {
event.preventDefault();
let rec = img.getBoundingClientRect();
let xx = event.clientX - rec.x;
let xy = event.clientY - rec.y;
const x = event.clientX;
const y = event.clientY;
pointX = (x - startX);
pointY = (y - startY);
if (!panning) {
return;
}
ts.translate.x =
mouseTX + (x - mouseX);
ts.translate.y =
mouseTY + (y - mouseY);
setTransform();
};
function setTransform() {
const steps = `translate(${ts.translate.x}px,${ts.translate.y}px) scale(${ts.scale}) rotate(${ts.rotate}deg) translate3d(0,0,0)`;
//console.log(steps);
img.style.transform = steps;
}
function reset() {
ts.scale = 1;
ts.rotate = 0;
ts.translate = {
x: 0,
y: 0
};
rotate.value = 180;
img.style.transform = 'none';
}
setTransform();
</script>
</body>
</html>
To make things simple I am adding 0.2 to the scale instead of multiplying with 1.2. You can change it back with right proportions. Also added a reset button.
Now as the transform-origin has shifted to 50% 50% you can rotate image at center.
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>
I wanna do some effect to my background so i tought i can add some shapes flying around but i do only 1 shape i cant loop or anything
i tried call function more than a one time but it doesnt help
function animasyon(a) {
window.onload=function(){
var id = setInterval(anim,5);
$('body').append('<div class=shape></div>');
$('body').append('<div class=shape></div>');
var kutu = document.getElementsByClassName("shape");
var pos = 0;
var x = window.innerWidth;
var y = window.innerHeight;
var borderSize = Math.floor(Math.random() * 3 ) + 1;
var ySize = Math.floor(Math.random() * y ) + 1;
var Size = Math.floor(Math.random() * 30 ) + 5;
var yon = Math.floor(Math.random() *2)+1;
var dolu = Math.floor(Math.random() *2)+1;
if (ySize > 50) { ySize-=20; }
function anim(){
if (pos == x) {
clearInterval(id);
document.getElementById("shape").remove();
}else{
pos++;
kutu[a].style.position = "absolute";
kutu[a].style.border = "solid rgb(119,38,53) "+borderSize+"px";
kutu[a].style.left = pos+"px";
kutu[a].style.width = Size+"px";
kutu[a].style.height = Size+"px";
if (yon == 1) { ySize-=0.2; } else { ySize+=0.2; }
if (dolu==1) {kutu[a].style.background = "rgb(119,38,53)";}
if (kutu[a].offsetTop < 0 || kutu[a].offsetTop > y-30) {document.getElementById("shape").remove();}
kutu[a].style.top = ySize+"px";
}
}
}
}
animasyon(0);
Try Calling 'anim' function in this way
setInterval(function(){anim()},5);
Your problem is that you are assigning window.onload function a value inside the function animasyon
window.onload holds only 1 function. If you call animation function more than once, then the last one will overwrite the first one.
Edit: You need to separate your animation logic from the page load logic. They are completely separate things.
Here is an example of adding multiple objects and animating each separately.
// HTML
<div id="container">
<div id="ball"></div>
<div id="ball2"></div>
<div id="ball3"></div>
<div id="ball4"></div>
</div>
// CSS
#ball, #ball2, #ball3, #ball4 {
background: red;
border: 1px solid #FAFDFA;
display: inline-block;
width: 1em;
height: 1em;
border-radius: 2em;
position: absolute;
}
#ball2 {
background: blue;
}
#ball3 {
background: green;
}
#ball4 {
background: purple;
}
#container {
width: 512px;
height: 512px;
background: black;
position: relative;
}
// JS
const container = document.getElementById('container');
const stageWidth = 512;
const stageHeight = 512;
const makeFall = (elementId) => {
// this function makes an enlement fall in random direction
const animationSpeed = 4;
// your onload function
const ball = document.getElementById(elementId);
let directionX = (Math.random() * animationSpeed * 2) - animationSpeed;
let directionY = Math.random() * animationSpeed;
const setRandomStart = () => {
ball.style.top = '10px';
ball.style.left = (Math.random() * (stageWidth / 2)) + 'px';
directionX = (Math.random() * animationSpeed * 2) - animationSpeed;
directionY = Math.random() * animationSpeed;
}
setRandomStart();
let animationInterval = setInterval(() => {
let px = parseFloat(ball.style.left);
let py = parseFloat(ball.style.top);
px += directionX;
py += directionY;
if (px > stageWidth - 20 || py > stageHeight - 20 || px < -20) {
setRandomStart();
} else {
ball.style.left = px + 'px';
ball.style.top = py + 'px';
}
}, 48);
}
// In Your onload function you can add the elements and then animate
makeFall('ball');
makeFall('ball2');
makeFall('ball3');
makeFall('ball4');
https://jsfiddle.net/753oL8re/4/
I used interact.js library to write this piece of code which works absolutely fine standalone on chrome, firefox and w3schools "Try it Yourself" (doesn't work on Edge and IE for some reason). The problem is that when I call a template.phtml with this code inside from the layout.xml, the magento renders it only once, thus the user is not allowed to resize the cubes.
<!-- CSS -->
<style type="text/css">
svg {
width: 100%;
height: 300px;
background-color: #CDC9C9;
-ms-touch-action: none;
touch-action: none;
}
.edit-rectangle {
fill: black;
stroke: #fff;
}
body { margin: 0; }
</style>
<!-- Content -->
<br>
<svg>
</svg>
<br>
<button onclick="location.href = 'square';" id="previousbutton">Go back</button>
<button onclick="location.href = 'squaresection';" style="float:right" id="nextButton">Proceed to next step</button>
<br>
<br>
<script type="text/javascript" src="interact.js">
</script>
<!-- JavaScript -->
<script type="text/javascript">
var svgCanvas = document.querySelector('svg'),
svgNS = 'http://www.w3.org/2000/svg',
rectangles = [];
labels = [];
rectNumb = 5;
function Rectangle (x, y, w, h, svgCanvas) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.stroke = 0;
this.el = document.createElementNS(svgNS, 'rect');
this.el.setAttribute('data-index', rectangles.length);
this.el.setAttribute('class', 'edit-rectangle');
rectangles.push(this);
this.draw();
svgCanvas.appendChild(this.el);
}
function Label (x, y, text, svgCanvas){
this.x = x;
this.y = y;
this.text = text;
this.el = document.createElementNS(svgNS, 'text');
labels.push(this);
this.draw();
svgCanvas.appendChild(this.el);
}
Label.prototype.draw = function () {
this.el.setAttribute('x', this.x);
this.el.setAttribute('y', this.y);
this.el.setAttribute('font-family', "Verdana");
this.el.setAttribute('font-size', 14);
this.el.setAttribute('fill', "black");
this.el.innerHTML = this.text;
}
Rectangle.prototype.draw = function () {
this.el.setAttribute('x', this.x + this.stroke / 2);
this.el.setAttribute('y', this.y + this.stroke / 2);
this.el.setAttribute('width' , this.w - this.stroke);
this.el.setAttribute('height', this.h - this.stroke);
this.el.setAttribute('stroke-width', this.stroke);
}
interact('.edit-rectangle')
// change how interact gets the
// dimensions of '.edit-rectangle' elements
.rectChecker(function (element) {
// find the Rectangle object that the element belongs to
var rectangle = rectangles[element.getAttribute('data-index')];
// return a suitable object for interact.js
return {
left : rectangle.x,
top : rectangle.y,
right : rectangle.x + rectangle.w,
bottom: rectangle.y + rectangle.h
};
})
/*
.draggable({
max: Infinity,
onmove: function (event) {
var rectangle = rectangles[event.target.getAttribute('data-index')];
rectangle.x += event.dx;
rectangle.y += event.dy;
rectangle.draw();
}
})
*/
.resizable({
onstart: function (event) {},
onmove : function (event) {
if (event.target.getAttribute('data-index') > 0)
{
// Main Rect
var rectangle = rectangles[event.target.getAttribute('data-index')];
var rectangle2 = rectangles[event.target.getAttribute('data-index') - 1];
if (rectangle.w - event.dx > 10 && rectangle2.w + event.dx > 10){
rectangle.x += event.dx;
rectangle.w = rectangle.w - event.dx;
rectangle2.w = rectangle2.w + event.dx;
}
rectangle.draw();
rectangle2.draw();
var label = labels[event.target.getAttribute('data-index')];
var label2 = labels[event.target.getAttribute('data-index') - 1];
label.text = rectangle.w + " mm";
label2.text = rectangle2.w + " mm";
label.x = rectangle.x + rectangle.w / 4;
label2.x = rectangle2.x + rectangle2.w / 4;
label.draw();
label2.draw();
}
},
onend : function (event) {},
edges: {
top : false, // Disable resizing from top edge.
left : true,
bottom: false,
right : false // Enable resizing on right edge
},
inertia: false,
// Width and height can be adjusted independently. When `true`, width and
// height are adjusted at a 1:1 ratio.
square: false,
// Width and height can be adjusted independently. When `true`, width and
// height maintain the aspect ratio they had when resizing started.
preserveAspectRatio: false,
// a value of 'none' will limit the resize rect to a minimum of 0x0
// 'negate' will allow the rect to have negative width/height
// 'reposition' will keep the width/height positive by swapping
// the top and bottom edges and/or swapping the left and right edges
invert: 'reposition',
// limit multiple resizes.
// See the explanation in the #Interactable.draggable example
max: Infinity,
maxPerElement: 3,
});
interact.maxInteractions(Infinity);
var positionX = 50,
positionY = 80,
width = 80,
height = 80;
for (var i = 0; i < rectNumb; i++) {
positionX = 50 + 82 * i;
new Rectangle(positionX, positionY, width, height, svgCanvas);
}
for (var i = 0; i < rectNumb; i++) {
positionX = 50 + 82 * i;
new Label(positionX + width/4, positionY + height + 20, width +" mm", svgCanvas);
}
</script>
Any suggestions of what I could do to implement this code into magento would be much appreciated.
Magento did not render the code only once. The problem was that canvas event listener always assumed that pointer coordinates were wrong. Since canvas is the first element of the page(because it is the first element in that .phtml file), event listener assumed it will be displayed at the top, but that was not the case because of the way magento page rendering works.
This issue was resolved simply by measuring the height of content above canvas and just mathematically subtracting that from pointers position before passing it to event listener.
The problem with this solution is that it works only for single page or with multiple pages that have the same height of content above canvas(=>same design). If anyone knows a way in which person would not need to "recalculate" the height for every single page that has different design, sharing knowledge would be much appreciated.