Bab 5: Animasi

2 menit baca

5.1 Bezier Curve

Apa Itu Bezier Curve?

Bezier curve adalah kurva matematika yang mengontrol kecepatan animasi (timing function). Menentukan apakah animasi bergerak konstan, pelan-pelan lalu cepat, atau memantul.

Analogi: Bayangkan kamu lempar bola. Bezier curve menentukan BAGAIMANA bola bergerak — apakah langsung cepat (linear), pelan dulu baru ngebut (ease-in), atau ngebut dulu baru pelan (ease-out).

Cubic Bezier

CSS dan JavaScript pakai cubic-bezier dengan 4 titik kontrol: P0, P1, P2, P3.

  • P0 = (0, 0) — titik awal (fixed)
  • P1 = (x1, y1) — kontrol 1 (kamu atur)
  • P2 = (x2, y2) — kontrol 2 (kamu atur)
  • P3 = (1, 1) — titik akhir (fixed)
css
/* Sintaks CSS */
transition-timing-function: cubic-bezier(x1, y1, x2, y2);

/* Preset yang sering dipakai */
transition-timing-function: linear;       /* cubic-bezier(0, 0, 1, 1) */
transition-timing-function: ease;         /* cubic-bezier(0.25, 0.1, 0.25, 1) */
transition-timing-function: ease-in;      /* cubic-bezier(0.42, 0, 1, 1) */
transition-timing-function: ease-out;     /* cubic-bezier(0, 0, 0.58, 1) */
transition-timing-function: ease-in-out;  /* cubic-bezier(0.42, 0, 0.58, 1) */

Visualisasi

ease-in (pelan → cepat): | ___/ | _/ | / | / |_/________ ease-out (cepat → pelan): | /‾‾‾‾ | / | / | / |/________ linear (konstan): | / | / | / | / |/________

Nilai di Luar 0-1 (Bounce Effect)

css
/* y bisa > 1 atau < 0 untuk efek "melewati target" */
transition-timing-function: cubic-bezier(0.5, -0.5, 0.5, 1.5);
/* Animasi "mundur dulu" lalu "melewati target" lalu balik */

Contoh di JavaScript

javascript
// Fungsi cubic bezier sederhana (approximation)
function cubicBezier(x1, y1, x2, y2) {
  return function(t) {
    // Simplified - untuk production pakai library
    const cx = 3 * x1;
    const bx = 3 * (x2 - x1) - cx;
    const ax = 1 - cx - bx;

    const cy = 3 * y1;
    const by = 3 * (y2 - y1) - cy;
    const ay = 1 - cy - by;

    function sampleX(t) { return ((ax * t + bx) * t + cx) * t; }
    function sampleY(t) { return ((ay * t + by) * t + cy) * t; }

    // Newton's method to find t for given x
    let guessT = t;
    for (let i = 0; i < 4; i++) {
      const currentX = sampleX(guessT) - t;
      const currentSlope = (3 * ax * guessT + 2 * bx) * guessT + cx;
      guessT -= currentX / currentSlope;
    }

    return sampleY(guessT);
  };
}

const easeOut = cubicBezier(0, 0, 0.58, 1);
console.log(easeOut(0.5)); // ~0.8 (sudah 80% jalan di waktu 50%)

5.2 CSS Animations dari JavaScript

Transition

javascript
// Trigger CSS transition dari JavaScript
const box = document.getElementById('box');

// Set transition di CSS atau JS
box.style.transition = 'all 0.5s ease-out';

// Ubah property → animasi otomatis jalan!
box.style.transform = 'translateX(200px)';
box.style.opacity = '0.5';
box.style.backgroundColor = 'blue';

Mendeteksi Akhir Animasi

javascript
const box = document.getElementById('box');

// Untuk transition
box.addEventListener('transitionend', (event) => {
  console.log(`Property "${event.propertyName}" selesai animasi`);
  console.log(`Durasi: ${event.elapsedTime}s`);
});

// Untuk @keyframes animation
box.addEventListener('animationend', (event) => {
  console.log(`Animasi "${event.animationName}" selesai`);
  box.classList.remove('animate');
});

// Event lain
box.addEventListener('animationstart', () => console.log('Mulai'));
box.addEventListener('animationiteration', () => console.log('Ulang'));

Contoh: Animasi Berurutan

