3D Animations dengan CSS — Web Animation

3D Animations dengan CSS CSS 3D Transform memungkinkan kamu membuat animasi tiga dimensi tanpa WebGL atau library berat. Browser menggunakan GPU untuk…

CSS 3D memutar elemen datar di ruang tiga dimensi, lalu menggambarnya seperti dilihat mata dari jarak tertentu. Bayangin kartu kamar di meja resepsionis Bu Sri: depannya foto "Teras pagi", belakangnya daftar fasilitas. Kamu membaliknya seperti membalik kartu sungguhan. Seberapa "dramatis" kartu itu terlihat bergantung pada jarak matamu ke kartu (perspective): dekat, tepinya tampak besar sekali; jauh, nyaris datar. Kartu itu juga punya dua sisi yang direkatkan punggung ke punggung, dan kamu cuma melihat sisi yang menghadapmu (backface-visibility).

🧪 Tebakan cepat: Kartu Kamar Teras lebarnya 320 px, diputar rotateY(60deg) di dalam induk dengan perspective: 800px. Berapa px lebarnya yang terlihat? Jawabannya ada di bagian "Matematika rotateY".

perspective: jarak mata ke layar

.scene { perspective: 800px; perspective-origin: 50% 50%; } /* mata 800 px di depan layar, di tengah */
.flip  { transform: rotateY(60deg); }                       /* anak-anaknya diputar di ruang 3D */

Kartu yang bisa dibalik

<div class="scene">
  <div class="flip">
    <div class="face depan">Teras pagi</div>
    <div class="face belakang">Wi-Fi · Sarapan · Air panas · Parkir motor</div>
  </div>
</div>
<button type="button" class="balik" aria-pressed="false">Lihat fasilitas</button>
.scene { perspective: 800px; }
.flip {
  position: relative; height: 200px;
  transform-style: preserve-3d;              /* anak-anaknya ikut di ruang 3D yang sama */
  transition: transform 400ms ease-in-out;
}
.flip.dibalik { transform: rotateY(180deg); }
.face { position: absolute; inset: 0; border-radius: 12px; overflow: hidden;
        backface-visibility: hidden; }       /* sisi yang membelakangi mata disembunyikan */
.belakang { transform: rotateY(180deg); }    /* punggung ke punggung dengan sisi depan */

Matematika rotateY

Tanpa perspective, titik yang berjarak x dari sumbu putar tampak di x × cos θ. Kartu 320 px (setengahnya 160 px):

Sudut cos θ Lebar terlihat
0° 1 320 px
60° 0,5 160 px
90° 0 0 px: kartunya jadi garis, saat sisi depan berganti ke belakang
180° -1 320 px lagi, terbalik

Dengan perspective d, titik yang maju z px ke arah mata tampak dikali d / (d - z). Di 60°, satu tepi maju 160 × sin 60° = 138,6 px dan tepi lainnya mundur sejauh itu:

Itu jawaban tebakannya: 165 px, sedikit lebih lebar dari 160 px karena tepi yang dekat membesar lebih banyak daripada tepi jauh mengecil. getBoundingClientRect() memberi 165 px di Chromium 147, Firefox 146, dan WebKit 26.4.

Jebakan: preserve-3d yang diam-diam jadi datar

Spesifikasi CSS Transforms 2 menyebut nilai-nilai yang memaksa browser menggambar anak-anak sebagai satu gambar datar dulu, sehingga preserve-3d di elemen itu dipakai sebagai flat: overflow selain visible atau clip, opacity di bawah 1, filter selain none, clip-path, clip, isolation: isolate, mask-image, mask-border-source, mix-blend-mode selain normal, dan contain: paint (termasuk content-visibility: hidden).

Kami taruh label harga dengan translateZ(60px) di dalam kartu. Normalnya label 100 px itu tampak 108,1 px (800 / 740). Dengan overflow: hidden, filter: drop-shadow(...), atau opacity: 0.99 di .flip, lebarnya 100 px di ketiga mesin: kedalamannya hilang. Kartu yang dibalik 180° pun menampilkan sisi depan yang terbalik, bukan fasilitasnya. Jebakannya: getComputedStyle tetap bilang preserve-3d, karena yang berubah nilai pakainya (used value). Pasang overflow, sudut bulat, dan bayangan di .face atau .scene, jangan di .flip.

Performa dan "Kurangi gerakan"

@media (prefers-reduced-motion: reduce) {
  .flip, .flip.dibalik { transform: none; transition: none; }
  .belakang { transform: none; opacity: 0; }
  .face { transition: opacity 150ms; }
  .flip.dibalik .depan { opacity: 0; }
  .flip.dibalik .belakang { opacity: 1; }
}

Latihan: membangun string transform

const UNITS = { perspective: 'px', translateX: 'px', translateY: 'px', translateZ: 'px',
                rotateX: 'deg', rotateY: 'deg', rotateZ: 'deg', scale: '' };

function buildTransform(transforms) {
  return transforms.map((t) => t.type + '(' + t.value + (UNITS[t.type] ?? '') + ')').join(' ');
}
function calculateFlipState(isFlipped, axis) {
  return 'rotate' + axis + '(' + (isFlipped ? 180 : 0) + 'deg)';
}
function parseTransformDeg(text, type) {
  const match = text.match(new RegExp(type + '\\((-?[\\d.]+)deg\\)'));
  return match ? Number(match[1]) : 0;
}

⚠️ Jebakan yang sering ketemu

TL;DR: Properti perspective di induk = satu mata untuk semua anak (satu titik hilang, digeser dengan perspective-origin); fungsi perspective() di awal transform = mata sendiri untuk tiap elemen. transform-style: preserve-3d membuat anak-anak berada di ruang 3D induknya; backface-visibility: hidden di kedua sisi menyembunyikan sisi yang membelakangi mata; sisi belakang diberi rotateY(180deg), dan kartunya dibalik dengan rotateY(180deg) lewat tombol, 400 ms. Lebar terlihat = lebar × cos θ (60° = 160 px, 90° = garis); dengan perspective d, titik yang maju z dikali d / (d - z), jadi kartu 320 px di 60° dengan 800px tampak 165 px. overflow (selain visible/clip), opacity < 1, filter, clip-path, mask, isolation, mix-blend-mode, dan contain: paint memaksa preserve-3d jadi datar (label translateZ(60px) 108,1 px jadi 100 px, kartu 180° menampilkan depannya terbalik) walau getComputedStyle tetap bilang preserve-3d. Saat "Kurangi gerakan", ganti putaran dengan crossfade 150 ms. Latihan: perspective(1000px) rotateY(30deg), rotateX(0deg), angka diambil kembali dengan regex -?[\d.]+.

Yang akan kamu pelajari