Canvas Animation — Creative Web

Canvas Animation Animasi di canvas bekerja dengan prinsip yang sama seperti film — menampilkan serangkaian gambar (frame) dengan sangat cepat sehingga…

Animasi di canvas adalah menggambar ulang seluruh gambar berkali-kali per detik, tiap kali sedikit berbeda. Bayangin buku flipbook: tiap halaman digambar lengkap, cangkirnya juga, padahal cuma uapnya yang bergeser sedikit. Dibalik cepat, uapnya kelihatan naik. Di lesson ini, uap di atas cangkir Sablon Kopi Raka kita bikin bergerak.

🧪 Tebakan cepat: Raka menulis y -= 1 tiap frame, dan di laptopnya (layar 60 Hz) uapnya naik pas. Di HP dengan layar 120 Hz, uapnya naik lebih cepat, sama, atau lebih lambat? Tebak dulu, jawabannya ada di bagian "Gerak per frame vs gerak per detik".

Loop dengan requestAnimationFrame

requestAnimationFrame(fungsi) meminta browser memanggil fungsi tepat sebelum menggambar frame berikutnya. Frame adalah satu gambar yang tampil di layar. Browser memanggilnya sebanyak refresh rate layar: 60 kali per detik di layar 60 Hz, 120 kali di layar 120 Hz. Supaya jadi loop, fungsinya meminta frame berikutnya lagi:

