Canvas Interaktif — Creative Web

Canvas Interaktif Canvas tidak punya built-in event handling untuk objek individual (tidak seperti HTML/SVG). Kamu harus menangani interaksi secara manual —…

Canvas interaktif artinya gambar di canvas bisa disentuh, diseret dan diubah, padahal canvas cuma menyimpan pixel. Bayangin papan sablon Raka di etalase: pelanggan menunjuk "yang bulat itu" di balik kaca. Kacanya gak tahu apa yang ditunjuk. Raka yang tahu, karena ia punya catatan: stiker bulat di sini, cangkir di sana. Di canvas, catatan itu adalah array objek milikmu, dan mencocokkan posisi jari dengan catatan itu disebut hit testing.

Di lesson ini Sablon Kopi Raka jadi desain yang bisa diatur pelanggan: cangkir, tulisan KOPI dan stiker bulat "R" bisa diseret dan diganti warna tintanya.

🧪 Tebakan cepat: canvas 300 × 300 ditampilkan selebar 240 px di HP. Pelanggan menyentuh tepat di tengahnya, dan e.clientX - rect.left bernilai 120. Titik mana di gambar yang disentuh: x 120 atau x 150? Tebak dulu, jawabannya ada di bagian "Dari layar ke koordinat canvas".

Canvas cuma tahu pixel

Elemen HTML biasa (dan SVG) adalah objek di DOM: tombol bisa diberi addEventListener('click') sendiri. Canvas hanya menyimpan pixel. Begitu cangkir digambar, gak ada objek "cangkir" yang bisa dipasangi listener. Jadi kamu menyimpan desainnya sendiri, lalu menggambarnya dari data itu:

const desain = [
  { jenis: 'cangkir', x: 150, y: 165, tinta: '#c47a3d' },
  { jenis: 'teks', x: 150, y: 282, lebar: 108, tinta: '#f5e6c8' }, // lebar: measureText('KOPI').width, dibulatkan
  { jenis: 'stiker', x: 235, y: 70, r: 28, tinta: '#c47a3d' },
];

function gambar() {
  ctx.clearRect(0, 0, 300, 300);
  for (const benda of desain) gambarBenda(benda); // urutan gambar = urutan tumpukan
}

Listener-nya cuma satu, di canvas, dan tiap kali ada sentuhan kamu sendiri yang mencari benda mana yang kena. Setelah data berubah (posisi atau tinta), panggil gambar() lagi.

Pointer events

pointerdown, pointermove dan pointerup adalah satu set event untuk mouse, jari dan stylus sekaligus, jadi kamu gak perlu menulis versi mouse... dan touch... terpisah.

canvas.addEventListener('pointerdown', mulaiSeret);
canvas.addEventListener('pointermove', seret);
canvas.addEventListener('pointerup', selesai);
canvas.addEventListener('pointercancel', selesai);
canvas { touch-action: none; } /* jari di canvas gak menggulir halaman */

Tanpa touch-action: none, di HP browser menganggap jari yang bergerak sebagai scroll: halamannya ikut bergeser, lalu browser mengirim pointercancel dan seretannya berhenti. preventDefault() di pointer event gak bisa mencegah scroll itu; touch-action yang bisa.

Dari layar ke koordinat canvas

e.clientX dan e.clientY adalah posisi pointer di jendela browser. canvas.getBoundingClientRect() memberi posisi dan ukuran canvas di jendela yang sama. Kurangi dengan pojok kirinya (rect.left, rect.top), lalu skalakan ke 300 satuan gambar:

function titikCanvas(e) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: (e.clientX - rect.left) * (300 / rect.width),
    y: (e.clientY - rect.top) * (300 / rect.height),
  };
}

Di HP, canvas tampil 240 px, dengan rect.left 40 dan rect.top 100. Pelanggan menyentuh di clientX 160, clientY 220:

Langkah x y
Dikurangi pojok canvas 160 - 40 = 120 220 - 100 = 120
Dikali 300 / 240 = 1,25 150 150

