jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

CSS 3D from Scratch · Part 4 — 3D Motion & Carousels

Set 3D objects in motion: pure-CSS keyframe spins, arranging cards in a ring with rotateY and translateZ, building a real 3D carousel with prev/next and auto-rotate, and the idea behind Apple-style coverflow. With a live demo.

5 MIN READ

Vật thể của con đã đứng được, đồ đệ — giờ dạy chúng nhảy múa. Bài này ta cho 3D chuyển động, và dựng thứ khiến người ta thốt lên “làm cái đó bằng CSS á?”: một carousel 3D thật.

Đây là bí mật đẹp đẽ: carousel chính là cái cube ở Phần 2 với nhiều mặt hơn, uốn thành vòng tròn. Nếu con hiểu cube, con đã hiểu cái này rồi.

Quay nó bằng Prev/Next (hoặc phím mũi tên), rồi đổi số item và bán kính.

Mở demo đầy đủ:

Trước hết, chuyển động cách dễ: keyframes

Con đã gặp ở cube. Mọi transform 3D đều animate được như mọi giá trị CSS khác:

@keyframes orbit {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}
.showcase {
  transform-style: preserve-3d;          /* keep the depth while spinning */
  animation: orbit 8s linear infinite;
}

Hai nhắc nhở từ các phần trước, càng quan trọng khi chuyển động:

  • Cha cần preserve-3d, không thì cú xoay dẹp thành bánh kếp 2D.
  • Chỉ animate transform (và opacity) để mượt, do GPU lo. Đừng animate top/left/width (Phần 5 giải thích sâu).

Giờ tới món chính. Ta muốn N card cách đều quanh một vòng tròn, tất cả quay mặt ra ngoài. Công thức là công thức của cube — xoay để nhắm, translateZ để đẩy ra — nhưng góc giờ là 360° ÷ N thay vì cố định 90°.

<div class="stage">          <!-- camera -->
  <div class="ring">         <!-- the rotating circle -->
    <div class="ring-card">1</div>
    <div class="ring-card">2</div>
    <!-- ...N cards... -->
  </div>
</div>
.stage { perspective: 1100px; }
.ring {
  position: relative;
  transform-style: preserve-3d;
  transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); /* smooth slot changes */
}
.ring-card { position: absolute; inset: 0; }

Mỗi card được đặt bằng một chút JS (vì góc phụ thuộc vào có bao nhiêu cái):

const n = 6;
const angle = 360 / n;        // 60° between cards
const radius = 260;           // how far from the center

cards.forEach((card, i) => {
  // Aim card i around the circle, then push it out to the rim.
  card.style.transform = `rotateY(${i * angle}deg) translateZ(${radius}px)`;
});

Đọc transform của card i như các mặt cube: xoay không gian cục bộ của nó i × góc, rồi translateZ đẩy ra dọc theo Z của chính nó đã xoay — nên nó đáp lên vành, quay mặt ra ngoài. Card 0 quay ra trước, card 1 ở 60° vòng quanh, cứ thế.

Đưa một card ra trước

Để hiện card k, xoay cả vòng theo chiều ngược lại k × góc:

ring.style.transform = `translateZ(-${radius}px) rotateY(${-k * angle}deg)`;

Có hai chuyện xảy ra, đều quan trọng:

  • rotateY(-k × góc) xoay vòng để card k quay ra trước.
  • translateZ(-bán kính) đẩy cả vòng lùi đúng bằng bán kính, để card trước nằm gần mặt phẳng máy ảnh thay vì lơ lửng bán kính pixel trước mọi thứ.

.ringtransition, mỗi lần đổi slot đều trôi mượt. Prev/Next chỉ là k-- / k++:

nextBtn.onclick = () => { k++; show(k); };
prevBtn.onclick = () => { k--; show(k); };

Để ý k được phép vượt quá n hoặc âm — vòng cứ quay cùng một chiều mãi, cảm giác tự nhiên hơn là giật ngược. Chỉ dùng phép chia lấy dư để quyết định card nào đang “active” để tô sáng.

Nghịch slider itemsradius. Nhiều item cần bán kính lớn hơn không thì chồng lên nhau; ít item với bán kính lớn thì cách xa nhau.

Coverflow — kinh điển nhà Apple

Coverflow là anh em “phẳng” của vòng tròn: thay vì vòng tròn đầy đủ, các item nằm thành một hàng, và những cái lệch tâm bị xoay nghiêng ra như cuốn sách mở.

/* center item faces you; side items angle away and sit slightly back */
.cover.left   { transform: translateX(-120px) rotateY(45deg)  translateZ(-80px); }
.cover.center { transform: translateX(0)       rotateY(0deg)   translateZ(0);    }
.cover.right  { transform: translateX(120px)  rotateY(-45deg) translateZ(-80px); }

Cùng các primitive — translateX, rotateY, translateZ — xếp thành đường thẳng thay vì vòng tròn. Một khi con thấy carousel, coverflow, và cube đều là “rotate + translateZ theo một quy luật”, con có thể tự sáng tạo cách sắp xếp riêng: xoắn ốc, hình quạt, bức tường.

Lời cảnh báo của sư phụ

  • Đừng animate left/margin để dời card. Dùng transform để chuyển động ở lại trên GPU. Carousel animate layout sẽ giật trên điện thoại.
  • Để ý backface-visibility. Card quay lưng trong vòng sẽ lộ mặt sau. Ẩn chúng (hidden) trừ khi con có thiết kế mặt sau.
  • Overflow cắt mất phép thuật. Nếu cha có overflow: hidden, nó có thể dẹp preserve-3d (Phần 2). Cho vòng 3D không gian thở.
  • Giữ thân thiện bàn phím. Carousel chỉ phản hồi kéo sẽ loại người dùng bàn phím — đấu phím mũi tên và <button> thật, như demo.

Luyện tập, không thì coi như chưa học

  1. Dựng vòng: đặt 6 card với rotateY(i*60deg) translateZ(260px) và đấu Prev/Next. Đưa một card ra trước gọn gàng.
  2. Cho tự chạy: tự tiến mỗi 1.4s bằng setInterval, và dừng khi hover/focus.
  3. Tính bán kính: thay vì đoán, tính bán kính vừa cho N card rộng w không chồng: radius = (w/2) / tan(180°/N).
  4. Dựng coverflow: xếp 5 cover thành hàng, xoay các cái bên, và trượt hàng trái/phải để đổi item trung tâm.

Phần tiếp theo

Giờ con animate được 3D và dựng được các tương tác đỉnh — showcase xoay, vòng, và coverflow — tất cả từ rotateY + translateZ.

Phần 5, bài cuối, ta làm cho nó sẵn sàng production: ánh sáng và shading giả để vật thể trông thật sự đặc, rồi phần nghề không hào nhoáng-nhưng-thiết-yếu — hiệu năng, khả năng tiếp cận, fallback duyên dáng, và cách debug 3D khi nó trục trặc.