function frame(now) {
  // now: waktu frame ini dalam milidetik, dikirim browser
  // ...update dan gambar...
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Kenapa bukan setInterval(fn, 16)? Interval gak sinkron dengan layar, jadi kadang dua update jatuh di satu frame dan gerakannya patah-patah. Di tab yang disembunyikan, interval tetap jalan (dibatasi browser jadi paling sering sekali per detik), sedangkan requestAnimationFrame berhenti.

Satu frame: hapus, update, gambar ulang

Canvas cuma menyimpan pixel, gak ada "objek uap" yang bisa digeser. Jadi tiap frame:

  1. Hapus frame lama: ctx.clearRect(0, 0, 300, 300) membuat seluruh canvas transparan lagi.
  2. Update posisi tiap partikel.
  3. Gambar ulang semuanya: sablonnya juga, karena clearRect ikut menghapusnya.

Tanpa langkah 1, uap baru digambar di atas uap lama, dan yang terlihat jadi jejak putih tebal, bukan uap yang naik. Dengan clearRect tapi tanpa menggambar ulang sablonnya, cangkirnya hilang dan tinggal uap di canvas kosong. Kalau gambar pertamamu mengecat seluruh canvas (kain hitam fillRect(0, 0, 300, 300)), kain itu sudah menimpa frame lama, dan clearRect jadi gak wajib.

Gerak per frame vs gerak per detik

Gerak per frame (y -= 1) menggeser 1 px tiap kali fungsi dipanggil. Jaraknya per detik ikut banyaknya frame. Gerak per detik memakai dt (delta time): berapa detik berlalu sejak frame sebelumnya, dihitung dari now. Uap Raka naik 60 px per detik dan hidup 1,5 detik, jadi naik 90 px (dari y 112 sampai y 22).

Layar Jarak antar frame y -= 1 per frame y -= 60 * dt
30 Hz 33,3 ms 30 px/detik, sampai atas 3 detik 2 px per frame, 60 px/detik, 1,5 detik
60 Hz 16,7 ms 60 px/detik, 1,5 detik 1 px per frame, 60 px/detik, 1,5 detik
120 Hz 8,3 ms 120 px/detik, 0,75 detik 0,5 px per frame, 60 px/detik, 1,5 detik

Di HP 120 Hz, uap per frame naik dua kali lebih cepat. Itu jawaban tebakannya. Dengan dt, layar yang lebih cepat cuma memberi langkah yang lebih kecil, jadi kecepatannya sama di semua layar.

Uap kopi

let last = null;  // waktu frame sebelumnya (ms)
let uap = [];     // partikel uap yang masih hidup
let tunggu = 0;   // detik sampai partikel berikutnya lahir

function frame(now) {
  if (last === null) last = now;
  const dt = Math.min((now - last) / 1000, 0.1); // detik, paling besar 0,1
  last = now;

  // update: geser yang sudah ada
  for (const p of uap) {
    p.y -= 60 * dt;   // naik 60 px per detik
    p.x += p.vx * dt; // melayang ke samping
    p.umur += dt;
  }
  uap = uap.filter((p) => p.umur < 1.5);
  // lalu lahirkan yang baru
  tunggu -= dt;
  if (tunggu <= 0) {
    uap.push({ x: 120 + Math.random() * 60, y: 112, umur: 0, vx: (Math.random() - 0.5) * 12 });
    tunggu += 0.1; // 10 partikel per detik
  }

  // gambar ulang
  ctx.clearRect(0, 0, 300, 300);
  gambarSablon(); // kain, cangkir, tulisan dari lesson sebelumnya
  for (const p of uap) {
    const t = p.umur / 1.5; // 0 baru lahir, 1 hampir hilang
    ctx.globalAlpha = 0.6 * (1 - t);
    ctx.beginPath();
    ctx.arc(p.x, p.y, 6 + 8 * t, 0, Math.PI * 2);
    ctx.fillStyle = '#f5e6c8';
    ctx.fill();
  }
  ctx.globalAlpha = 1;

  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Memantul di tepi

Benda yang digeser tiap frame tanpa cek tepi jalan terus melewati tepi canvas dan gak pernah berhenti. Benda yang memantul membalik arah kecepatannya saat menyentuh tepi. Contoh biji kopi berjari-jari 20 di canvas setinggi 400, mulai di y 370, dy = 4 per frame:

y += dy;                                          // geser dulu
if (y + radius > 400 || y - radius < 0) dy *= -1; // lalu cek tepi
Frame y sesudah geser y + radius Lebih dari 400? dy
1 374 394 gak 4
2 378 398 gak 4
3 382 402 ya, balik -4
4 378 398 gak -4

dy *= -1 cuma membalik sumbu yang menyentuh tepi; sumbu x tetap. Di latihan, satu kali update() sama dengan satu frame.

Tab disembunyikan dan "kurangi gerakan"

Kebanyakan browser menghentikan requestAnimationFrame di tab yang disembunyikan dan di iframe yang tersembunyi, untuk menghemat baterai. Safari di iPhone juga membatasinya jadi 30 frame per detik dalam mode hemat daya (Low Power Mode), dan di iframe dari situs lain yang belum disentuh pengguna. Gerak per detik tetap benar di 30 fps. Tapi saat tab dibuka lagi setelah 5 detik, now - last bernilai 5.000 ms: tanpa Math.min, dt jadi 5 detik, semua uap umurnya lewat 1,5 detik dan hilang serentak. Dengan Math.min(..., 0.1), frame itu cuma maju 0,1 detik.

Sebagian orang pusing atau mual melihat gerakan, dan menyalakan "Kurangi gerakan" (Reduce motion) di sistemnya. Browser memberi tahu lewat media query:

const kurangiGerak = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (kurangiGerak) gambarSatuFrameDiam(); // uap diam, plus tombol Putar
else requestAnimationFrame(frame);

Animasi yang mulai sendiri dan berjalan lebih dari 5 detik juga butuh tombol Jeda (WCAG 2.2.2). Jeda berarti berhenti memanggil requestAnimationFrame, dan saat lanjut, set last = null lagi.

⚠️ Jebakan yang sering ketemu

TL;DR: Animasi canvas adalah loop requestAnimationFrame(frame): tiap frame hapus (clearRect), update, lalu gambar ulang semuanya, termasuk bagian yang diam. requestAnimationFrame dipanggil sebanyak refresh rate layar dan memberi timestamp now; hitung dt = (now - last) / 1000 dan gerakkan dengan kecepatan × dt, supaya uap naik 60 px per detik di 30, 60 maupun 120 Hz, sedangkan y -= 1 per frame jadi dua kali lebih cepat di 120 Hz. Memantul: geser, lalu kalau y + radius > tinggi atau y - radius < 0, dy *= -1. Browser menghentikan rAF di tab tersembunyi, jadi batasi dt dengan Math.min(dt, 0.1). Hormati prefers-reduced-motion dan sediakan tombol Jeda.

Yang akan kamu pelajari