Sprite frame as div background, auto scales wrongly on div rotation - javascript

As the title suggests, I'm rendering a frame of a sprite on a div background.
That all works fine, and resizes correctly just until a rotation is applied on the div.
When the image is first loads in the rotated div, the scale is correct, but if we resize the document, the frame scale becomes incorrect.
Please see this JSFIDDLE. To replicate the issue just resize the "result" section.
Code:
HTML
<div class="img"></div>
CSS
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.img {
overflow: hidden;
background-repeat: no-repeat;
background: url(https://image.freepik.com/free-vector/orange-mushroom-game-sprites_22191-71.jpg);
width: 50vw;
height: 50vw;
}
JS
const $img = document.querySelector('.img');
const firstFrameWidth = 185;
const firstFrameHeight = 155;
const firstFrameX = 0;
const firstFrameY = -10;
const spriteWidth = 626;
const spriteHeight = 348;
function resize() {
const bounds = $img.getBoundingClientRect();
const scaleX = bounds.width / firstFrameWidth;
const scaleY = bounds.height / firstFrameHeight;
const bgWidth = spriteWidth * scaleX;
const bgHeight = spriteHeight * scaleY;
const bgX = firstFrameX * scaleX;
const bgY = firstFrameY * scaleY;
$img.style.backgroundSize = `${bgWidth}px ${bgHeight}px`;
$img.style.backgroundPosition = `${firstFrameX}px ${firstFrameY}px`;
}
window.addEventListener('resize', resize);
resize();
$img.style.transform = 'rotate(45deg)';
Any ideas?

I actually figure out much later that getBoundingClientRect returns different values from window.getComputedStyles...apparently like you said, getBoundingClientRect returns the height and width post rotation, but I needed the original height and width to scale the sprite so the trick was to use window.getComputedStyle.

Related

JavaScript given an outer div and an aspect ratio for an inner div, how do I calculate the maximum size of the inner div

I have 2 boxes / divs. An outer box and an inner box. The inner box is always contained within the outer box. Given the following info:
outer box: width=200 height=100
inner box: aspect ratio=16/9
In JavaScript, how do I calculate the maximum size of the inner box such that its aspect ratio is preserved?
I know you're asking for JavaScript specifically, but this is pretty simple to do in CSS with aspect-ratio, and if you need the dimensions in JS you could just grab the rendered dimensions.
const pre = document.querySelector('pre');
const aspect = document.querySelector('.ratio');
// getboundingClientRect also works.
const dimensions = window.getComputedStyle(aspect);
pre.textContent = `width: ${dimensions.width}, height: ${dimensions.height}`;
.container {
width: 200px;
height: 100px;
}
.ratio {
aspect-ratio: 16 / 9;
/* Just keeping it within the constaints */
max-height: 100px;
border: 1px solid red;
}
.no-ratio {
border: 1px solid red;
}
<div class="container">
<div class="ratio">
16:9
</div>
</div>
<pre></pre>
<div class="container">
<div class="no-ratio">
Not 16:9.
</div>
</div>
<pre></pre>
Get the outer aspect ratio and use that to determine if the inner box needs to be letter-boxed (landscape, shorter) or pillar-boxed (portrait, narrower) relative to the outer box. Calculate the inner dimensions based on that. You can also calculate the offsets needed to center it.
const outerWidth = 200;
const outerHeight = 100;
const aspectRatio = 16/9;
let innerWidth, innerHeight, innerTop, innerLeft;
if (outerWidth / outerHeight > aspectRatio) {
innerWidth = outerHeight * aspectRatio;
innerHeight = outerHeight;
innerLeft = (outerWidth - innerWidth) / 2;
innerTop = 0;
} else {
innerWidth = outerWidth;
innerHeight = outerWidth / aspectRatio;
innerLeft = 0;
innerTop = (outerHeight - innerHeight) / 2;
}
let outerBoxWidth = 200;
let outerBoxHeight = 100;
let maxInnerBoxWidth = ((outerBoxWidth / 16) | 0);
let maxInnerBoxHeight = ((outerBoxHeight / 9) | 0);
let widthLower = maxInnerBoxHeight > maxInnerBoxWidth;
if(widthLower){
let innerBoxHeight = 9 * maxInnerBoxWidth;
let innerBoxWidth = 17 * maxInnerBoxWidth;
}else{
let innerBoxHeight = 9 * maxInnerBoxHeight;
let innerBoxWidth = 17 * maxInnerBoxHeight;
}
Based on the answer from Ouroborus I came up with the following which proves it works.
const getBoxes = (outerWidth, outerHeight, innerAspectRatio) => {
const outer = { width: outerWidth, height: outerHeight, aspectRatio: outerWidth / outerHeight }
const inner = { width: null, height: null, aspectRatio: innerAspectRatio }
const pillarBoxed = outer.aspectRatio > inner.aspectRatio
inner.width = !pillarBoxed ? outer.width : outer.height * inner.aspectRatio
inner.height = pillarBoxed ? outer.height : outer.width / inner.aspectRatio
return { outer, inner }
}
const adjust = 40
const boxes1 = getBoxes(160 + adjust, 90, 16/9)
const boxes2 = getBoxes(160, 90 + adjust, 16/9)
// display purposes only
const displayBoxes = (boxes, outerId, innerId) => {
const outerEl = document.getElementById(outerId)
outerEl.style.width = boxes.outer.width + 'px'
outerEl.style.height = boxes.outer.height + 'px'
const innerEl = document.getElementById(innerId)
innerEl.style.width = boxes.inner.width + 'px'
innerEl.style.height = boxes.inner.height + 'px'
}
displayBoxes(boxes1, 'outer1', 'inner1')
displayBoxes(boxes2, 'outer2', 'inner2')
// console.log('boxes1', boxes1)
// console.log('boxes2', boxes2)
#outer1, #outer2 {
background-color: black;
display: inline-flex;
justify-content: center;
align-items: center;
margin: 10px;
}
#inner1, #inner2 {
background-color: red;
}
<div id="outer1">
<div id="inner1"></div>
</div>
<div id="outer2">
<div id="inner2"></div>
</div>
To make this easy to test, I made the outer box 160x90 BUT 40px wider (first example) and 40px taller (second example). Given that this works nicely with a 16/9 aspect ratio, there should be 20px left and right in the first example and 20px above and below in the second example. Which there is!
Also, the inner box should be 160x90 in both examples, which it is!
Proof that this code works as expected.

