jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

CSS 3D from Scratch · Part 3 — Interactive Tilt & Parallax

Make 3D respond to the human: a card that tilts toward the cursor in real time, layered parallax depth with translateZ, a moving glare highlight, and the will-change performance trick. Beginner-friendly, with a live tilt demo.

5 MIN READ

Tới giờ, đồ đệ, 3D của con mới chỉ được ngắm từ xa — một cú flip ở đây, một cube ở kia. Hôm nay nó vươn ra và chạm lại người dùng. Ta dựng hiệu ứng con đã thấy trên mọi trang sản phẩm sang chảnh: một tấm card nghiêng về phía con trỏ như có sự sống, với các lớp bên trong lơ lửng ở những độ sâu khác nhau.

Sư phụ nói thật: cái này cần một nhúm JavaScript. CSS không đọc được vị trí chuột. Nhưng JS rất nhỏ — chừng 12 dòng — và CSS vẫn gánh toàn bộ phần 3D nặng nhọc.

Di chuột lên card. Rồi chỉnh max tilt, perspective, và layer depth.

Mở demo đầy đủ:

Công thức

Mọi card nghiêng theo chuột đều là ba bước:

  1. Đặt perspective lên container (máy ảnh — Phần 1).
  2. Trong JS, đọc vị trí con trỏ bên trong card (tỉ lệ 0→1).
  3. Ánh xạ tỉ lệ đó sang rotateX/rotateY và ghi vào transform của card.
<div class="stage">         <!-- the camera -->
  <div class="card">        <!-- the thing that tilts -->
    <div class="layer">…</div>
  </div>
</div>
.stage { perspective: 800px; }
.card {
  transform-style: preserve-3d;     /* so inner layers get real depth */
  transition: transform 0.12s ease-out;  /* smooth, and snaps back on leave */
}

12 dòng JavaScript

Đây là toàn bộ bộ não của hiệu ứng. Đọc comment — toán chỉ là “chuột đang ở đâu, từ 0 đến 1?”

const stage = document.querySelector('.stage');
const card = document.querySelector('.card');
const MAX = 14; // degrees of tilt at the edges

