پیاده‌سازی انیمیشن‌های مبتنی بر اسکرول

گردآوری و تالیف : ارسطو عباسی
تاریخ انتشار : 22 مرداد 1398
دسته بندی ها : جاوا اسکریپت

یک نوع از انیمیشن وجود دارد که استفاده از آن تمام نمی‌شود، انگار تکنیکی است که هیچگاه قرار نیست قدیمی شود، چرا که در بسیاری از وبسایت‌های مدرن و البته وبسایت‌های قدیمی نیز استفاده می‌شود. این انیمیشن‌ها مبتنی بر رویداد scroll در جاوااسکریپت هستند. زمانی که استفاده از جلوه‌های پارالکس در اوج خود بود استفاده از این شکل انیمیشن‌ها نیز بسیار زیاد شد.

اما واقعیت آن است که در زمان پیاده‌سازی این حالت در وبسایت، نیاز است که بسیار دقت کنید چرا که این انیمیشن‌ها روی کارایی وبسایت‌تان بسیار تاثیرگذار هستند. انیمیشن‌های مبتنی بر رویداد پردازنده را بسیار درگیر می‌کنند، حال اگر کاربران‌تان از موبایل نیز استفاده بکنند، اهمیت کارایی دو چندان می‌شود. 

به همین دلیل است که باید بهینه‌ترین شکل ممکن از کدهای جاوااسکریپت را برای پیاده‌سازی چنین قابلیتی بنویسید. در این آموزش قرار است از کدهای جاوااسکریپت به صورت خام و بدون به کارگیری هیچگونه کتابخانه‌ای استفاده کنیم. سعی داریم تا بهینه‌ترین حالت ممکن را پیاده‌سازی کنیم.

قبل از اینکه شروع کنیم می‌توانید اطلاعات این پروژه را در لینک‌های زیر مشاهده کنید:

بیایید شروع کنیم:

ساختار HTML

قرار است که در این پروژه از یک ساختار ساده برای HTML استفاده کنیم. به ازای هر تصویر در این ساختار از یک div استفاده می‌کنیم. همچنین برای قرار دادن تصاویر و موقعیت‌دهی به آن‌ها از CSS استفاده خواهیم کرد.

<!-- The `.container` element will contain all the images -->
<!-- It will be used also to perform the custom scroll behavior -->
<div class="container">
  <!-- Each following `div` correspond to one image -->
  <!-- The images will be set using CSS backgrounds -->
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
  <div class="image"></div>
</div>

اعمال استایل‌های CSS

ابتدا بیایید یک لایه‌بندی درست را با استفاده از کدهای CSS ایجاد کنیم. برای اینکار قصد داریم از CSS Grid استفاده کنیم. 

// The container for all images
.container {
  // 2 columns grid
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 0 10%;
  justify-items: end; // This will align all items (images) to the right

  // Fixed positioned, so it won't be affected by default scroll
  // It will be moved using `transform`, to achieve a custom scroll behavior
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
}

به عنوان یک نکته مهم ما از مقدار position: fixed; برای کلاس .container استفاده کرده‌ایم. این کار به این دلیل انجام شده که .container از رفتار طبیعی اسکرول تاثیر نگیرد، چرا که ما قصد ساخت یک حالت سفارشی را داریم.

حال نیاز است که تصاویر را به صفحه اضافه کنیم. همانطور که گفته شد برای انجام این کار قصد داریم تا از CSS استفاده کنیم:

// Styles for image elements
// Mainly positioning and background styles
.image {
  position: relative;
  width: 300px;
  height: 100vh;
  background-repeat: no-repeat;
  background-position: center;

  // This will align all even images to the left
  // For getting centered positioned images, respect to the viewport
  &:nth-child(2n) {
    justify-self: start;
  }

  // Set each `background-image` using a SCSS `for` loop
  @for $i from 1 through 10 {
    &:nth-child(#{$i}) {
      background-image: url('../img/image#{$i}.jpg');
    }
  }
}

حال بیایید با استفاده از مدیاکوئری برای صفحات کوچکتر یکسری تنظیمات جدید را اعمال کنیم:

// Adjusting layout for small screens
@media screen and (max-width: 760px) {
  .container {
    // 1 column grid
    grid-template-columns: 1fr;
    // Fix image centering
    justify-items: center;
  }

  // Fix image centering
  .image:nth-child(2n) {
    justify-self: center;
  }
}

تا به اینجای کار استایل کلی وبسایت طراحی شده است و حال زمان بررسی کدهای اصلی مربوط به این پروژه است. در چند قدم بعدی قصد داریم تا با استفاده از جاوااسکریپت انیمیشن مورد نظرمان را ایجاد کنیم.

پیاده‌سازی انیمیشن با استفاده از جاوااسکریپت

حال بیایید شیوه پیاده‌سازی یک انیمیشن را با استفاده از جاوااسکریپت خام -بدون استفاده از کتابخانه- یاد بگیریم. قصد داریم در پیاده‌سازی این انیمیشن‌ها از ترکیب المان‌های محلی و مواردی که توسط خودمان سفارشی می‌شود استفاده کنیم.