javascript
function animateSequence(element, animations) {
  return animations.reduce((promise, anim) => {
    return promise.then(() => {
      return new Promise(resolve => {
        element.style.transition = `all ${anim.duration}ms ${anim.easing || 'ease'}`;
        Object.assign(element.style, anim.styles);

        element.addEventListener('transitionend', resolve, { once: true });
      });
    });
  }, Promise.resolve());
}

// Penggunaan
await animateSequence(box, [
  { duration: 500, styles: { transform: 'translateX(200px)' } },
  { duration: 300, styles: { transform: 'translateX(200px) scale(1.5)' } },
  { duration: 500, styles: { transform: 'translateX(0) scale(1)' }, easing: 'ease-in' },
]);
console.log('Semua animasi selesai!');

Web Animations API (Modern)

javascript
// Cara modern — lebih powerful dari CSS transitions
const box = document.getElementById('box');

const animation = box.animate([
  // Keyframes (dari → ke)
  { transform: 'translateX(0)', opacity: 1 },
  { transform: 'translateX(100px)', opacity: 0.5, offset: 0.7 },
  { transform: 'translateX(200px)', opacity: 1 }
], {
  duration: 1000,      // ms
  easing: 'ease-out',
  iterations: 1,       // Infinity untuk loop
  fill: 'forwards',    // tetap di posisi akhir
  delay: 200           // delay sebelum mulai
});

// Kontrol animasi
animation.pause();
animation.play();
animation.reverse();
animation.cancel();

// Event
animation.onfinish = () => console.log('Selesai!');
animation.finished.then(() => console.log('Promise: selesai!'));

// Cek status
console.log(animation.playState); // "running", "paused", "finished"
console.log(animation.currentTime); // waktu saat ini (ms)

Contoh: Fade In/Out Utility

javascript
function fadeIn(element, duration = 300) {
  element.style.display = '';
  const anim = element.animate(
    [{ opacity: 0 }, { opacity: 1 }],
    { duration, fill: 'forwards' }
  );
  return anim.finished;
}

function fadeOut(element, duration = 300) {
  const anim = element.animate(
    [{ opacity: 1 }, { opacity: 0 }],
    { duration, fill: 'forwards' }
  );
  return anim.finished.then(() => {
    element.style.display = 'none';
  });
}

// Penggunaan
await fadeOut(modal);
await fadeIn(notification);
⚠️Jebakan!
  1. transitionend fire per-property: Kalau animasi all, event fire berkali-kali
  2. fill: 'forwards' bisa konflik: Element "terkunci" di state akhir, CSS normal tidak berlaku
  3. Performance: Animasi transform dan opacity = GPU-accelerated. Animasi width/height/top/left = lambat (trigger layout)

5.3 JavaScript Animations (requestAnimationFrame)

Apa Itu requestAnimationFrame?

requestAnimationFrame (rAF) adalah cara browser bilang: "Panggil fungsi ini TEPAT sebelum frame berikutnya di-render." Biasanya 60x per detik (60fps).

Analogi: Bayangkan kamu bikin flipbook (buku gambar yang dibalik cepat). requestAnimationFrame itu kayak browser bilang: "Oke, halaman berikutnya siap digambar — mau gambar apa?"

Kenapa Bukan setInterval?

javascript
// ❌ setInterval — tidak sinkron dengan refresh rate layar
setInterval(() => {
  box.style.left = pos++ + 'px';
}, 16); // 16ms ≈ 60fps, tapi TIDAK TEPAT

// ✅ requestAnimationFrame — sinkron dengan layar
function animate() {
  box.style.left = pos++ + 'px';
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

Animasi Dasar

javascript
function animate(element, from, to, duration) {
  const start = performance.now();

  function frame(currentTime) {
    // Hitung progress (0 → 1)
    let progress = (currentTime - start) / duration;
    if (progress > 1) progress = 1;

    // Terapkan perubahan
    const current = from + (to - from) * progress;
    element.style.transform = `translateX(${current}px)`;

    // Lanjut kalau belum selesai
    if (progress < 1) {
      requestAnimationFrame(frame);
    }
  }

  requestAnimationFrame(frame);
}

// Gerakkan box dari 0 ke 300px dalam 1 detik
animate(box, 0, 300, 1000);

Dengan Timing Function (Easing)

javascript
// Timing functions
const timings = {
  linear: (t) => t,
  easeIn: (t) => t * t,
  easeOut: (t) => 1 - (1 - t) * (1 - t),
  easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
  bounce: (t) => {
    const n1 = 7.5625, d1 = 2.75;
    if (t < 1 / d1) return n1 * t * t;
    if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
    if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
    return n1 * (t -= 2.625 / d1) * t + 0.984375;
  },
  elastic: (t) => {
    if (t === 0 || t === 1) return t;
    return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * (2 * Math.PI / 3));
  }
};