stage.addEventListener('pointermove', (e) => {
  const r = card.getBoundingClientRect();
  const px = (e.clientX - r.left) / r.width;   // 0 (left) → 1 (right)
  const py = (e.clientY - r.top) / r.height;   // 0 (top)  → 1 (bottom)

  const ry = (px - 0.5) * 2 * MAX;   // -MAX → +MAX as cursor goes left→right
  const rx = -(py - 0.5) * 2 * MAX;  // +MAX → -MAX as cursor goes top→bottom

  card.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`;
});

stage.addEventListener('pointerleave', () => {
  card.style.transform = 'rotateX(0deg) rotateY(0deg)'; // snap back flat
});

Hai điểm tinh tế sư phụ muốn con để ý:

  • Ta trừ 0.5 để tâm là trung tính (0°) còn các cạnh là nghiêng tối đa.
  • rotateX bị đảo dấu. Khi con trỏ ở dưới, con muốn cạnh dưới tiến về phía con — đó là rotateX âm. Đảo dấu này thì card nghiêng “ngược” (đôi khi cũng đẹp — thử nút direction).

Ta dùng pointermove (không phải mousemove) để chạy cho chuột, bút, và cảm ứng với một handler.

Parallax: phần làm nên đẳng cấp

Một card phẳng mà nghiêng thì tạm ổn. Một card mà bên trong di chuyển ở tốc độ khác nhau mới là phép thuật. Đó là parallax, và con đã có đủ mọi thứ cần: translateZ.

Cho mỗi lớp bên trong một độ cao Z khác nhau và để cha nghiêng mang chúng theo:

.card { transform-style: preserve-3d; }   /* required, or layers flatten */

.layer.badge { transform: translateZ(70px); }  /* floats high — moves a LOT */
.layer.title { transform: translateZ(40px); }
.layer.chip  { transform: translateZ(55px); }
.layer.foot  { transform: translateZ(20px); }  /* near the surface — barely moves */

Vì sao nó tạo parallax miễn phí? Vì khi cha xoay, một điểm nằm cao 70px so với bề mặt quét qua một cung lớn hơn nhiều so với điểm ở 20px. Lớp cao hơn di chuyển xa hơn trên màn hình — đúng như cách vật gần và xa hành xử ngoài đời.

Kéo layer depth × trong demo từ 1 xuống 0. Ở 0, mọi lớp dẹp về bề mặt và card trở nên vô hồn. Cái slider đó chính là khác biệt giữa “đẹp” và “wow”.

Glare — ánh sáng giả, độ bóng thật

Một vệt sáng di chuyển theo con trỏ đánh lừa mắt thấy một bề mặt bóng đang bắt sáng. Nó chỉ là một radial gradient mà ta dời tâm bằng một biến CSS:

.glare {
  position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(
    circle at var(--gx, 50%) var(--gy, 50%),
    rgba(255,255,255,0.28), transparent 45%
  );
  transform: translateZ(80px); /* sit it above the other layers */
}
glare.style.setProperty('--gx', `${px * 100}%`);
glare.style.setProperty('--gy', `${py * 100}%`);

Ta sẽ làm shading theo hướng đúng nghĩa ở Phần 5. Đây là bản rẻ tiền mà vui mắt.

will-change — mẹo hiệu năng

Khi một thứ nghiêng liên tục, hãy gợi ý trình duyệt nâng nó lên một lớp GPU riêng trước:

.card { will-change: transform; }

Cái này tránh giật ở lần di chuyển đầu. Nhưng nghe lời cảnh báo của sư phụ — will-change là dao mổ, không phải gia vị:

Đừng phết will-change lên mọi thứ. Mỗi cái tốn bộ nhớ GPU. Chỉ dùng trên phần tử thực sự sắp animate, và lý tưởng là thêm/bỏ nó quanh tương tác.

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

  • Quên preserve-3d trên card? Các lớp parallax sẽ dẹp phẳng và chỉ còn lại cái nghiêng.
  • Đọc sai rect. Đo getBoundingClientRect() của card, không phải cửa sổ — nếu không tâm trung tính sẽ lệch khỏi card.
  • Không reset khi rời. Không có reset pointerleave, card đứng hình giữa chừng khi con trỏ rời đi. Luôn bật về phẳng.
  • Tôn trọng reduced motion. Một số người bị chóng mặt. Bọc cái nghiêng trong một kiểm tra (xem Phần 5) và đơn giản là bỏ qua cho họ.

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

  1. Tự dựng card nghiêng: bắt đầu từ một <div> đơn giản có tiêu đề và nút, thêm 12 dòng, và làm nó nghiêng về con trỏ.
  2. Thêm ba lớp parallax: đặt một icon, tiêu đề, và footer ở translateZ 80, 40, và 15px. Cảm nhận chiều sâu hiện ra.
  3. Làm mượt: thay vì ghi transform trực tiếp, hãy nội suy (lerp) về mục tiêu mỗi khung hình bằng requestAnimationFrame cho cảm giác mượt như bơ, có quán tính.
  4. Cả một lưới: làm lưới 3×3 card nghiêng. (Bài học: cho mỗi cái một container perspective riêng, hoặc chung một cái cho khung cảnh thống nhất — thử cả hai.)

Phần tiếp theo

Giờ con làm được 3D phản hồi con người theo thời gian thực, với chiều sâu phân lớp thuyết phục và một vệt sáng di động. Đó là kỹ thuật đứng sau vô số UI “cao cấp”.

Phần 4, ta cho vật thể tự chuyển động — animation 3D thuần CSS: một showcase xoay tròn, và viên ngọc quý, một carousel / coverflow 3D dựng từ đúng rotateYtranslateZ.