前端網頁動效

-

如何用原生 JS 和 CSS 做 3D carousel?

this.web

前陣子看到台灣服飾品牌 namesake 的網站,它們產品介紹的頁面是使用 3d carousel,覺得超酷 👇

研究一下之後其實不會很難,今天就帶你用原生 JS 和 CSS 做出一樣的特效!👇

原理介紹

整個效果的原理不難,先用 CSS 3d 並控制每個 image 旋轉的角度以及偏移,再利用 JS 控制整個 container 的位移和旋轉效果即可,接下來就讓我們從 HTML 開始看吧!

HTML

HTML 架構不複雜,基本上就是兩層 container,外層負責 x、z 的偏移,內層負責旋轉。

並且在每張圖片宣告 —index 的 css 變數等等用來控制旋轉的角度。

<div class="images">
  <div class="images__rotator">
    <div
      class="images__item"
      style="--index: 0"
    >
      <img src="./assets/1.jpg">
    </div>
    <div
      class="images__item"
      style="--index: 1"
    >
      <img src="./assets/2.jpg">
    </div>
    <div
      class="images__item"
      style="--index: 2"
    >
      <img src="./assets/3.jpg">
    </div>
    <!-- 總共 12 張 -->
    <div
      class="images__item"
      style="--index: 11"
    >
      <img src="./assets/12.jpg">
    </div>
  </div>
</div>

CSS

先將 body 設定 overflow: hidden 以及等等 transition 要用到的 timeline

:root {
  --ease-out-circ: cubic-bezier(0, 0.55, 0.45, 1);
}
body {
  overflow: hidden;
}

接著設定 images 的 style,包括圖片張數、每張圖片的寬度、位移,以及滑鼠滾動的大小。因為要用到 3d 的效果,所以記得加上 transform-style: preserve-3d;

rotator 也設定寬高,讓他撐滿整個螢幕,並也加上 transform-style: preserve-3d;

.images {
  --image-count: 12;
  --image-width: calc(300vw / var(--image-count));
  --shift: 4vw * var(--image-count);
  --wheel-momentum: 0;

  position: relative;
  transform-style: preserve-3d; /* 👈 */
  transition: 2s var(--ease-out-circ);
}

.images__rotator {
  width: 100vw;
  height: 100vh;
  transform-style: preserve-3d;
  transition: 2s var(--ease-out-circ);
}

接著設定每張 image__item 的 style。用 aspect-ration 設定比例並讓 img 稱滿 image__item

.images__item {
  --offset-z: calc(var(--shift) + var(--wheel-momentum) * 0.2px);

  position: absolute;
  width: var(--image-width);
  aspect-ratio: 9 / 16;
  top: 50%;
  left: 50%;

  transform:
    translate3d(-50%, -50%, 0) 
    rotateY(calc((360deg / var(--image-count)) * var(--index))) 
    translateZ(var(--offset-z)) 
    scale(calc(1 - var(--wheel-momentum)));

  transition: transform 0.8s;

  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

transform 看起來稍微複雜一點,讓我一個一個解釋:

  1. translate3d(-50%, -50%, 0) : 將圖片置中
  2. rotateY(calc((360deg / var(--image-count)) * var(--index))) : 讓每張圖依序旋轉一樣的角度
  3. --offset-z: calc(var(--shift) + var(--wheel-momentum) * 0.2px)translateZ(var(--offset-z)) : 當滑鼠滾動時會增加 z 軸的偏移量,讓圖片往外擴展的感覺
  4. scale(calc(1 - var(--wheel-momentum) / 1000)) : 滾動時讓圖片縮小

此時讓我們稍微旋轉一下 .images 的角度就可以看到每張圖片已經排成 3D carousel 的樣子了。

3d-carousel-css

JavaScript

接著來做 JS 吧!先獲得相關的 DOM,接著我們要監聽 wheel 和 mousemove 的事件,並控制 旋轉的角度以及調整 wheelMomentum (滾動力度)。加載完 DOM 後執行 runAnimation 並搭配 requestAnimationFrame 來做動畫。

const images = document.querySelector('.images');
const imagesRotator = document.querySelector('.images__rotator');
const rotationAngles = { x: -4, y: 200, z: 0 };

document.body.addEventListener('wheel', e => {
  const friction = 12;
  const wheel = e.deltaY / friction;

  rotationAngles.y -= wheel;
  setWheelMomentum(wheel);
})
document.body.addEventListener('mousemove', e => {
  const y = e.clientY - (innerHeight / 2);
  const x = e.clientX - (innerWidth / 2);
  rotationAngles.x = y / 60;
  rotationAngles.z = x / 100;
});

function runAnimations() {
  animateImages()
  animateImagesRotator()
  requestAnimationFrame(runAnimations);
}
document.addEventListener('DOMContentLoaded', runAnimations);

有了基本 JS 架構後,我們來看更細節的函數。

animateImages 和 animateImagesRotator 做的事情很簡單,就只是利用 setProperty 控制 DOM 的 style 而已。

const animateImages = () => {
  images.style.setProperty('transform', 
    `rotateX(${rotationAngles.x}deg) rotateZ(${rotationAngles.z}deg)`);
}
const animateImagesRotator = () => {
  imagesRotator.style.setProperty('transform', `rotateY(${rotationAngles.y}deg)`);
}

setWheelMomentum 也不難,基本上就是賦予 css 變數—whell-momentum 的值,並在 1.8 秒後將 --wheel-momentum 設為 0。

let wheelMomentumTimeout = null;
const setWheelMomentum = (momentum = 1) => {
  const friction = 1000;
  images.style.setProperty('--wheel-momentum', Math.abs(momentum / friction));

  clearTimeout(wheelMomentumTimeout);

  wheelMomentumTimeout = setTimeout(() => {
    images.style.setProperty('--wheel-momentum', 0);
  }, 1800);
}

到這邊就大功告成了!其實沒有想像中難吧!如果想要更好的動畫效果,可以嘗試用 gsap 動畫函式庫來調整,會讓整個動畫更滑順。

那今天就這樣!下篇貼文見囉~!

相關系列文章