function animateWithEasing(element, property, from, to, duration, easing = 'linear') {
  const start = performance.now();
  const timingFn = timings[easing];

  return new Promise(resolve => {
    function frame(currentTime) {
      let progress = (currentTime - start) / duration;
      if (progress > 1) progress = 1;

      // Apply easing
      const easedProgress = timingFn(progress);
      const current = from + (to - from) * easedProgress;

      element.style[property] = `${current}px`;

      if (progress < 1) {
        requestAnimationFrame(frame);
      } else {
        resolve();
      }
    }

    requestAnimationFrame(frame);
  });
}

// Penggunaan
await animateWithEasing(ball, 'top', 0, 300, 1000, 'bounce');
console.log('Bola sudah memantul!');

Contoh: Animasi Bola Jatuh (Gravity)

javascript
function dropBall(ball, height, bounceFactor = 0.6) {
  let velocity = 0;
  const gravity = 0.5;
  let y = 0;
  let bounces = 0;

  function frame() {
    velocity += gravity;
    y += velocity;

    // Pantul saat menyentuh "lantai"
    if (y >= height) {
      y = height;
      velocity = -velocity * bounceFactor;
      bounces++;
    }

    ball.style.transform = `translateY(${y}px)`;

    // Stop setelah 10 pantulan atau velocity sangat kecil
    if (bounces < 10 && Math.abs(velocity) > 0.5) {
      requestAnimationFrame(frame);
    }
  }

  requestAnimationFrame(frame);
}

dropBall(document.getElementById('ball'), 400);

Contoh: Progress Bar Animasi

javascript
function animateProgress(element, targetPercent, duration = 1000) {
  const start = performance.now();
  const startWidth = parseFloat(element.style.width) || 0;

  function frame(time) {
    let progress = (time - start) / duration;
    if (progress > 1) progress = 1;

    // Ease out
    const eased = 1 - Math.pow(1 - progress, 3);
    const currentWidth = startWidth + (targetPercent - startWidth) * eased;

    element.style.width = `${currentWidth}%`;
    element.textContent = `${Math.round(currentWidth)}%`;

    if (progress < 1) {
      requestAnimationFrame(frame);
    }
  }

  requestAnimationFrame(frame);
}

// Penggunaan
animateProgress(progressBar, 75); // animasi ke 75%

Cancel Animation

javascript
let animationId;

function startAnimation() {
  function frame() {
    // ... animasi ...
    animationId = requestAnimationFrame(frame);
  }
  animationId = requestAnimationFrame(frame);
}

function stopAnimation() {
  cancelAnimationFrame(animationId);
}

// Penggunaan
startAnimation();
setTimeout(stopAnimation, 3000); // stop setelah 3 detik
⚠️Jebakan!
  1. Jangan pakai waktu fixed (16ms): Pakai performance.now() dan hitung delta time
  2. Tab tidak aktif = rAF di-pause: Browser pause rAF saat tab background (ini bagus untuk performa)
  3. Jangan animasi layout properties: width, height, top, left = lambat. Pakai transform dan opacity
  4. Memory leak: Selalu simpan ID dan cancelAnimationFrame saat component unmount

🏆 Challenge

Buat animasi "Typing Effect":

  1. Teks muncul satu huruf per satu (kayak diketik)
  2. Ada cursor berkedip di akhir (pakai CSS animation)
  3. Setelah selesai ketik, tunggu 2 detik, lalu hapus satu-satu
  4. Setelah terhapus, ketik teks berikutnya (loop dari array teks)
  5. Pakai requestAnimationFrame untuk timing
javascript
// Hint:
// - Array teks: ['Hello World!', 'Saya belajar JS', 'Ini keren!']
// - State: currentText, currentChar, isDeleting
// - Speed: ketik 100ms/huruf, hapus 50ms/huruf
// - Pakai performance.now() untuk kontrol timing di rAF

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.