Yang disentuh adalah (150, 150), tengah gambar. Itu jawaban tebakannya. Tanpa dikali 1,25, kodenya mengira (120, 120): meleset 30 px ke kiri atas, dan stiker yang disentuh gak terpilih.

Angka 300 di sini adalah satuan gambar, karena kita menggambar lewat setTransform(dpr, 0, 0, dpr, 0, 0) (lesson Canvas API Dasar). Kalau kamu menggambar langsung di pixel canvas asli, pakai canvas.width / rect.width.

Hit testing dengan matematika

Stiker bulat kena kalau jarak pointer ke pusatnya gak lebih dari jari-jarinya. Jaraknya dihitung dengan Pythagoras, dan supaya gak perlu Math.sqrt, bandingkan kuadratnya:

function kenaLingkaran(s, x, y) {
  const dx = x - s.x;
  const dy = y - s.y;
  return dx * dx + dy * dy <= s.r * s.r;
}

function kenaKotak(t, x, y) {
  return x >= t.x - t.lebar / 2 && x <= t.x + t.lebar / 2 &&
         y >= t.y - 33 && y <= t.y + 1; // teks: 33 px di atas baseline, 1 px di bawahnya
}

Stiker di (235, 70) dengan r 28, disentuh di (250, 84): dx 15, dy 14, jadi 225 + 196 = 421. Batasnya 28 × 28 = 784. 421 ≤ 784, kena. Versi Math.sqrt(dx * dx + dy * dy) < r sama artinya, cuma sedikit lebih lambat.

Hit testing dengan isPointInPath

Badan cangkir berbentuk trapesium, dan rumusnya repot. Simpan bentuknya sebagai Path2D (path yang bisa dipakai ulang), lalu tanya canvas:

const badanCangkir = new Path2D();
badanCangkir.moveTo(-55, -45); // relatif ke pusat cangkir
badanCangkir.lineTo(55, -45);
badanCangkir.lineTo(45, 45);
badanCangkir.lineTo(-45, 45);
badanCangkir.closePath();

function kenaCangkir(c, x, y) {
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.translate(c.x, c.y);
  // titik dalam pixel canvas asli: transform gak berlaku untuknya
  return ctx.isPointInPath(badanCangkir, x * dpr, y * dpr);
}

Menyeret

let dipegang = null;
let geser = { x: 0, y: 0 };

function mulaiSeret(e) {
  const p = titikCanvas(e);
  // dari yang paling atas: yang digambar terakhir dicek duluan
  for (let i = desain.length - 1; i >= 0; i--) {
    if (kena(desain[i], p.x, p.y)) {
      dipegang = desain[i];
      geser = { x: p.x - dipegang.x, y: p.y - dipegang.y };
      canvas.setPointerCapture(e.pointerId);
      return;
    }
  }
}

function seret(e) {
  if (!dipegang) return;
  const p = titikCanvas(e);
  dipegang.x = p.x - geser.x;
  dipegang.y = p.y - geser.y;
  gambar();
}

function selesai() {
  dipegang = null;
}

Keyboard dan pembaca layar

Canvas gak punya elemen di dalamnya, jadi tombol Tab gak bisa memilih cangkir, dan pembaca layar cuma menemukan satu gambar. Yang ditawarkan sebagai gantinya:

⚠️ Jebakan yang sering ketemu

TL;DR: Canvas cuma menyimpan pixel, jadi simpan desainmu sebagai array objek dan gambar ulang dari situ. Pakai pointer events (pointerdown/move/up), dan ubah posisinya: (e.clientX - rect.left) * (300 / rect.width), begitu juga y dengan rect.top. Hit testing: lingkaran dengan dx * dx + dy * dy <= r * r, kotak dengan perbandingan x dan y, bentuk lain dengan isPointInPath(path, x * dpr, y * dpr), karena titiknya gak ikut transform. Seret dengan menyimpan jarak ke pusat dan setPointerCapture, cek dari benda paling atas, dan pasang touch-action: none. Untuk keyboard dan pembaca layar, sediakan tombol asli, status aria-live, dan aria-label di canvas.

Yang akan kamu pelajari