Canvas adalah elemen HTML <canvas> berisi lembar pixel kosong yang kamu cat dengan perintah JavaScript: "isi kotak ini", "tarik garis ke sana", "tulis teks di sini". Bayangin Raka menyablon kaos: tinta ditarik lewat screen satu warna demi satu warna. Begitu tintanya nempel, yang ada di kaos cuma tinta. Cangkirnya gak bisa diangkat lalu dipindah; mau ganti, ya sablon kaos baru. Canvas sama: tiap perintah meninggalkan pixel, dan canvas gak ingat ada "cangkir" di situ.
Di track ini kita bikin situs kreatif Raka, pemilik Toko Kaos Raka. Yang paling laku adalah Kaos Sablon Kopi (Rp 95.000), dan di lesson ini sablon "Kopi"-nya kita gambar di canvas, langkah demi langkah.
🧪 Tebakan cepat: canvas
width="300"ditampilkan 300 px di HP yangdevicePixelRatio-nya 2. Gambarnya tajam atau buram? Tebak dulu, jawabannya ada di bagian "Canvas buram di layar retina".
Canvas dan context 2D
<canvas id="sablon" width="300" height="300"></canvas>
const canvas = document.getElementById('sablon');
const ctx = canvas.getContext('2d');
widthdanheightadalah jumlah pixel gambarnya. Tanpa atribut itu, canvas berukuran 300 × 150.getContext('2d')mengembalikan context: kuas yang punya semua perintah menggambar 2D. Semua gambar lewatctx, bukan lewatcanvas.
Sistem koordinat
Titik (0, 0) ada di pojok kiri atas. x bertambah ke kanan, y bertambah ke bawah, kebalikan dari grafik di pelajaran matematika. Di canvas 300 × 300, pojok kanan bawah adalah (300, 300) dan tengahnya (150, 150).
Sudut ditulis dalam radian, bukan derajat, dan karena y ke bawah, sudut positif berputar searah jarum jam:
| Derajat | Radian | Arah dari titik pusat |
|---|---|---|
| 0° | 0 |
kanan (jam 3) |
| 90° | Math.PI / 2 |
bawah (jam 6) |
| 180° | Math.PI |
kiri (jam 9) |
| -90° | -Math.PI / 2 |
atas (jam 12) |
| 360° | Math.PI * 2 |
satu putaran penuh |
Sablon Kopi, langkah demi langkah
Tiga warna tinta Raka: hitam kaos, krem, dan coklat kopi. Uap di atas cangkir (y 30 sampai 110) sengaja dikosongkan; lesson berikutnya menganimasikannya.
const KAOS = '#1c1917'; // hitam kaos
const KREM = '#f5e6c8'; // tinta krem
const COKLAT = '#c47a3d'; // tinta coklat kopi
// 1. Kain: kotak penuh seukuran canvas
ctx.fillStyle = KAOS;
ctx.fillRect(0, 0, 300, 300);
// 2. Badan cangkir: path empat titik, mengecil ke bawah
ctx.beginPath();
ctx.moveTo(95, 120);
ctx.lineTo(205, 120);
ctx.lineTo(195, 210);
ctx.lineTo(105, 210);
ctx.closePath();
ctx.fillStyle = COKLAT;
ctx.fill();
// 3. Garis tepi cangkir: path yang sama, sekarang di-stroke
ctx.strokeStyle = KREM;
ctx.lineWidth = 8;
ctx.stroke();
// 4. Gagang: setengah lingkaran di kanan
ctx.beginPath();
ctx.arc(200, 160, 24, -Math.PI / 2, Math.PI / 2);
ctx.stroke();
// 5. Tatakan: satu garis tebal
ctx.beginPath();
ctx.moveTo(75, 228);
ctx.lineTo(225, 228);
ctx.lineWidth = 10;
ctx.stroke();
// 6. Tulisan
ctx.font = 'bold 44px sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = KREM;
ctx.fillText('KOPI', 150, 282);
fillRect(x, y, lebar, tinggi)mengisi kotak. (x, y) adalah pojok kiri atasnya, bukan titik tengah. Kain mulai di (0, 0), lebar 300, tinggi 300.fillStyle(warna isi),strokeStyle(warna garis) danlineWidth(tebal garis) dipasang sebelum perintah gambarnya. Mereka berlaku untuk semua gambar sesudahnya sampai diganti.beginPath()membuka path baru dan membuang yang lama. Path adalah rangkaian garis yang belum terlihat sampai kamu memanggilfill()ataustroke().moveTo(x, y)memindahkan pena tanpa menggambar.lineTo(x, y)menarik garis dari posisi pena ke titik itu.closePath()menarik garis kembali ke titik awal.fill()mengisi bagian dalam path,stroke()menggambar garis tepinya. Garis 8 px duduk tepat di atas garis path: 4 px ke dalam, 4 px ke luar.arc(x, y, jariJari, sudutAwal, sudutAkhir)membuat busur. Dari-Math.PI / 2(atas) keMath.PI / 2(bawah), searah jarum jam lewat jam 3: setengah lingkaran sebelah kanan. Canvas gak punyacircle(); lingkaran penuh adalaharc(x, y, r, 0, Math.PI * 2).fillText(teks, x, y)menulis teks. y adalah garis dasar huruf (baseline), bukan pojok atasnya.textAlign = 'center'membuat x jadi titik tengah tulisan.
save, restore, dan transform
Biji kopi di kiri dan kanan tulisan adalah lingkaran yang dipipihkan dan dimiringkan. Daripada menghitung titik-titiknya, kita pindahkan dan putar kertasnya:
function biji(x, y, sudut) {
ctx.save(); // simpan keadaan ctx sekarang
ctx.translate(x, y); // titik (0, 0) pindah ke (x, y)
ctx.rotate(sudut); // putar kertas (radian)
ctx.scale(1, 0.6); // tinggi jadi 60%: lingkaran jadi lonjong
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI * 2);
ctx.fillStyle = COKLAT;
ctx.fill();
ctx.restore(); // kembali ke keadaan saat save()
}
biji(62, 266, -0.5);
biji(238, 266, 0.5);
translate,rotatedanscalemengubah sistem koordinatnya, dan menumpuk: transform kedua dihitung di atas yang pertama.save()menyimpan transform, warna,lineWidth,fontdan pengaturan lain ke sebuah tumpukan.restore()mengambil yang terakhir disimpan.- Tanpa
save()/restore(),biji(238, ...)digeser dari kertas yang sudah dipindah ke (62, 266), diputar -0,5 dan dipipihkan. Biji kedua jatuh di sekitar (347, 292), di luar canvas 300 × 300. Gak ada error, bijinya hilang begitu saja.
Canvas buram di layar retina
devicePixelRatio adalah berapa pixel layar yang dipakai untuk 1 px CSS. Layar laptop biasa 1, laptop retina 2, kebanyakan HP 2 sampai 3.
Canvas width="300" punya 300 × 300 = 90.000 pixel. Ditampilkan 300 px CSS di layar DPR 2, area itu berisi 600 × 600 = 360.000 pixel layar. Browser terpaksa menarik tiap pixel canvas jadi 2 × 2 pixel layar, dan hasilnya buram. Itu jawaban tebakannya. Di DPR 3, 90.000 pixel ditarik ke 810.000.
Perbaikannya: kasih canvas pixel sebanyak layarnya, tapi tetap tampilkan 300 px:
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(300 * dpr); // DPR 2: 600 pixel asli
canvas.height = Math.round(300 * dpr);
canvas.style.width = '300px'; // ukuran tampil tetap 300 px
canvas.style.height = '300px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // sama dengan scale(dpr, dpr), tanpa menumpuk
// ...lalu gambar seperti biasa, tetap pakai angka 0 sampai 300
Math.round: DPR bisa pecahan (1,25 atau 1,5 di laptop Windows), sedangkanwidthharus bilangan bulat.- Mengubah
canvas.widthmenghapus gambarnya dan me-reset context (transform, warna, font). Jadi urutannya: ukuran dulu, lalusetTransform, baru gambar. setTransform(dpr, 0, 0, dpr, 0, 0)menimpa transform lama.scale(dpr, dpr)yang dipanggil dua kali (misalnya tiap resize) jadi 4 kali lipat, dan gambarnya membesar.
drawImage: menempel gambar lain
drawImage menyalin gambar ke canvas. Sumbernya bisa <img>, <video>, atau canvas lain.
// Logo Raka di pojok kanan atas, 26 × 26
const logo = new Image();
logo.onload = () => ctx.drawImage(logo, 262, 12, 26, 26);
logo.src = 'logo-raka.png';
// Kaca pembesar: kotak 24 × 24 di pojok kiri bawah cangkir, diperbesar 6 kali
kaca.imageSmoothingEnabled = false; // tampilkan pixel apa adanya
kaca.drawImage(canvas, 93 * dpr, 198 * dpr, 24 * dpr, 24 * dpr, 0, 0, 144, 144);
- Bentuk 5 argumen:
drawImage(sumber, x, y, lebar, tinggi). Bentuk 9 argumen: empat angka pertama memotong kotak dari sumber (dalam pixel sumber), empat berikutnya kotak tujuannya. - Gambar dari file butuh waktu dimuat.
drawImageyang dipanggil sebelumonloadgak menggambar apa-apa, tanpa error.
⚠️ Jebakan yang sering ketemu
- Lupa
beginPath(). Tanpa itu di langkah 5, path gagang masih ada, jadistroke()tatakan ikut menggambar ulang gagangnya denganlineWidth10: gagangnya menebal. - Warna dipasang setelah bentuknya.
fillRectlalufillStyle = '#3498db'menggambar kotak dengan warna sebelumnya. Pasang warnanya dulu. - Derajat, bukan radian.
arc(200, 160, 24, 0, 180)gak menghasilkan setengah lingkaran: 180 radian lebih dari satu putaran penuh, jadi yang keluar lingkaran utuh. - Ukuran canvas cuma lewat CSS.
width: 100%di CSS meregangkan 300 pixel yang sama: buram, dan kalau rasionya beda, lingkaran jadi lonjong.
TL;DR: <canvas width height> adalah lembar pixel; getContext('2d') memberi kuasnya. (0, 0) di kiri atas, y ke bawah, sudut dalam radian searah jarum jam. fillRect(x, y, lebar, tinggi) mulai dari pojok kiri atas. Path: beginPath, moveTo (pindah tanpa menggambar), lineTo, arc (lingkaran = 0 sampai Math.PI * 2), closePath, lalu fill atau stroke, dengan fillStyle/strokeStyle/lineWidth dipasang sebelumnya. fillText menulis di baseline. Transform menumpuk, jadi bungkus dengan save()/restore(). drawImage menempel gambar atau canvas lain, setelah gambarnya dimuat. Untuk layar retina, canvas.width = 300 * dpr, CSS tetap 300 px, lalu setTransform(dpr, 0, 0, dpr, 0, 0).