Transform dom elements to match an image regardless of its deformations

I want to have an image on page with DIVs sitting at the same place relative to the image even when the image gets stretched. Basically, I know the position the stars should take in the original picture, however I dont know how to move them such that they stand in the right place.
Here I explain with a picture :
Here is my HTML markup. The .slide-point elements are the red stars:
let points = document.querySelectorAll(".slide-point");
points.forEach(point => {
const x = point.dataset.x;
const y = point.dataset.y;
const h = point.dataset.h;
const w = point.dataset.w;
let image = point.closest(".slide").querySelector("img");
let natH = image.naturalHeight;
let natW = image.naturalWidth;
let clientH = image.clientHeight;
let clientW = image.clientWidth;
point.style.left = x * clientW / natW + "px";
point.style.top = y * clientH / natH + "px";
point.style.width = w;
point.style.height = h;
});
.slide-point {
position: absolute;
background: red;
}
.slide>img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: top left;
}
<ul data-slides>
<li class="slide" data-active>
<div class="slide-point" data-x="240" data-y="2295" data-h="165" data-w="165">★</div>
<div class="slide-point" data-x="512" data-y="2155" data-h="165" data-w="165">★</div>
<img id="slide-img" src="https://via.placeholder.com/800" alt="Photo1">
</li>
</ul>
Obviously, as soon as I resize the page, the stars don't align with the image anymore.
Thank you in advance !

How to make a zoom effect on part of an image?

