在HTML/CSS中平滑地设置图像位置和比例的动画以适应屏幕



我想在单击时为图像设置动画,以填充整个屏幕,使其从原始位置无缝过渡到完全大小,然后再返回,就像在Medium上一样。

这里的问题是,具有topleft的CSSposition属性是不可设置动画的。在尝试之后,我考虑使用transform: scale()属性,但这将导致一堆计算,如果可能的话,我希望避免这些计算。

我的复杂解决方案是使用getBoundingClientRect()获取元素的原始位置,然后从中找到图像必须位于的结束位置,并在每次使用Element.animate将图像放大到全尺寸时创建自定义动画。我不确定这是最好的方法,因为弄清楚图像的最终大小和位置将是一些额外的数学运算,我真的不想搞砸。

下面是我当前的标记,以及一些CSS显示可以使用translateX()translateY()为位置动画设置关键帧,但不是我真正需要的

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.com/images/12/30/5e283733ff3cd2bd18d7cc13f40a-1435525.jpg!d" loading="auto" />
</picture>
<figcaption>
<header>Title</header>
<footer>Description</footer>
</figcaption>
</figure>

我开始按照下面的方式编写一些代码,但很快意识到另一种解决方案可能会更好。

// 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

如果您知道一个普通的CSS解决方案,我们将不胜感激。

使用getBoundingClientRect时,您正朝着正确的方向前进。通过使用这个并在上面应用一些计算,我能够想出这个

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>

这里是我使用的另一个想法。类似于Medium的缩放效果。

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.com/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 commodo, est sed efficitur condimentum, massa purus facilisis tellus, at commodo ex est a tellus. Morbi quis iaculis mi. Nam et
iaculis sapien, at mattis ipsum.</p>
<div>
<img src="https://images.unsplash.com/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.com/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.com/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, commodo metus. Nunc ac facilisis dolor, sit amet dignissim dui. Praesent vehicula ut dui hendrerit commodo. 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>

在此报价

最新更新