Bab 5: Animasi
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)
/* 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)
/* 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
// 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
// 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
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
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)
// 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
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);transitionendfire per-property: Kalau animasiall, event fire berkali-kalifill: 'forwards'bisa konflik: Element "terkunci" di state akhir, CSS normal tidak berlaku- Performance: Animasi
transformdanopacity= GPU-accelerated. Animasiwidth/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?
// ❌ 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
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)
// 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)
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
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
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- Jangan pakai waktu fixed (16ms): Pakai
performance.now()dan hitung delta time - Tab tidak aktif = rAF di-pause: Browser pause rAF saat tab background (ini bagus untuk performa)
- Jangan animasi layout properties:
width,height,top,left= lambat. Pakaitransformdanopacity - Memory leak: Selalu simpan ID dan
cancelAnimationFramesaat component unmount
🏆 Challenge
Buat animasi "Typing Effect":
- Teks muncul satu huruf per satu (kayak diketik)
- Ada cursor berkedip di akhir (pakai CSS animation)
- Setelah selesai ketik, tunggu 2 detik, lalu hapus satu-satu
- Setelah terhapus, ketik teks berikutnya (loop dari array teks)
- Pakai
requestAnimationFrameuntuk timing
// 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 rAFSudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.