![Return True - انیمیشن تصاویر در صفحه هنگام اسکرول](https://returntrue.ir/assets/images/20210805/20210805194202.original.png)
انیمیشن تصاویر در صفحه هنگام اسکرول
در این آموزش قصد داریم یک افکت ساده به تصاویر داخلی صفحه اضافه کنیم. این افکت در هنگام اسکرول کردن صفحه اتفاق می افتد، و تصاویر زمانی این انیمیشن را می گیرند که در صفحه به نمایش در بیایند.در آموزش قبلی نحوه اضافه کردن اسکرول نرم به صفحه را یاد گرفتیم. در این آموزش به آموزش قبلی، این قابلیت رو اضافه میکنیم.
تفکر کلی
قصد ما حرکت دادن تصویر به بالا و یا پایین در هنگام اسکرول می باشد. با تصویر پس زمینه آیتم ها کار خواهیم کرد. آیتمی که این پس زمینه را خواهد گرفت، ارتفاعی کوچکتر از تصویر پس زمینه خواهد داشت تا بتوانیم تصویر را در پس زمینه بالا و یا پایین ببریم بدون اینکه جایی خالی از تصویر باشد.
ساختار HTML
یک آیتم در برگیرنده برای آیتمی که می خواهد پس زمینه بگیرد نیاز داریم و overflow:hidden
خواهیم کرد.
<div class="item">
<div class="item__img-wrap"><div class="item__img"></div></div>
<!-- ... --->
</div>
CSS
در اینجا ما از padding
به جای height
برای در برگیرنده استفاده می کنیم در نتیجه می توانیم نسبت ظاهری درستی برای آیتم داخلی که تصویر پس زمینه دارد انتخاب کنیم. برای این کار از یک متغیر استفاده خواهیم کرد. پس تنها کار که مورد نیاز ست کردن عرض و ارتفاع تصویر است و ادامه کار را به CSS
وا گذار میکنیم. برای خواندن بیشتر در باره این تکنیک به Apect Ratio Boxes در CSS-Tricks مراجعه کنید.
.item__img-wrap {
--aspect-ratio: 1/1.5;
overflow: hidden;
width: 500px;
max-width: 100%;
padding-bottom: calc(100% / (var(--aspect-ratio)));
will-change: transform;
}
.item:first-child .item__img-wrap {
--aspect-ratio: 8/10;
--image: url(https://www.returntrue.io/laravel-filemanager/photos/1/demo/1.jpeg);
}
.item:nth-child(2) .item__img-wrap {
width: 1000px;
--aspect-ratio: 120/76;
--image: url(https://www.returntrue.io/laravel-filemanager/photos/1/demo/2.jpeg);
}
...
آیتمی که می خواهیم همراه با اسکرول کردن صفحه به بالا و یا پایین حرکت بدهیم باید ارتفاعی بلندتر از در برگیرنده خودش داشته باشد.
.item__img {
--overflow: 40px;
height: calc(100% + (2 * var(--overflow)));
top: calc( -1 * var(--overflow));
width: 100%;
position: absolute;
background-image: var(--image);
background-size: cover;
background-position: 50% 0%;
will-change: transform;
}
.item__img--t1 {
--overflow: 60px;
}
.item__img--t2 {
--overflow: 80px;
}
.item__img--t3 {
JAVASCRIPT
این قسمت در ادامه کدهای اضافه کردن اسکرول نرم به صفحه می باشد. پس اگه هنوز اون رو نخوندین، پیشنهاد می کنم برای درک بهتر یه سر بهش بزنین.
در متد constructor
از کلاس SmoothScroll
تمامی کلاس های item
را در صفحه پیدا کرده و در متغیر ذخیره می کنیم.
class SmoothScroll {
constructor() {
...
this.items = [];
[...this.DOM.main.querySelectorAll('.content > .item')].forEach(item => this.items.push(new Item(item)));
...
}
}
در متد render
از کلاس SmoothScroll
کد زیر را قرار می دهیم. این قسمت از کد، برای تمامی آیتم هایی که در اسکرول قابل مشاهده هستند عمل می کند و translation
آیتم هایی با تصویر را تنظیم می کند.
render() {
...
for (const item of this.items) {
// if the item is inside the viewport call it's render function
// this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport
if ( item.isVisible ) {
item.render();
}
}
...
}
از آنجایی که انجام این کار با تکیه بر ارتفاع تصویر است، ما نیاز داریم قبل از نمایش صفحه از لود شدن تصاویر برای بدست آوردن ارتفاع آنها اطمینان حاصل کنیم. به همین منظور از imagesLoaded استفاده خواهیم کرد
const preloadImages = () => {
return new Promise((resolve, reject) => {
imagesLoaded(document.querySelectorAll('.item__img'), {background: true}, resolve);
});
};
در ادامه پس از لود شدن تصاویر:
preloadImages().then(() => {
document.body.classList.remove('loading');
// Get the scroll position
getPageYScroll();
// Initialize the Smooth Scrolling
new SmoothScroll(document.querySelector('main'));
});
تا اینجا متدهای کلاس اضافه کردن اسکرول نرم به صفحه را بر طبق نیازمون تغییر دادیم. حالا کلاس item
برای هر یک از آیتم های صفحه ایجاد می کنیم.
class Item {
constructor(el) {
this.DOM = {el: el};
this.DOM.image = this.DOM.el.querySelector('.item__img');
this.renderedStyles = {
innerTranslationY: {
previous: 0,
current: 0,
ease: 0.1,
maxValue: parseInt(getComputedStyle(this.DOM.image).getPropertyValue('--overflow'), 10),
setValue: () => {
const maxValue = this.renderedStyles.innerTranslationY.maxValue;
const minValue = -1 * maxValue;
return Math.max(Math.min(MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, minValue, maxValue), maxValue), minValue)
}
}
};
}
...
}
منطق کلی این کلاس همانند کلاس SmoothScroll
است. یک شیئ با نام renderedStyles
به منظور نکهداری ویژگی هایی که می خواهیم آپدیت کنیم ساخته ایم. در این مورد ما آیتم دارای تصویر را حرکت می دهیم ( this.DOM.image
). تنها تفاوت در اینجا قرار دادن متغیری برای نگه داری حداکثر میزان حرکت است ( maxValue
) . این مقدار قبلا در متغیر CSS
ست شده ( –overflow
). بنابراین کمترین مقدار translation
را به شکل -1*maxVal
محاسبه می کنیم.
تابع setValue
کارهای زیر را انجام می دهد:
- زمانی که مقدار بالای آیتم ( که به
viewport
وابسته است ) برابر ارتفاع پنجره نمایش ( یعنی آیتم در صفحه به نمایش در می آید ) می شود، مقدارtranslation
به میزان کمترین مقدار در نظر گرفته شده ست می شود. - زمانی که مقدار بالای آیتم ( که به
viewport
وابسته است ) برابر منفی ارتفاع خود آیتم می شود ( یعنی آیتم دیگر در صفحه نمایش دیده نمی شود ) مقدارtranslation
بهمیزان بیشترین مقدار در نظر گرفته شده ست می شود.
در واقع ما بالای آیتم ( که به viewport
وابسته است ) را رصد می کنیم و رنج عملیاتی آیتم به شکل زیر است:
[window’s height, -item’s height] to [minVal, maxVal]
در ادامه نیاز داریم ارتفاع آیتم و موقعیت بالای آن را حساب کنیم.
constructor(el) {
...
this.update();
}
update() {
this.getSize();
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
}
this.layout();
}
layout() {
this.DOM.image.style.transform = `translate3d(0,${this.renderedStyles.innerTranslationY.previous}px,0)`;
}
getSize() {
const rect = this.DOM.el.getBoundingClientRect();
this.props = {
height: rect.height,
top: docScroll + rect.top
}
}
همین موارد را وقتی صفحه تغییر اندازه می دهد نیاز خواهیم داشت
initEvents() {
window.addEventListener('resize', () => this.resize());
}
resize() {
this.update();
}
در ادامه با استفاده از IntersectionObserver API متغیر های مربوطه به مواردی که در صفحه در حال نمایش هستند را تغییر می دهیم
constructor(el) {
...
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => this.isVisible = entry.intersectionRatio > 0);
});
this.observer.observe(this.DOM.el);
...
}
کار تمام است. می توانید دمو را از لینک زیر ببینید