توابع و متغیرهای مفید

ابتدا بیایید به توابعی که قرار است از آن‌ها استفاده کنیم نگاهی بیاندازیم. برای درک بهتر این موارد می‌توانید کامنت‌ها را مطالعه کنید:

// Easing function used for `translateX` animation
// From: https://gist.github.com/gre/1650294
function easeOutQuad (t) {
  return t *_ (2 - t)
}

// Returns a random number (integer) between __`min`__ and __`max`__
function random (min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min_
}

// Returns a random number as well, but it could be negative also
function randomPositiveOrNegative (min, max) {
  return random(min, max) * (Math.random() > 0.5 ? 1 : -1)
}

// Set CSS `tranform` property for an element
function setTransform (el, transform) {
  el.style.transform = transform
  el.style.WebkitTransform = transform
}

در کنار توابع، می‌توانید در قطعه کد زیر متغیرهایی که قرار است از آن‌ها استفاده کنیم را مشاهده نمایید:

// Current scroll position
var current = 0
// Target scroll position
var target = 0
// Ease or speed for moving from `current` to `target`
var ease = 0.075
// Utility variables for `requestAnimationFrame`
var rafId = undefined
var rafActive = false
// Container element
var container = document.querySelector('.container')
// Array with `.image` elements
var images = Array.prototype.slice.call(document.querySelectorAll('.image'))
// Variables for storing dimmensions
var windowWidth, containerHeight, imageHeight

// Variables for specifying transform parameters (max limits)
var rotateXMaxList = []
var rotateYMaxList = []
var translateXMax = -200

// Popullating the `rotateXMaxList` and `rotateYMaxList` with random values
images.forEach(function () {
  rotateXMaxList.push(randomPositiveOrNegative(20, 40))
  rotateYMaxList.push(randomPositiveOrNegative(20, 60))
})

حال با آماده شدن این موارد بیایید شیوه پیاده‌سازی یک اسکرول سفارشی را مشاهده کنیم.

همانطور که می‌دانید ما از position:fixed; برای .container استفاده کردیم، بنابراین قابلیت اسکرولینگ طبیعی را برداشته و حال نیاز است که یک حالت سفارشی را ایجاد کنیم. برای انجام چنین کاری نیاز است که از طریق جاوااسکریپت یک div را به body اضافه کنیم. 

// The `fakeScroll` is an element to make the page scrollable
// Here we are creating it and appending it to the `body`
var fakeScroll = document.createElement('div')
fakeScroll.className = 'fake-scroll'
document.body.appendChild(fakeScroll)
// In the `setupAnimation` function (below) we will set the `height` properly

کلاس .face-scroll به یکسری استایل‌های CSS نیاز خواهد داشت، برای اینکار به فایل CSS برگشته و استایل زیر را به آن اضافه کنید:

// The styles for a `div` element (inserted with Javascript)
// Used to make the page scrollable
// Will be setted a proper `height` value using Javascript
.fake-scroll {
  position: absolute;
  top: 0;
  width: 1px;
}

برای محاسبات مربوط به اندازه صفحه و المان‌های پایه‌ای اسکرولینگ قصد داریم تا تابع setupAnimation() را ایجاد کنیم. کدهای این تابع به صورت زیر خواهد بود:

// Geeting dimmensions and setting up all for animation
function setupAnimation () {
  // Updating dimmensions
  windowWidth = window.innerWidth
  containerHeight = container.getBoundingClientRect().height
  imageHeight = containerHeight / (windowWidth > 760 ? images.length / 2 : images.length)
  // Set `height` for the fake scroll element
  fakeScroll.style.height = containerHeight + 'px'
  // Start the animation, if it is not running already
  startAnimation()
}

زمانی که تابع setupAnimation فراخوانی شود، قابلیت اسکرولینگ در صفحه فعال شده و همه چیز آماده اجرا کردن رویداد scroll است.

// Update scroll `target`, and start the animation if it is not running already
function updateScroll () {
  target = window.scrollY || window.pageYOffset
  startAnimation()
}

// Listen for `scroll` event to update `target` scroll position
window.addEventListener('scroll', updateScroll)

هر بار که شما رویداد scroll را صدا می‌زنید مقدار متغیر target برابر با موقعیت جدیدی خواهد بود. اگر دقت کنید متوجه خواهید شد که یک تابع با نام startAnimation در کدهای بالا فراخوانی شده  که هنوز آن را پیاده‌سازی نکرده‌ایم:

// Start the animation, if it is not running already
function startAnimation () {
  if (!rafActive) {
    rafActive = true
    rafId = requestAnimationFrame(updateAnimation)
  }
}

یکی دیگر از توابع مهمی که در بحث محاسبات قرار است استفاده شود updateAnimation است. برای درک بهتر این تابع بیایید کدهای آن را مشاهده کنیم:

// Do calculations and apply CSS `transform`s accordingly
function updateAnimation () {
  // Difference between `target` and `current` scroll position
  var diff = target - current
  // `delta` is the value for adding to the `current` scroll position
  // If `diff < 0.1`, make `delta = 0`, so the animation would not be endless
  var delta = Math.abs(diff) < 0.1 ? 0 : diff * ease

  if (delta) { // If `delta !== 0`
    // Update `current` scroll position
    current += delta
    // Round value for better performance
    current = parseFloat(current.toFixed(2))
    // Call `update` again, using `requestAnimationFrame`
    rafId = requestAnimationFrame(updateAnimation)
  } else { // If `delta === 0`
    // Update `current`, and finish the animation loop
    current = target
    rafActive = false
    cancelAnimationFrame(rafId)
  }

  // Update images (explained below)
  updateAnimationImages()

  // Set the CSS `transform` corresponding to the custom scroll effect
  setTransform(container, 'translateY('+ -current +'px)')
}

تا به اینجای کار اسکرول سفارشی ما آماده است. بعد از فراخوانی تابع setupAnimation می‌توانید به صورت عادی در صفحه اسکرول کنید.

حال تنها قدمی که باقی مانده این است که به تصاویر در حالت اسکرولینگ قابلیت‌های متحرک‌سازی بدهیم.

متحرک‌سازی تصاویر با اسکرولینگ

برای متحرک‌سازی تصاویر از موقعیت کنونی اسکرول سفارشی استفاده می‌کنیم و بعد از آن مقدار intersectionratio بین هر کدام از تصاویر را اندازه می‌گیریم. بعد از آن تنها کافی‌ست که transformation مورد نظر را روی تصاویر اعمال کنیم.

// Calculate the CSS `transform` values for each `image`, given the `current` scroll position
function updateAnimationImages () {
  // This value is the `ratio` between `current` scroll position and images `height`
  var ratio = current / imageHeight
  // Some variables for using in the loop
  var intersectionRatioIndex, intersectionRatioValue, intersectionRatio
  var rotateX, rotateXMax, rotateY, rotateYMax, translateX

  // For each `image` element, make calculations and set CSS `transform` accordingly
  images.forEach(function (image, index) {
    // Calculating the `intersectionRatio`, similar to the value provided by
    // the IntersectionObserver API
    intersectionRatioIndex = windowWidth > 760 ? parseInt(index / 2) : index
    intersectionRatioValue = ratio - intersectionRatioIndex
    intersectionRatio = Math.max(0, 1 - Math.abs(intersectionRatioValue))
    // Calculate the `rotateX` value for the current `image`
    rotateXMax = rotateXMaxList[index]
    rotateX = rotateXMax - (rotateXMax _ intersectionRatio)
    rotateX = rotateX.toFixed(2)
    // Calculate the _`rotateY`_ value for the current _`image`_
    rotateYMax = rotateYMaxList_[_index]
    rotateY = rotateYMax - (rotateYMax _ intersectionRatio)
    rotateY = rotateY.toFixed(2)
    // Calculate the `translateX` value for the current `image`
    if (windowWidth > 760) {
      translateX = translateXMax - (translateXMax * easeOutQuad(intersectionRatio))
      translateX = translateX.toFixed(2)
    } else {
      translateX = 0
    }
    // Invert `rotateX` and `rotateY` values in case the image is below the center of the viewport
    // Also update `translateX` value, to achieve an alternating effect
    if (intersectionRatioValue < 0) {
      rotateX = -rotateX
      rotateY = -rotateY
      translateX = index % 2 ? -translateX : 0
    } else {
      translateX = index % 2 ? 0 : translateX
    }
    // Set the CSS `transform`, using calculated values
    setTransform(image, 'perspective(500px) translateX('+ translateX +'px) rotateX('+ rotateX +'deg) rotateY('+ rotateY +'deg)')
  })
}

شروع انیمیشن

برای اجرا کردن پروژه باید تابع setupAnimation را فراخوانی کنیم. همچنین نیاز است برای محاسبه مجدد ابعاد صفحه به رویداد resize توجه کنیم:

// Listen for `resize` event to recalculate dimmensions
window.addEventListener('resize', setupAnimation)

// Initial setup
setupAnimation()

در پایان

در این مطلب سعی کردیم تا شما را با شیوه پیاده‌سازی انیمیشن‌های مبتنی بر اسکرول آشنا کنیم. همچنین اگر قصد دسترسی به کدهای این پروژه را داشته باشید می‌توانید به این مخزن گیت‌هاب مراجعه کنید.

منبع

مقالات پیشنهادی

  • افکت های اسکرول برای صفحات وبسایت

    امروز قصد داریم یک کتابخانه کوچک برای افکت های اسکرول ، صفحه های خودمون بسازیم . ما با قرار دادن برخی از افکت های فانتزی قصد داریم زمانی که کاربر در...

    حسام موسوی
  • پیاده‌سازی اسکرول کردن نرم در React

    آیا نمی‌دانید که اسکرول کردن نرم چیست؟ خب، با استفاده از این قابلیت، به جای این کاربر که بر روی یک دکمه کلیک کند و به سرعت به بخش دیگری از صفحه مشابه...

    عرفان کاکایی