I want to implement a zoom effect on certain areas of the image on hover. I tried to do it but can't think of a correct solution.
let's say I can't enlarge the top right corner.
window.addEventListener('DOMContentLoaded', () => {
const wrapper = document.querySelector('.wrapper');
const img = document.querySelector('img');
const handleMouseMove = (e) => {
const { left, top, width, height } = e.target.getBoundingClientRect();
const x = ((e.pageX - left) / width) * 100;
const y = ((e.pageY - top) / height) * 100;
const styles = `transform: scale(2); transform-origin: ${x}px ${y}px`;
img.style = styles;
}
const handleMouseLeave = () => {
img.style = '';
console.log('leave');
}
wrapper.addEventListener('mousemove', (e) => handleMouseMove(e));
wrapper.addEventListener('mouseleave', handleMouseLeave);
});
.wrapper {
width: 300px;
height: 300px;
margin: 50px auto;
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
<div class="wrapper">
<img src="https://img.freepik.com/free-photo/adorable-brown-white-basenji-dog-smiling-giving-high-five-isolated-white_346278-1657.jpg?size=626&ext=jpg&ga=GA1.2.1449299337.1618704000" alt="">
</div>
I could understand your objective. The principal error was that you had been trying to update the zoom in basis of the current position of the image, which was constantly changing because you were changing the image properties.
In my solution I used the left, and top variables to make it the most generic possible, it could be easier in React.js in my opinion.
I take the top and left position on the first load, that the real solution.
window.addEventListener('DOMContentLoaded', () => {
const wrapper = document.querySelector('.wrapper');
const img = document.querySelector('img');
const { left, top } = wrapper.getBoundingClientRect();
const handleMouseMove = (e) => {
const x = (e.pageX - left);
const y = (e.pageY - top);
const styles = `transform: scale(2); transform-origin: ${x}px ${y}px`;
img.style = styles;
}
const handleMouseLeave = () => {
img.style = '';
}
wrapper.addEventListener('mousemove', (e) => handleMouseMove(e));
wrapper.addEventListener('mouseleave', handleMouseLeave);
});
.wrapper {
width: 300px;
height: 300px;
margin: 50px auto;
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
<div class="wrapper">
<img src="https://img.freepik.com/free-photo/adorable-brown-white-basenji-dog-smiling-giving-high-five-isolated-white_346278-1657.jpg?size=626&ext=jpg&ga=GA1.2.1449299337.1618704000" alt="">
</div>
You made the calculations way more complicated than you had to.
I changed ...
const x = ((e.pageX - left) / width) * 100;
const y = ((e.pageY - top) / height) * 100;
... to ...
const x = e.pageX - left;
const y = e.pageY - top;
but the real magic happens when I added pointer-events: none; to img so it's always sending in .wrapper as e.target.
NOTE: the zoom functionality bugs out if the image is "over" the edge of the page, i.e. if you need to scroll down to see the full image. This happens in the original code as well. Just a heads up.
window.addEventListener('DOMContentLoaded', () => {
const wrapper = document.querySelector('.wrapper');
const img = document.querySelector('img');
const handleMouseMove = (e) => {
const { left, top, width, height } = e.target.getBoundingClientRect();
const x = e.pageX - left;
const y = e.pageY - top;
const styles = `transform: scale(2); transform-origin: ${x}px ${y}px`;
img.style = styles;
}
const handleMouseLeave = () => {
img.style = '';
}
wrapper.addEventListener('mousemove', (e) => handleMouseMove(e));
wrapper.addEventListener('mouseleave', handleMouseLeave);
});
.wrapper {
width: 300px;
height: 300px;
margin: 50px auto;
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
<div class="wrapper">
<img src="https://img.freepik.com/free-photo/adorable-brown-white-basenji-dog-smiling-giving-high-five-isolated-white_346278-1657.jpg?size=626&ext=jpg&ga=GA1.2.1449299337.1618704000" alt="">
</div>

Calculation of a mouse position after multiple scales on different coordinates of image

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

Move and expand image to fill viewport transition

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';
};

Categories

Resources