ساختن جلوه اختلاف منظر در پیمایش

جلوه اختلاف منظر در پیمایش یک تکنیک بصری است که در آن لایه‌های مختلف محتوا با سرعت‌های متفاوتی حرکت می‌کنند وقتی کاربر صفحه را به پایین پیمایش می‌کند. این کار توهم عمق ایجاد می‌کند، به‌طوری‌که به‌نظر می‌رسد عناصر پیش‌زمینه سریع‌تر از عناصر پس‌زمینه حرکت می‌کنند و درنتیجه تجربه مروری جذاب و فراگیر ایجاد می‌شود. این جلوه بااستفاده از «پویانمایی‌های پیمایش‌محور CSS» که به شما امکان می‌دهد پویانمایی‌ها را به موقعیت پیمایش محفظه پیوند دهید، به بهترین شکل حاصل می‌شود.

نحوه پیاده‌سازی

نحوه ایجاد جلوه اختلاف منظر پایه در اینجا آمده است:

  1. ایجاد عنصر پوشاننده: این عنصر به‌سادگی همه لایه‌های جلوه اختلاف منظر را باهم گروه‌بندی می‌کند. عنصر قابل‌پیمایش نیست، بنابراین سرریز آن باید بریده شود. همچنین height به آن بدهید که با ارتفاع یکی از لایه‌های جلوه اختلاف منظر مطابقت داشته باشد.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. اعلام لایه‌ها: در داخل پوشاننده، لایه‌های جداگانه‌ای را که با سرعت‌های مختلف حرکت می‌کنند اضافه کنید.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. پویانمایی ترجمه اضافه کنید: پویانمایی CSS تعریف کنید که خصوصیت transform لایه‌ها را تغییر دهد. برای جلوه اختلاف منظر، معمولاً از translateY برای حرکت دادن عمودی لایه‌ها استفاده می‌کنید.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. view-timeline را راه‌اندازی کنید: برای پیوند دادن پویانمایی به موقعیت پیمایش، یک view-timeline روی عنصر پوشاننده ایجاد کنید و سپس آن را روی لایه‌ها اعمال کنید.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. پویانمایی‌ها را به‌صورت متناوب پخش کنید: برای اینکه لایه‌ها با سرعت‌های مختلف حرکت کنند، می‌توانید از یکی از دو رویکرد اصلی استفاده کنید: متناوب کردن در قاب‌های کلیدی، یا متناوب کردن animation-range.

    هر دو رویکرد می‌توانند از مقادیر کدگذاری‌شده استفاده کنند یا از پیاده‌سازی sibling-index()/sibling-count() استفاده کنند. مقادیر کدشده سخت ساده‌ترین هستند و همچنین زمانی مفید هستند که فقط تعداد محدودی لایه وجود داشته باشد. پیاده‌سازی sibling-index()/sibling-count() زمانی مفید است که لایه‌های زیادی داشته باشید.

    • تلوتلو خوردن در قاب‌های کلیدی:

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

      .layer:nth-child(1) { --offset: 100px; }
      .layer:nth-child(2) { --offset: 200px; }
      .layer:nth-child(3) { --offset: 300px; }
      
      @keyframes parallax {
        from {
          transform: translateY(var(--offset));
        }
      }
      

      بااستفاده از sibling-index()، اجازه دهید تابع sibling-index() نمایه عنصر فرزند را در میان عناصر هم‌رده‌اش برگرداند تا جلوه متناوب به‌طور خودکار محاسبه شود.

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • تغییر دادن animation-range:

      بااستفاده از مقادیر کدگذاری‌شده، می‌توانید مرزهای animation-range را به‌طور صریح در هر لایه به‌صورت جداگانه تعریف کنید.

      .layer:nth-child(1) { animation-range: entry 25% exit 50%; }
      .layer:nth-child(2) { animation-range: entry 25% exit 75%; }
      .layer:nth-child(3) { animation-range: entry 25% exit 100%; }
      

      بااستفاده از sibling-index() و sibling-count()، می‌توانید محدوده را به‌صورت ریاضی براساس تعداد کل لایه‌ها (sibling-count()) محاسبه کنید.

      .layer {
        animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
      }
      

کد نمونه

