admin管理员组文章数量:1332383
I want to animate an image when clicked to fill the whole screen, in such a way that seamlessly transitions from its original position to its full size, and back again, like on Medium.
The problem here is that the CSS position
property, with top
and left
, is not animatable. After trying that, I thought of using transform: scale()
properties, but this will lead to a bunch of calculations that I'd like to avoid if possible.
My plicated solution would be to get the element's original position using getBoundingClientRect()
, and from there find the end position the image must be in, and create a custom animation every time the image gets blown to full size using Element.animate
. I'm not sure that's the best way to go about this, as figuring out the final size and position of the image will be some extra math I don't really want to mess with.
Below is my current markup, and some CSS showing it's possible to keyframe a positional animation using translateX()
and translateY()
, but not as I really need to.
document.querySelector('picture').onclick = function () {
document.querySelector('picture').classList.toggle('modal')
}
<style>
figure {
margin: 0 0 0 0;
display: inline-block; /* Stays same width as image contents */
background-color: whitesmoke;
}
img {
max-width: 100%; /* Images should fit within their container by default */
height: auto;
background-color: lightgrey;
margin: auto;
}
picture.modal {
position: fixed;
top: 0;
left: 0;
background-color: black;
height: 100vh;
width: 100vw;
margin: 0 0;
display: flex;
align-content: center;
object-fit: contain;
}
picture.modal img {
animation-name: slidein;
animation-duration: 1s;
}
@keyframes slidein {
0% {
transform: translateX(30px);
}
100% {
transform: translateX(0);
}
}
figcaption {
padding: 8px; /* Matches default page margin for Chrome/Edge */
}
</style>
<figure>
<picture>
<img src=".jpg!d" loading="auto" />
</picture>
<figcaption>
<header>Title</header>
<footer>Description</footer>
</figcaption>
</figure>
I want to animate an image when clicked to fill the whole screen, in such a way that seamlessly transitions from its original position to its full size, and back again, like on Medium.
The problem here is that the CSS position
property, with top
and left
, is not animatable. After trying that, I thought of using transform: scale()
properties, but this will lead to a bunch of calculations that I'd like to avoid if possible.
My plicated solution would be to get the element's original position using getBoundingClientRect()
, and from there find the end position the image must be in, and create a custom animation every time the image gets blown to full size using Element.animate
. I'm not sure that's the best way to go about this, as figuring out the final size and position of the image will be some extra math I don't really want to mess with.
Below is my current markup, and some CSS showing it's possible to keyframe a positional animation using translateX()
and translateY()
, but not as I really need to.
document.querySelector('picture').onclick = function () {
document.querySelector('picture').classList.toggle('modal')
}
<style>
figure {
margin: 0 0 0 0;
display: inline-block; /* Stays same width as image contents */
background-color: whitesmoke;
}
img {
max-width: 100%; /* Images should fit within their container by default */
height: auto;
background-color: lightgrey;
margin: auto;
}
picture.modal {
position: fixed;
top: 0;
left: 0;
background-color: black;
height: 100vh;
width: 100vw;
margin: 0 0;
display: flex;
align-content: center;
object-fit: contain;
}
picture.modal img {
animation-name: slidein;
animation-duration: 1s;
}
@keyframes slidein {
0% {
transform: translateX(30px);
}
100% {
transform: translateX(0);
}
}
figcaption {
padding: 8px; /* Matches default page margin for Chrome/Edge */
}
</style>
<figure>
<picture>
<img src="https://c.pxhere./images/12/30/5e283733ff3cd2bd18d7cc13f40a-1435525.jpg!d" loading="auto" />
</picture>
<figcaption>
<header>Title</header>
<footer>Description</footer>
</figcaption>
</figure>
I started stubbing out some code as below, but quickly realized that another solution may be much better.
// Get the position of elements for animation
let x = document.querySelector('img').getBoundingClientRect().x
let y = document.querySelector('img').getBoundingClientRect().y
// Set the animation on the image so that it moves smoothly from its position outwards
Help with a vanilla CSS solution, if it is known, would be greatly appreciated.
Share Improve this question asked Jul 19, 2020 at 2:12 Denis G. LabrecqueDenis G. Labrecque 1,30118 silver badges37 bronze badges2 Answers
Reset to default 6 +50You were going in the right direction with getBoundingClientRect
. By using this and applying some calculations on it on, I was able to e up with this
let imageResizing = false;
function zoomUnzoomImage(resizeEvent) {
if (!resizeEvent && this.classList.contains('zoomed')) {
this.classList.remove('zoomed');
this.style.transform = "";
document.querySelector('.image-backdrop').classList.remove('zoomed');
removeZoomOutListeners();
removeResizeListener();
} else {
let imageCordinates
if (resizeEvent) {
imageCordinates = this._originalImageCordinates;
}
else {
imageCordinates = getBoundingClientRect(this);
this._originalImageCordinates = imageCordinates;
}
const deviceRatio = window.innerHeight / window.innerWidth;
const imageRatio = imageCordinates.height / imageCordinates.width;
// Scale image according to the device and image size
const imageScale = deviceRatio > imageRatio ?
window.innerWidth / imageCordinates.width :
window.innerHeight / imageCordinates.height;
const imageX = ((imageCordinates.left + (imageCordinates.width) / 2));
const imageY = ((imageCordinates.top + (imageCordinates.height) / 2));
const bodyX = (window.innerWidth) / 2;
const bodyY = (window.innerHeight) / 2;
const xOffset = (bodyX - imageX) / (imageScale);
const yOffset = (bodyY - imageY) / (imageScale);
this.style.transform = "scale(" + imageScale + ") translate(" + xOffset + "px," + yOffset + "px) ";
this.classList.add('zoomed');
document.querySelector('.image-backdrop').classList.add('zoomed');
registersZoomOutListeners();
registerResizeListener();
}
}
function registersZoomOutListeners() {
// zoom out on scroll
document.addEventListener('scroll', scrollZoomOut);
// zoom out on escape
document.addEventListener('keyup', escapeClickZoomOut);
// zoom out on clicking the backdrop
document.querySelector('.image-backdrop').addEventListener('click', backDropClickZoomOut);
}
function removeZoomOutListeners() {
document.removeEventListener('scroll', scrollZoomOut);
document.removeEventListener('keyup', escapeClickZoomOut);
document.querySelector('.image-backdrop').removeEventListener('click', backDropClickZoomOut);
}
function registerResizeListener() {
window.addEventListener('resize', onWindowResize)
}
function removeResizeListener() {
window.removeEventListener('resize', onWindowResize)
}
function scrollZoomOut() {
if (document.querySelector('.zoomable-image.zoomed') && !imageResizing) {
zoomUnzoomImage.call(document.querySelector('.zoomable-image.zoomed'));
}
}
function backDropClickZoomOut() {
if (document.querySelector('.zoomable-image.zoomed')) {
zoomUnzoomImage.call(document.querySelector('.zoomable-image.zoomed'));
}
}
function escapeClickZoomOut(event) {
if (event.key === "Escape" && document.querySelector('.zoomable-image.zoomed')) {
zoomUnzoomImage.call(document.querySelector('.zoomable-image.zoomed'));
}
}
function onWindowResize() {
imageResizing = true;
if (document.querySelector('.zoomable-image.zoomed')) {
debounce(
function () {
zoomUnzoomImage.call(document.querySelector('.zoomable-image.zoomed'), true)
imageResizing = false;
}, 100)()
}
}
function getBoundingClientRect(element) {
var rect = element.getBoundingClientRect();
return {
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
};
}
function debounce(func, delay) {
let debounceTimer
return function () {
const context = this
const args = arguments
clearTimeout(debounceTimer)
debounceTimer
= setTimeout(() => func.apply(context, args), delay)
}
}
document.addEventListener('click', function (event) {
if (event && event.target && event.target.className.includes('zoomable-image')) {
zoomUnzoomImage.call(event.target)
}
});
figure {
margin: 0 0 0 0;
display: inline-block;
/* Stays same width as image contents */
background-color: whitesmoke;
}
img {
max-width: 100%;
/* Images should fit within their container by default */
height: auto;
background-color: lightgrey;
margin: auto;
transition: transform 0.3s;
}
.zoomable-image {
cursor: zoom-in;
}
.zoomable-image.zoomed {
cursor: zoom-out;
z-index: 100;
position: relative;
}
.image-backdrop.zoomed {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: 50;
background-color: rgba(255, 255, 255, 0.95);
}
<div class="image-grid">
<img class="zoomable-image" src="https://picsum.photos/200/400?random=1" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/400/200?random=2" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/600/200?random=3" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/600/100?random=3" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/100/400?random=4" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/400/100?random=5" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/1000?random=6" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/300/400?random=7" loading="auto" />
<img class="zoomable-image" src="https://picsum.photos/400/300?random=8" loading="auto" />
</div>
<div class="image-backdrop"></div>
Here is another idea that I use. Similar to Medium's zoom effect.
const {
fromEvent
} = rxjs;
const images = document.querySelectorAll('article img');
const detailModal = document.querySelector('#detail-modal');
const detailBgModal = document.querySelector('.bg');
let canShowModal = true;
detailBgModal.addEventListener("transitionend", () => {
if (detailBgModal.style.opacity === '0') {
const showImage = document.querySelector('[fullscreen=true]')
showImage.style.zIndex = 0;
detailBgModal.style.bottom = 'auto';
showImage.removeAttribute('fullscreen')
canShowModal = true;
}
});
const checkIsImagePortrait = (src) => {
return new Promise((resolve) => {
const img = new Image();
img.src = src;
img.onload = () => {
let isImagePortrait;
const ratio = img.naturalWidth / img.naturalHeight;
const pratio = window.innerWidth / window.innerHeight;
console.log('pratio', pratio)
if (ratio < pratio) {
isImagePortrait = true;
} else {
isImagePortrait = false
}
resolve(isImagePortrait);
};
});
};
const showModal = (imageElement) => {
const src = imageElement.getAttribute('src');
const modalImage = document.querySelector('#detail-modal img');
return checkIsImagePortrait(src).then(isPortrait => {
const src = imageElement.getAttribute('src');
if (isPortrait) {
modalImage.style.height = '100%';
modalImage.style.width = 'auto';
} else {
modalImage.style.height = 'auto';
modalImage.style.width = '100%';
}
detailModal.style.top = `${window.scrollY}px`;
detailModal.style.height = `${window.innerHeight}px`;
detailModal.style.display = 'flex';
detailBgModal.style.bottom = '0';
detailBgModal.style.opacity = 1;
document.querySelector('#detail-modal img').setAttribute('src', src);
});
};
const hideModal = () => {
detailBgModal.style.opacity = 0;
detailModal.style.display = 'none';
canShowModal = false;
};
let modalDetailPos;
const handleBodyScroll = () => {
const {
scrollY
} = window;
if (Math.abs(scrollY - modalDetailPos) > 50) {
const event = new Event('click');
detailModal.dispatchEvent(event);
window.removeEventListener('scroll', handleBodyScroll);
}
};
images.forEach((image) => {
fromEvent(image, 'click').subscribe(() => {
if (!canShowModal) {
return
}
image.setAttribute('fullscreen', true)
console.log('show image')
showModal(image).then(() => {
const modalImage = document.querySelector('#detail-modal img');
const firstSnap = image.getBoundingClientRect();
const lastSnap = modalImage.getBoundingClientRect();
const {
deltaX,
deltaY,
deltaWidth,
deltaHeight
} = getDelta(firstSnap, lastSnap);
modalImage.animate([{
transformOrigin: 'top left',
transform: `
translate(${deltaX}px, ${deltaY}px)
scale(${deltaWidth}, ${deltaHeight})
`
},
{
transformOrigin: 'top left',
transform: 'none'
}
], {
duration: 300,
easing: 'ease-in-out',
fill: 'both'
}).onfinish = () => {
modalDetailPos = window.scrollY;
window.addEventListener('scroll', handleBodyScroll)
};
});
});
})
const moveElementToFullscreen = (element) => {
element.style.position = 'fixed';
element.style.left = 0;
element.style.top = 0;
element.style.right = 0;
element.style.bottom = 0;
};
const moveElementToNormalState = (element) => {
element.style.position = null;
element.style.left = null;
element.style.top = null;
element.style.right = null;
element.style.bottom = null;
};
const getDelta = (firstSnap, lastSnap) => {
const deltaX = firstSnap.left - lastSnap.left;
const deltaY = firstSnap.top - lastSnap.top;
const deltaWidth = firstSnap.width / lastSnap.width;
const deltaHeight = firstSnap.height / lastSnap.height;
return {
deltaX: deltaX,
deltaY: deltaY,
deltaWidth: deltaWidth,
deltaHeight: deltaHeight
};
}
fromEvent(detailModal, 'click').subscribe(() => {
const showImage = document.querySelector('[fullscreen=true]');
if (!showImage) {
return;
}
const modalImage = document.querySelector('#detail-modal img');
console.log('showImage', showImage)
const firstSnap = modalImage.getBoundingClientRect();
const lastSnap = showImage.getBoundingClientRect();
hideModal();
const {
deltaX,
deltaY,
deltaWidth,
deltaHeight
} = getDelta(firstSnap, lastSnap);
showImage.style.zIndex = 100;
showImage.animate([{
transformOrigin: 'top left',
transform: `
translate(${deltaX}px, ${deltaY}px)
scale(${deltaWidth}, ${deltaHeight})
`
},
{
transformOrigin: 'top left',
transform: 'none'
}
], {
duration: 400,
easing: 'ease',
fill: 'both'
});
});
article {
max-width: 700px;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
}
p {
font-family: 'Nunito';
font-size: 18px;
color: rgba(0, 0, 0, .84);
line-height: 1.60;
margin: 30px auto;
}
article img {
max-width: 100%;
display: block;
position: relative;
cursor: zoom-in;
}
#detail-modal {
justify-content: center;
align-items: center;
display: none;
position: absolute;
left: 0;
right: 0;
top: 0;
}
#detail-modal img {
display: block;
position: relative;
z-index: 100;
cursor: zoom-out;
}
.bg {
position: fixed;
left: 0;
top: 0;
right: 0;
background-color: rgba(0,0,0,.3);
opacity: 0;
display: block;
transition: opacity .3s;
}
<script src="https://cdnjs.cloudflare./ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
<article>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut rutrum mauris id nibh ultrices, vitae hendrerit nibh venenatis. Phasellus volutpat mauris in diam lacinia, sit amet blandit ante scelerisque. Mauris porttitor risus sit amet urna vestibulum
porta. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer id diam sem. Nunc modo, est sed efficitur condimentum, massa purus facilisis tellus, at modo ex est a tellus. Morbi quis iaculis mi. Nam et
iaculis sapien, at mattis ipsum.</p>
<div>
<img src="https://images.unsplash./photo-1507358522600-9f71e620c44e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2850&q=80" />
</div>
<p>Nullam non porttitor nibh. Etiam mollis libero turpis, vitae sagittis ipsum gravida nec. Vivamus diam sapien, laoreet vel mi ultrices, efficitur tristique nunc. Nam tempus pharetra felis, nec condimentum leo vehicula a. Duis rutrum orci a tellus tristique
scelerisque. Suspendisse potenti. Proin mollis turpis feugiat, pulvinar risus ac, scelerisque diam. Aenean sodales venenatis tellus, in lacinia sapien. Nam tempus efficitur ligula id feugiat. Donec pretium, nunc sit amet dignissim rutrum, urna est
tristique ante, id convallis arcu urna vel dui. Cras a metus id orci aliquet tincidunt eget ac mi. Pellentesque elementum lorem in elementum vehicula. Nunc et dolor orci. Nulla varius lorem metus, vel cursus leo ultricies non.</p>
<div>
<img src="https://images.unsplash./photo-1548636200-691c76f69390?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=668&q=80" />
</div>
<p>
Aliquam at arcu mauris. Curabitur tincidunt massa ut sem porttitor ornare. Duis dapibus dignissim lectus. Cras sodales urna vitae libero lobortis, in consequat dolor efficitur. Sed eleifend nibh mi, sit amet euismod sem faucibus sed. Aenean ac accumsan
libero, ut dictum ex. Aenean tincidunt gravida enim, in luctus ante volutpat eu. Curabitur sed orci nec nisi cursus blandit.
</p>
<div>
<img src="https://images.unsplash./reserve/fPuLkQNXRUKI6HQ2cMPf_IMG_4761.jpg?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80" />
</div>
<p>
Morbi ac quam luctus, aliquam odio in, consectetur orci. Etiam et dui sollicitudin, congue odio sit amet, modo metus. Nunc ac facilisis dolor, sit amet dignissim dui. Praesent vehicula ut dui hendrerit modo. Vivamus ac elementum turpis. Proin non
erat semper, dignissim risus vel, ornare libero. Ut volutpat libero non lacus eleifend ultrices. Morbi augue massa, placerat eget eros vel, consequat tincidunt sapien. Vestibulum placerat diam placerat tincidunt lacinia. Proin lorem justo, viverra
pretium laoreet eu, condimentum et odio. Proin vitae nibh felis.
</p>
</article>
<div class="bg"></div>
<div id="detail-modal">
<img />
</div>
quoted from here
本文标签: javascriptSmoothly Animating Image Position and Scale to Fit the Screen in HTMLCSSStack Overflow
版权声明:本文标题:javascript - Smoothly Animating Image Position and Scale to Fit the Screen in HTMLCSS - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742298012a2449113.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论