I'm trying to apply a 3d rotation hover effect on the child elements of a container, but I'm having difficulty to make it work with for loop. Code works fine when I remove the loop and [i] tags, and use the effect on a single element, but it doesn't when I try to apply the effect on all child elements. Do you know what part I'm doing wrong?
let rotate = document.getElementsByClassName("group")[0].getElementsByTagName("a");
var i;
for (i=0; i<rotate.length; i++) {
const height = rotate[i].clientHeight;
const width = rotate[i].clientWidth;
rotate[i].addEventListener('mousemove', handleMove)
function handleMove(e) {
const xVal = e.layerX;
const yVal = e.layerY;
const yRotation = 20 * ((xVal - width / 2) / width);
const xRotation = -20 * ((yVal - height / 2) / height);
const string = 'perspective(500px) scale(1.1) rotateX(' + xRotation + 'deg) rotateY(' + yRotation + 'deg)';
rotate[i].style.transform = string;
}
rotate[i].addEventListener('mouseout', function() {rotate[i].style.transform = 'perspective(500px) scale(1) rotateX(0) rotateY(0)'})
rotate[i].addEventListener('mousedown', function() {rotate[i].style.transform = 'perspective(500px) scale(0.9) rotateX(0) rotateY(0)'})
rotate[i].addEventListener('mouseup', function() {rotate[i].style.transform = 'perspective(500px) scale(1.1) rotateX(0) rotateY(0)'})
}
https://codepen.io/technokami/pen/abojmZa
You can use querySelectorAll() to get an array of selected elements (use css selector) then use forEach() loop on your previously created array.
Here's a working demo.
/* Get elements and loop on*/
let elements = document.querySelectorAll(".group > a");
elements.forEach((el) => {
/* Get the height and width of the element */
const height = el.clientHeight;
const width = el.clientWidth;
/*
* Add a listener for mousemove event
* Which will trigger function 'handleMove'
* On mousemove
*/
el.addEventListener("mousemove", handleMove);
/* Define function a */
function handleMove(e) {
/*
* Get position of mouse cursor
* With respect to the element
* On mouseover
*/
/* Store the x position */
const xVal = e.layerX;
/* Store the y position */
const yVal = e.layerY;
/*
* Calculate rotation valuee along the Y-axis
* Here the multiplier 20 is to
* Control the rotation
* You can change the value and see the results
*/
const yRotation = 20 * ((xVal - width / 2) / width);
/* Calculate the rotation along the X-axis */
const xRotation = -20 * ((yVal - height / 2) / height);
/* Generate string for CSS transform property */
const string =
"perspective(500px) scale(1.1) rotateX(" +
xRotation +
"deg) rotateY(" +
yRotation +
"deg)";
/* Apply the calculated transformation */
el.style.transform = string;
}
/* Add listener for mouseout event, remove the rotation */
el.addEventListener("mouseout", function() {
el.style.transform = "perspective(500px) scale(1) rotateX(0) rotateY(0)";
});
/* Add listener for mousedown event, to simulate click */
el.addEventListener("mousedown", function() {
el.style.transform = "perspective(500px) scale(0.9) rotateX(0) rotateY(0)";
});
/* Add listener for mouseup, simulate release of mouse click */
el.addEventListener("mouseup", function() {
el.style.transform = "perspective(500px) scale(1.1) rotateX(0) rotateY(0)";
});
});
html {
display: flex;
justify-content: center;
align-items: center;
align-contents: center;
height: 100%;
}
/* Styling purpose */
.group {
display: block
}
/* Styles for the tilt block */
.group>a {
display: inline-block;
height: 200px;
width: 300px;
background-color: grey;
margin: 0 auto;
transition: box-shadow 0.1s, transform 0.1s;
/*
* Adding image to the background
* No relation to the hover effect.
*/
background-image: url(http://unsplash.it/300/200);
background-size: 100%;
background-repeat: no-repeat;
}
.group a:hover {
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.6);
cursor: pointer;
}
<div class="group">
<a href="#" target="_blank">
<!-- Container for our block -->
</a>
<a href="#" target="_blank">
<!-- Container for our block -->
</a>
</div>
<div class="group">
<a href="#" target="_blank">
<!-- Container for our block -->
</a>
<a href="#" target="_blank">
<!-- Container for our block -->
</a>
</div>
Instead of trying to retrieve data using an array and index (which have problems with scope) You should query the element itself when the event is handled. The below code does this via the this keyword, which in this context, with an event handler added via code, can be considered to refer to the element which triggered the event currently being handled.
So, I've grabbed the element's width and height each time the mousemove handler fires and in the other 3 handlers, i've used this in-place of self.rotate[i].
"use strict";
function qsa(sel,parent=document){return parent.querySelectorAll(sel)}
window.addEventListener('load', onLoaded, false);
function onLoaded(evt)
{
let targets = qsa('div');
targets.forEach(
function(curTgt, index, collection)
{
curTgt.addEventListener('mousemove', onMouseMove);
curTgt.addEventListener('mouseout', function(evt) {this.style.transform = 'perspective(500px) scale(1) rotateX(0) rotateY(0)'})
curTgt.addEventListener('mousedown', function(evt) {this.style.transform = 'perspective(500px) scale(0.9) rotateX(0) rotateY(0)'})
curTgt.addEventListener('mouseup', function(evt) {this.style.transform = 'perspective(500px) scale(1.1) rotateX(0) rotateY(0)'})
}
);
function onMouseMove(evt)
{
let height = this.clientHeight;
let width = this.clientWidth;
let xVal = evt.layerX;
let yVal = evt.layerY;
let yRotation = 20 * ((xVal - width / 2) / width);
let xRotation = -20 * ((yVal - height / 2) / height);
let string = 'perspective(500px) scale(1.1) rotateX(' + xRotation + 'deg) rotateY(' + yRotation + 'deg)';
this.style.transform = string;
}
}
div
{
display: inline-block;
border: solid 1px #999;
padding: 16px;
margin: 8px;
}
<div>2</div><div>1</div><div>4</div><div>3</div><div>6</div>
Related
The problem is solved when adding angles individually and then using ttheta(without calling a function to add angles and than using ttheta), but can anyone tell about why using function here is wrong or what problem is this function causing
The issue is solved by using this:
dtransform = window.getComputedStyle(leg1, null).getPropertyValue("transform");
values = dtransform.split('(')[1].split(')')[0].split(',');
dtheta = Math.round(Math.atan2(values[1], values[0]) * (180 / Math.PI));
dtransform1 = window.getComputedStyle(leg2, null).getPropertyValue("transform");
values1 = dtransform1.split('(')[1].split(')')[0].split(',');
dtheta1 = Math.round(Math.atan2(values1[1], values1[0]) * (180 / Math.PI));
ttheta = dtheta + dtheta1;
Instead of using function.
What I am trying to achieve is to get value of end points of an element when it is rotated from left and top of browser.
X & Y values are max-distance of end points of shoe
I get right values at some points and wrong at some. I tried to add angle from the parent element but that also don't solve the problem.
This is the related answer from which I had taken help
To check values are right or wrong I added an event to get clientX of mouse click. And values of element positions are taken when Try button is clicked.
Am I doing something wrong, any insights will be really helpful
let leg1 = document.querySelector(".Leg1Shoe")
let leg2 = document.querySelector(".Leg1Part")
let animeAll = document.querySelectorAll(".allClass")
let animePause = false
let ttheta = 0;
function getPos() {
if (!animePause) {
animeAll.forEach(e => {
e.classList.add("AnimatePaused");
})
animePause = true;
} else {
animeAll.forEach(e => {
e.classList.remove("AnimatePaused");
})
animePause = false;
}
let h, w, x, dx, tx, y, dy, ty = "";
leg1.style.outline = "1px solid red"
h = leg1.offsetHeight;
w = leg1.offsetWidth;
x = leg1.getBoundingClientRect().left;
y = leg1.getBoundingClientRect().top;
func2(leg2);
func2(leg1);
dx = (Number(h * (Math.sin(ttheta * (Math.PI / 180)))) + Number(w * (Math.cos(ttheta * (Math.PI / 180))))).toFixed(2);
dy = (Number(w * (Math.sin(ttheta * (Math.PI / 180)))) + Number(h * (Math.cos(ttheta * (Math.PI / 180))))).toFixed(2);
tx = (Number(x) + Number(Math.abs(dx))).toFixed(2);
ty = (Number(y) + Number(Math.abs(dy))).toFixed(2);
console.log("X:" + tx, "Y:" + ty);
}
function func2(e) {
let dtransform, dtheta, values = "";
dtransform = window.getComputedStyle(e, null).getPropertyValue("transform");
if (dtransform != "none") {
values = dtransform.split('(')[1].split(')')[0].split(',');
dtheta = Math.round(Math.atan2(values[1], values[0]) * (180 / Math.PI));
} else {
dtheta = 0;
};
ttheta = Number(ttheta) + Number(dtheta);
}
leg1.addEventListener('click', mousePos);
function mousePos(e) {
console.log("X:" + e.clientX, "Y:" + e.clientY)
}
.Leg1Part {
position: relative;
left: 100px;
top: 43px;
width: 20px;
height: 75px;
background-color: green;
transform-origin: top center;
animation: animateLeg1Part 5.0s linear infinite alternate;
}
#keyframes animateLeg1Part {
0% {
transform: rotate(40deg);
}
25% {
transform: rotate(25deg);
}
50% {
transform: rotate(10deg);
}
75% {
transform: rotate(30deg);
}
100% {
transform: rotate(60deg);
}
}
.Leg1Shoe {
position: absolute;
left: 0px;
top: 73px;
width: 40px;
height: 20px;
background-color: blue;
transform-origin: center left;
animation: animateLeg1Shoe 5.0s linear infinite alternate;
}
#keyframes animateLeg1Shoe {
0% {
transform: rotate(15deg);
}
50% {
transform: rotate(-20deg);
}
100% {
transform: rotate(30deg);
}
}
.AnimatePaused {
animation-play-state: paused;
}
<div class="Leg1Part allClass">
<div class="Leg1Shoe allClass"></div>
</div>
<button onclick="getPos()">Try</button>
Thanks for help in advance
This is not a simple answer that can give you a complete solution, but just an outline of the process you can follow.
You get the transformation matrices for the "leg" and the "shoe", as you already do, by calling getPropertyValue("transform") This gives you a string like this: matrix(-0.568718, 0.822533, -0.822533, -0.568718, 0, 0), this is a shortened form of a 3x3 transformation matrix:
| cos(theta) -sin(theta) 0 |
| sin(theta) cos(theta) 0 |
| 0 0 1 |
Parse the string and create a 2d array for this matrix. Note: since you don't have any translation (two zeros in the last column) you can operate on 2x2 matrices.
Multiple the transformation matrices for the "leg" and the "shoe". It's a tedious process, and you can use the mathjs library to help.
Multiply the resulting transformation matrix by the vector of each point's original coordinates. This will give you the coordinates of the point with all rotations applied.
Here are some references:
https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix()
https://en.wikipedia.org/wiki/Affine_transformation
http://web.cse.ohio-state.edu/~shen.94/681/Site/Slides_files/transformation_review.pdf
https://en.wikipedia.org/wiki/Matrix_multiplication_algorithm
I am trying to draw a red rectangle on image but somehow my rectangle is NOT starting from coordinates where I click with my mouse. Here is the code:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let img = new Image();
let fileName = "";
let rect = {};
let drag = false;
// Add image to canvas
reader.addEventListener(
"load",
() => {
// Create image
img = new Image();
// Set image src
img.src = reader.result;
// On image load add to canvas
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
canvas.removeAttribute("data-caman-id");
};
canvas.addEventListener('mousedown', mouseDown, false);
canvas.addEventListener('mouseup', mouseUp, false);
canvas.addEventListener('mousemove', mouseMove, false);
},
false
);
});
function mouseDown(e) {
rect.startX = e.pageX - this.offsetLeft;
rect.startY = e.pageY - this.offsetTop;
drag = true;
}
function mouseUp() { drag = false; }
function mouseMove(e) {
if (drag) {
ctx.clearRect(0, 0, 500, 500);
ctx.drawImage(img, 0, 0);
rect.w = (e.pageX - this.offsetLeft) - rect.startX;
rect.h = (e.pageY - this.offsetTop) - rect.startY;
ctx.strokeStyle = 'red';
ctx.strokeRect(rect.startX, rect.startY, rect.w, rect.h);
console.log("rect.startX: " + rect.startX + " - rect.startY: " +rect.startY)
}
}
And here is what I get in return:
I cannot show my mouse position when I take screenshot so I've labeled it on the image. Basically, I start from top left corner and rectangle seems to start drawing itself from bottom right corner. What am I doing wrong?
OK my answer is more related to the (original) question title and a little bit of jQuery fun.
Your (original) question code involves canvas and no jQuery code, just javascript.
This answer is more just a primitive jQuery approach.
Here is a very rough concept below using jQuery .on events for handling...
mousedown event to start drawing the rectangle
resize event for updating image size and offsets
mousemove event for updating rectangle draw positions
mouseup event for setting rectangle in set position
All of the above .on events (excluding resize event) run in document selector .area, which has a child img element in this example below.
Please note I have not coded this example for multiple usages of .area div, you will need go even deeper with the script logic in my example to accommodate multiple usages of .area div.
My script example code below may inspire you to get to where you want, may not.
The main key thing I am doing is converting pixel cursor positions within the image into a percentage integer. Using percentages as xy positions means the rendered result will be the same if the window or .area is resized responsively (with no javascript).
Here is a jQuery and CSS fun example below.
jsFiddle version: https://jsfiddle.net/joshmoto/w5bcx9o8/2/
See comments in code to understand what is happening...
// our updatable variable objects to use globally
let img = {};
let position = {};
// image matrix function to update img object variable
function imgMatrix() {
// our image object inside area
let $img = $('IMG', '.area');
// offset data of image
let offset = $img.offset();
// add/update object key data
img.width = $img.outerWidth();
img.height = $img.outerHeight();
img.offsetX = offset.left - $(document).scrollLeft();
img.offsetY = offset.top - $(document).scrollTop();
}
// position matrix function to update position object variable
function positionMatrix(e, mousedown = false) {
// if mousedown param is true... for use in
if (mousedown) {
// set the top/left position object data with percentage position
position.top = (100 / img.height) * ( (e.pageY - $(document).scrollTop()) - img.offsetY);
position.left = (100 / img.width) * ( (e.pageX - $(document).scrollLeft()) - img.offsetX);
}
// set the right/bottom position object data with percentage position
position.right = 100 - ((100 / img.width) * ((e.pageX - $(document).scrollLeft()) - img.offsetX));
position.bottom = 100 - ((100 / img.height) * ((e.pageY - $(document).scrollTop()) - img.offsetY));
}
// mouse move event function in area div
$(document).on('mousemove', '.area', function(e) {
// update img object variable data upon this mousemove event
imgMatrix();
// if this area has draw class
if ($(this).hasClass('draw')) {
// update position object variable data passing current event data
positionMatrix(e);
// if image x cursor drag position percent is negative to mousedown x position
if ((100 - position.bottom) < position.top) {
// update rectange x negative positions css
$('.rect', this).css({
top: (100 - position.bottom) + '%',
bottom: (100 - position.top) + '%'
});
// else if image x cursor drag position percent is positive to mousedown x position
} else {
// update rectange x positive positions css
$('.rect', this).css({
bottom: position.bottom + '%',
top: position.top + '%',
});
}
// if image y cursor drag position percent is negative to mousedown y position
if ((100 - position.right) < position.left) {
// update rectange y negative positions css
$('.rect', this).css({
left: (100 - position.right) + '%',
right: (100 - position.left) + '%'
});
// else if image y cursor drag position percent is positive to mousedown y position
} else {
// update rectange y positive positions css
$('.rect', this).css({
right: position.right + '%',
left: position.left + '%'
});
}
}
});
// mouse down event function in area div
$(document).on('mousedown', '.area', function(e) {
// remove the drawn class
$(this).removeClass('drawn');
// update img object variable data upon this mousedown event
imgMatrix();
// update position object variable data passing current event data and mousedown param as true
positionMatrix(e, true);
// update rectange xy positions css
$('.rect', this).css({
left: position.left + '%',
top: position.top + '%',
right: position.right + '%',
bottom: position.bottom + '%'
});
// add draw class to area div to reveal rectangle
$(this).addClass('draw');
});
// mouse up event function in area div
$(document).on('mouseup', '.area', function(e) {
// update img object variable data upon this mouseup event
imgMatrix();
// if this area had draw class
if ($(this).hasClass('draw')) {
// update position object variable data passing current event
positionMatrix(e);
// math trunc on position values if x and y values are equal, basically no drawn rectangle on mouseup event
if ((Math.trunc(position.left) === Math.trunc(100 - position.right)) && (Math.trunc(position.top) === Math.trunc(100 - position.bottom))) {
// remove draw and drawn classes
$(this).removeClass('draw drawn');
// else if x and y values are not equal, basically a rectange has been drawn
} else {
// add drawn class and remove draw class
$(this).addClass('drawn').removeClass('draw');
}
}
});
// on window resize function
$(window).on('resize', function(e) {
// update img object variable data upon this window resize event
imgMatrix();
});
/* area div */
.area {
overflow: hidden;
position: relative;
margin-bottom: 8px;
}
/* area div hover cursor */
.area:hover {
cursor: crosshair;
}
/* img tag in area div */
.area IMG {
display: block;
width: 100%;
pointer-events: none;
user-select: none;
}
/* rectangle div in area div */
.area .rect {
opacity: 0;
transition: all 0s ease;
position: absolute;
border: 1px solid red;
z-index: 1;
}
/* rectangle div css when in draw or drawn mode */
.area.draw .rect,
.area.drawn .rect {
opacity: 1;
}
/* below css is for fun rendering outer exclusion area of drawn rectangle div with a opaque overlay */
/* rectange exclusing pseudo elems base css */
.area.drawn .rect .exclusion-x::before,
.area.drawn .rect .exclusion-x::after,
.area.drawn .rect .exclusion-y::before,
.area.drawn .rect .exclusion-y::after {
position: absolute;
content: '';
display: block;
background: #000;
opacity: .75;
z-index: -1;
pointer-events: none;
user-select: none;
}
/* rectange outer opaque x above css */
.area.drawn .rect .exclusion-x::before {
bottom: calc(100% + 1px);
left: 50%;
transform: translateX(-50%);
height: 200vh;
width: 200vw;
}
/* rectange outer opaque x below css */
.area.drawn .rect .exclusion-x::after {
top: calc(100% + 1px);
left: 50%;
transform: translateX(-50%);
height: 200vh;
width: 200vw;
}
/* rectange outer opaque y left css */
.area.drawn .rect .exclusion-y::before {
right: calc(100% + 1px);
top: -1px;
bottom: -1px;
width: 200vw;
}
/* rectange outer opaque y right css */
.area.drawn .rect .exclusion-y::after {
left: calc(100% + 1px);
top: -1px;
bottom: -1px;
width: 200vw;
}
<div class="area">
<img src="https://dragonballsuper-france.fr/wp-content/uploads/2017/10/DIHQF2OXoAAJi4n-660x330.jpg" alt="" />
<div class="rect">
<div class="exclusion-x"></div>
<div class="exclusion-y"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
My implementation,
http://kodhus.com/kodnest/land/PpNFTgp
I am curious, as I am not able for some reason to figure this out, how to get my JavaScript to make my slider behave more natural and smoother, if someone knows, how to, or can make this, feel free. I'd be happy to understand.
JavaScript:
const thumb = document.querySelector('.thumb');
const thumbIndicator = document.querySelector('.thumb .thumb-indicator');
const sliderContainer = document.querySelector('.slider-container');
const trackProgress = document.querySelector('.track-progress');
const sliderContainerStart = sliderContainer.offsetLeft;
const sliderContainerWidth = sliderContainer.offsetWidth;
var translate;
var dragging = false;
var percentage = 14;
document.addEventListener('mousedown', function(e) {
if (e.target.classList.contains('thumb-indicator')) {
dragging = true;
thumbIndicator.classList.add('focus');
}
});
document.addEventListener('mousemove', function(e) {
if (dragging) {
console.log('moving', e)
if (e.clientX < sliderContainerStart) {
translate = 0;
} else if (e.clientX > sliderContainerWidth + sliderContainerStart) {
translate = sliderContainerWidth;
} else {
translate = e.clientX - sliderContainer.offsetLeft;
}
thumb.style.transform = 'translate(-50%) translate(' + translate + 'px)';
trackProgress.style.transform = 'scaleX(' + translate / sliderContainerWidth + ')'
}
});
function setPercentage() {
thumb.style.transform = 'translate(-50%) translate(' + percentage/100 * sliderContainerWidth + 'px)';
trackProgress.style.transform = 'scaleX(' + percentage/100 + ')';
}
function init() {
setPercentage();
}
init();
document.addEventListener('mouseup', function(e) {
dragging = false;
thumbIndicator.classList.remove('focus');
});
EDIT: Is there a way to smoothly and naturally increment by one for every slow move?
Is it possible to make to behave as if, like when one clicks the progress bar so that it jumps there?
The kodhus site is very janky in my browser, so I can't tell if your code lacks responsiveness or whether it's the site itself. I feel that your code is a bit convoluted: translate and width / height are mixed unnecessarily; no need to use a dragging boolean when that information is always stored in the classlist. The following slider performs nicely, and has a few considerations I don't see in yours:
stopPropagation when clicking the .thumb element
drag stops if window loses focus
pointer-events: none; applied to every part of the slider but the .thumb element
let applySliderFeel = (slider, valueChangeCallback=()=>{}) => {
// Now `thumb`, `bar` and `slider` are the elements that concern us
let [ thumb, bar ] = [ '.thumb', '.bar' ].map(v => slider.querySelector(v));
let changed = amt => {
thumb.style.left = `${amt * 100}%`;
bar.style.width = `${amt * 100}%`;
valueChangeCallback(amt);
};
// Pressing down on `thumb` activates dragging
thumb.addEventListener('mousedown', evt => {
thumb.classList.add('active');
evt.preventDefault();
evt.stopPropagation();
});
// Releasing the mouse button (anywhere) deactivates dragging
document.addEventListener('mouseup', evt => thumb.classList.remove('active'));
// If the window loses focus dragging also stops - this can be a very
// nice quality of life improvement!
window.addEventListener('blur', evt => thumb.classList.remove('active'));
// Now we have to act when the mouse moves...
document.addEventListener('mousemove', evt => {
// If the drag isn't active do nothing!
if (!thumb.classList.contains('active')) return;
// Compute `xRelSlider`, which is the mouse position relative to the
// left side of the slider bar. Note that *client*X is compatible with
// getBounding*Client*Rect, and using these two values we can quickly
// get the relative x position.
let { width, left } = slider.getBoundingClientRect();
// Consider mouse x, subtract left offset of slider, and subtract half
// the width of the thumb (so drags position the center of the thumb,
// not its left side):
let xRelSlider = evt.clientX - left - (thumb.getBoundingClientRect().width >> 1);
// Clamp `xRelSlider` between 0 and the slider's width
if (xRelSlider < 0) xRelSlider = 0;
if (xRelSlider > width) xRelSlider = width;
// Apply styling (using percents is more robust!)
changed(xRelSlider / width);
evt.preventDefault();
evt.stopPropagation();
});
slider.addEventListener('mousedown', evt => {
let { width, left } = slider.getBoundingClientRect();
// Clicking the slider also activates a drag
thumb.classList.add('active');
// Consider mouse x, subtract left offset of slider, and subtract half
// the width of the thumb (so drags position the center of the thumb,
// not its left side):
let xRelSlider = evt.clientX - left - (thumb.getBoundingClientRect().width >> 1);
// Apply styling (using percents is more robust!)
changed(xRelSlider / width);
evt.preventDefault();
evt.stopPropagation();
});
changed(0);
};
let valElem = document.querySelector('.value');
applySliderFeel(document.querySelector('.slider'), amt => valElem.innerHTML = amt.toFixed(3));
.slider {
position: absolute;
width: 80%; height: 4px; background-color: rgba(0, 0, 0, 0.3);
left: 10%; top: 50%; margin-top: -2px;
}
.slider > .bar {
position: absolute;
left: 0; top: 0; width: 0; height: 100%;
background-color: #000;
pointer-events: none;
}
.slider > .thumb {
position: absolute;
width: 20px; height: 20px; background-color: #000; border-radius: 100%;
left: 0; top: 50%; margin-top: -10px;
}
.slider > .thumb.active {
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.5);
}
<div class="slider">
<div class="bar"></div>
<div class="thumb"></div>
</div>
<div class="value"></div>
I am writing an image viewer JS component and i'm currently stuck on the calculation for the zooming in. I figured out the zooming in or out on one point (instead of just the center) using css transforms. I'm not using transform-origin because there will be multiple points of zooming in or out, and the position must reflect that. However, that's exactly where i'm stuck. The algorithm i'm using doesn't calculate the offset correctly after trying to zoom in (or out) when the mouse has actually moved from the initial position and I can't for the life of me figure out what's missing in the equation.
The goal is whenever the mouse is moved to another position, the whole image should scale with that position as the origin, meaning the image should scale but the point the mouse was over should remain in its place.
For reference, look at this JS component. Load an image, zoom in somewhere, move your mouse to another position and zoom in again.
Here's a pen that demonstrates the issue in my code: https://codepen.io/Emberfire/pen/jOWrVKB
I'd really apreciate it if someone has a clue as to what i'm missing :)
Here's the HTML
<div class="container">
<div class="wrapper">
<div class="image-wrapper">
<img class="viewer-img" src="https://cdn.pixabay.com/photo/2020/06/08/17/54/rain-5275506_960_720.jpg" draggable="false" />
</div>
</div>
</div>
Here's the css that i'm using for the component:
* {
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
margin: 0;
}
.container {
width: 100%;
height: 100%;
}
.wrapper {
width: 100%;
height: 100%;
overflow: hidden;
transition: all .1s;
position: relative;
}
.image-wrapper {
position: absolute;
}
.wrapper img {
position: absolute;
transform-origin: top left;
}
And here's the script that calculates the position and scale:
let wrapper = document.querySelector(".wrapper");
let image = wrapper.querySelector(".image-wrapper");
let scale = 1;
image.querySelector("img").addEventListener("wheel", (e) => {
if (e.deltaY < 0) {
scale = Number((scale * 1.2).toFixed(2));
let scaledX = e.offsetX * scale;
let scaledY = e.offsetY * scale;
imageOffsetX = e.offsetX - scaledX;
imageOffsetY = e.offsetY - scaledY;
e.target.style.transform = `scale3d(${scale}, ${scale}, ${scale})`;
image.style.transform = `translate(${imageOffsetX.toFixed(2)}px, ${imageOffsetY.toFixed(2)}px)`;
} else {
scale = Number((scale / 1.2).toFixed(2));
let scaledX = e.offsetX * scale;
let scaledY = e.offsetY * scale;
imageOffsetX = e.offsetX - scaledX;
imageOffsetY = e.offsetY - scaledY;
e.target.style.transform = `scale3d(${scale}, ${scale}, ${scale})`;
image.style.transform = `translate(${imageOffsetX}px, ${imageOffsetY}px)`;
}
});
Thinking a little, I understood where the algorithm failed. Your calculation is focused on the information of offsetx and offsety of the image, the problem is that you were dealing with scale and with scale the data like offsetx and offsety are not updated, they saw constants. So I stopped using scale to enlarge the image using the "width" method. It was also necessary to create two variables 'accx' and 'accy' to receive the accumulated value of the translations
let wrapper = document.querySelector(".wrapper");
let image = wrapper.querySelector(".image-wrapper");
let img = document.querySelector('img')
let scale = 1;
let accx = 0, accy = 0
image.querySelector("img").addEventListener("wheel", (e) => {
if (e.deltaY < 0) {
scale = Number((scale * 1.2));
accx += Number(e.offsetX * scale/1.2 - e.offsetX * scale)
accy += Number(e.offsetY * scale/1.2 - e.offsetY * scale)
} else {
scale = Number((scale / 1.2));
accx += Number(e.offsetX * scale * 1.2 - e.offsetX * scale)
accy += Number(e.offsetY * scale * 1.2 - e.offsetY * scale)
}
e.target.style.transform = `scale3D(${scale}, ${scale}, ${scale})`
image.style.transform = `translate(${accx}px, ${accy}px)`;
});
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
.container {
width: 100%;
height: 100%;
position: relative;
}
.wrapper {
width: 100%;
height: 100%;
overflow: hidden;
transition: all 0.1s;
position: relative;
}
.image-wrapper {
position: absolute;
}
.wrapper img {
position: absolute;
transform-origin: top left;
}
.point {
width: 4px;
height: 4px;
background: #f00;
position: absolute;
}
<div class="container">
<div class="wrapper">
<div class="image-wrapper">
<img class="viewer-img" src="https://cdn.pixabay.com/photo/2020/06/08/17/54/rain-5275506_960_720.jpg" draggable="false" />
</div>
</div>
</div>
Preview the effect here: JsFiddle
You're missing to calculate the difference in the transform points after an element scales.
I would suggest to use transform-origin set to center, that way you can center initially your canvas using CSS flex.
Create an offset {x:0, y:0} that is relative to the canvas transform-origin's center
const elViewport = document.querySelector(".viewport");
const elCanvas = elViewport.querySelector(".canvas");
let scale = 1;
const scaleFactor = 0.2;
const offset = {
x: 0,
y: 0
}; // Canvas translate offset
elViewport.addEventListener("wheel", (ev) => {
ev.preventDefault();
const delta = Math.sign(-ev.deltaY); // +1 on wheelUp, -1 on wheelDown
const scaleOld = scale; // Remember the old scale
scale *= Math.exp(delta * scaleFactor); // Change scale
// Get pointer origin from canvas center
const vptRect = elViewport.getBoundingClientRect();
const cvsW = elCanvas.offsetWidth * scaleOld;
const cvsH = elCanvas.offsetHeight * scaleOld;
const cvsX = (elViewport.offsetWidth - cvsW) / 2 + offset.x;
const cvsY = (elViewport.offsetHeight - cvsH) / 2 + offset.y;
const originX = ev.x - vptRect.x - cvsX - cvsW / 2;
const originY = ev.y - vptRect.y - cvsY - cvsH / 2;
const xOrg = originX / scaleOld;
const yOrg = originY / scaleOld;
// Calculate the scaled XY
const xNew = xOrg * scale;
const yNew = yOrg * scale;
// Retrieve the XY difference to be used as the change in offset
const xDiff = originX - xNew;
const yDiff = originY - yNew;
// Update offset
offset.x += xDiff;
offset.y += yDiff;
// Apply transforms
elCanvas.style.scale = scale;
elCanvas.style.translate = `${offset.x}px ${offset.y}px`;
});
* {
margin: 0;
box-sizing: border-box;
}
.viewport {
margin: 20px;
position: relative;
overflow: hidden;
height: 200px;
transition: all .1s;
outline: 2px solid red;
display: flex;
align-items: center;
justify-content: center;
}
.canvas {
flex: none;
}
<div class="viewport">
<div class="canvas">
<img src="https://cdn.pixabay.com/photo/2020/06/08/17/54/rain-5275506_960_720.jpg" draggable="false" />
</div>
</div>
For more info head to this answer: zoom pan mouse wheel with scrollbars
I'm building a transition where I want to scale up a thumbnail to fill the viewport on click.
Currently I got the calculations right on finding the right scale ratio based on the viewport and it's size, but I'm having problems on centring the image in the viewport after it's scaled/expanded.
I only have the problem when I'm using transform-origin: center center; and the image is centered in a fixed full screen container. Here is a jsfiddle showing my problem.
I've also made a jsfiddle showing how it is without the centering. This is close to what I want, except I want to be able to transition the image from other positions rather than the top left corner.
Here is the code I have so far
// Helper fucntions
const getWindowWidth = () => {
return Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
}
const getWindowHeight = () => {
return Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
}
// Get element
const El = document.getElementById('test');
// Add listener
El.addEventListener('click', () => {
// Viewport size
const viewH = getWindowHeight();
const viewW = getWindowWidth();
// El size
const elHeight = El.offsetHeight;
const elWidth = El.offsetWidth;
// Scale ratio
let scale = 1;
// Get how much element need to scale
// to fill viewport
// Based on https://stackoverflow.com/a/28475234/1719789
if ((viewW / viewH) > (elWidth / elHeight)) {
scale = viewW / elWidth;
} else {
scale = viewH / elHeight;
}
// Center element
const x = ((viewW - ((elWidth) * scale)) / 2);
const y = ((viewH - ((elHeight) * scale)) / 2);
// matrix(scaleX(),skewY(),skewX(),scaleY(),translateX(),translateY())
El.style.transform = 'matrix(' + scale + ', 0, 0, ' + scale + ', ' + x + ', ' + y + ')';
});
And some css:
.Container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#El {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
transform-origin: center center;
transform: translate(-50%, -50%);
transition: transform .3s ease-in-out;
}
The best solution would be getting the current position of the clicked thumbnail (not always centered) and from that scale it to fill the screen. But I'm not really sure where to start to be able to do that.
Is this the effect you are looking for?
CSS:
.Container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#test {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
transform: translate(-50%, -50%);
transition: all 3.3s ease-in-out;
}
#test.rdy{
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
JS:
// Helper fucntions
const getWindowWidth = () => {
return Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
}
const getWindowHeight = () => {
return Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
}
const El = document.getElementById('test');
El.addEventListener('click', () => {
const viewH = getWindowHeight();
const viewW = getWindowWidth();
const elHeight = El.offsetHeight;
const elWidth = El.offsetWidth;
let scale = 1;
if ((viewW / viewH) > (elWidth / elHeight)) {
scale = viewW / elWidth;
} else {
scale = viewH / elHeight;
}
El.style.width = elWidth * scale + 'px';
El.style.height = elHeight * scale + 'px';
//Add .rdy class in case that position needs resetting :)
document.getElementById('test').className += ' rdy';
});
//A hack to make height transition work :)
window.onload = function(){
El.style.height = El.offsetHeight + 'px';
};