Canvas API Dasar — Creative Web

Pernah liat website portfolio yang background-nya bergerak partikel kayak galaksi? Atau game browser kayak agar.io yang main-stream zaman 2015? Atau…

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 yang devicePixelRatio-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');

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);

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);

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

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);

⚠️ Jebakan yang sering ketemu

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).

Yang akan kamu pelajari