@keyframes parallax {
  from {
    transform: translateY(calc(100px * sibling-index()));
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

می‌توانید از خصوصیت animation-range هم برای رسیدن به نتیجه مشابه استفاده کنید:

@keyframes parallax {
  from {
    transform: translateY(700px);
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
  animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

روال‌های مطلوب

هنگام استفاده از پویانمایی‌های پیمایش‌محور، مهم است که چند روال مطلوب را دنبال کنید تا تجربه‌ای روان و دسترس‌پذیر را تضمین کنید:

  • باید تشخیص ویژگی را اضافه کنید: همه مرورگرها از پویانمایی‌های پیمایش‌محور پشتیبانی نمی‌کنند. از @supports ((animation-timeline: view()) and (animation-range: entry)) برای بررسی پشتیبانی و ارائه بازگشت به روش پیش‌فرض برای مرورگرهایی که از آن پشتیبانی نمی‌کنند استفاده کنید.
    • بررسی (animation-range: entry) باید در اینجا گنجانده شود تا مرورگرهایی که فقط پشتیبانی جزئی دارند فیلتر شوند.
    • از بسته scroll-timeline-polyfill برای راهبرد جایگزین استفاده نکنید زیرا ویژگی‌های آن کامل نیست و مشکلات شناخته‌شده زیادی دارد.
    • اگر پویانمایی فقط تزئینی درنظر گرفته می‌شود، «ارتقای پیش‌رو» را انتخاب کنید و گزینه جایگزین ارائه ندهید.
  • باید به اولویت‌های کاربر احترام بگذارید: برخی‌از کاربران ترجیح می‌دهند حرکت کمتری در وب داشته باشند. از پُرسمان رسانه prefers-reduced-motion برای غیرفعال کردن یا کاهش پویانمایی‌ها برای این کاربران استفاده کنید.
  • باید سعی کنید فقط دارایی‌های CSS کارا را پویانمایی کنید: برای داشتن روان‌ترین پویانمایی‌ها، فقط دارایی‌هایی را پویانمایی کنید که رشته ترکیب‌کننده مرورگر می‌تواند آن‌ها را مدیریت کند، مثل transform و opacity. پویانمایی کردن ویژگی‌های دیگر مثل width یا height می‌تواند به مشکلات عملکردی منجر شود.
  • باید از ترتیب صحیح اعلام استفاده کنید: هنگام استفاده از ویژگی خلاصه animation، animation-timeline و animation-range را بعداز آن اعلام کنید تا از بازنشانی خط زمان توسط خلاصه جلوگیری شود.

برای تنظیم animation-range:

  • باید به همه لایه‌ها جابه‌جایی شروع یکسانی بدهید، مثلاً entry 25%
  • باید به همه لایه‌ها یک جابه‌جایی پایانی متفاوت بدهید که از sibling-count() و sibling-index() برای توزیع جابه‌جایی‌ها استفاده می‌کند، مثلاً exit calc(100% / sibling-count() * sibling-index()).

پشتیبانی مرورگر و راهبردهای جایگزین

بنابراین، معمولاً به یک استراتژی جایگزین نیاز است.

برای مرورگرهایی که از پویانمایی‌های پیمایش‌محور پشتیبانی نمی‌کنند، می‌توانید از یک جایگزین برای بازآفرینی جلوه‌های دیداری استفاده کنید. جایگزین‌ها معمولاً با شنونده پیمایش (برای جلوه‌های ScrollTimeline) یا IntersectionObserver API (برای جلوه‌های ViewTimeline) ساخته می‌شوند.

در مرورگرهایی که پشتیبانی داخلی از پویانمایی‌های پیمایش‌محور دارند، همیشه از پیاده‌سازی CSS بومی استفاده کنید زیرا عملکرد بهتری دارند.

توجه داشته باشید که همه جلوه‌ها را نمی‌توان بااستفاده از رویکرد جایگزین بازآفرینی کرد.

به‌طور خاص برای این مورد استفاده، دستورگان زیر برای مرورگرهایی که از پویانمایی‌های پیمایش‌محور پشتیبانی نمی‌کنند، جایگزین را اعمال می‌کند. این کد از IntersectionObserver برای ردیابی رؤیت‌پذیری عنصر .wrapper استفاده می‌کند و ویژگی transform لایه‌ها را براساس موقعیت پیمایش به‌روز می‌کند.

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const wrapper = document.querySelector('.wrapper');
  const layers = document.querySelectorAll('.layer');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        window.addEventListener('scroll', onScroll);
      } else {
        window.removeEventListener('scroll', onScroll);
      }
    });
  }, { threshold: 0 });

  observer.observe(wrapper);

  function onScroll() {
    const scrollY = window.scrollY;
    const wrapperRect = wrapper.getBoundingClientRect();
    const wrapperTop = wrapperRect.top + scrollY;
    const wrapperHeight = wrapperRect.height;
    const windowHeight = window.innerHeight;

    if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
      const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
      
      layers.forEach((layer, index) => {
        // This matches the effect as defined in the CSS example above.
        // Customize this further if needed.
        const initialTranslateY = 100 * index;
        const translateY = initialTranslateY * (1 - scrollPercent);
        layer.style.transform = `translateY(${translateY}px)`;
      });
    }
  }

  // Trigger onScroll once to set initial positions
  onScroll();
}