Pengembangan Canvas dengan Claude Code: HiDPI, RAF, Event, dan Tes
Bangun UI Canvas yang stabil dengan Claude Code: HiDPI, requestAnimationFrame, Pointer Events, state, dan Playwright.
Canvas yang bagus harus tahan di perangkat nyata
Canvas cocok untuk grafik khusus, anotasi gambar, efek partikel, game ringan, simulasi belajar, dan visualisasi yang sulit dibuat dengan DOM biasa. Namun Canvas adalah API tingkat rendah. Browser tidak menyimpan garis sebagai elemen DOM, tidak otomatis menyesuaikan kepadatan piksel, dan tidak mematikan loop animasi ketika komponen sudah tidak terlihat.
Karena itu, meminta Claude Code “buat animasi Canvas yang keren” sering menghasilkan demo desktop: terlihat menarik, tetapi buram di layar HiDPI, tidak bisa disentuh di ponsel, membuat scroll horizontal, atau tetap menjalankan requestAnimationFrame setelah pindah halaman. Untuk produksi, prompt harus membawa batasan layout, input, state, screenshot, dan CTA.
Baca juga animasi dengan Claude Code, Three.js dengan Claude Code, dan visualisasi data dengan Claude Code. Referensi resmi: Claude Code Docs, MDN Canvas API, requestAnimationFrame, Pointer Events, dan Playwright screenshots.
Prompt yang lebih aman
HiDPI berarti layar dengan kepadatan piksel tinggi. Satu piksel CSS bisa dipetakan ke beberapa piksel fisik. Jika Canvas hanya diberi width: 100% di CSS tetapi buffer internalnya tetap kecil, browser akan membesarkan gambar kecil dan hasilnya buram.
Implementasikan demo Canvas 2D.
Syarat:
- Pisahkan piksel CSS dan piksel internal Canvas dengan devicePixelRatio
- Render memakai requestAnimationFrame dan clamp dt setelah tab pause
- Gunakan Pointer Events untuk mouse, touch, dan pen
- Simpan state gambar dalam satu objek state; render(ctx) hanya menggambar
- Gunakan ResizeObserver untuk mengikuti ukuran container
- Tidak boleh ada horizontal scroll pada lebar mobile 375px
- Tambahkan Playwright check: visible, non-blank pixels, screenshot, dan mobile width
- Laporkan file berubah, risiko, failure cases, dan manual checks
Prompt seperti ini membuat Claude Code membangun sistem rendering, bukan hanya efek sekali pakai. Ini juga mencegah perubahan desain acak yang mengganggu artikel, iklan, atau tombol konsultasi.
Arsitektur sederhana
Pisahkan input, update state, update waktu, render, dan verifikasi.
Pointer Events
|
v
input handler ---> state update ---> update(dt)
|
ResizeObserver ---> resize(dpr) v
render(ctx)
|
v
Playwright checks
render(ctx) sebaiknya hanya membaca state dan menggambar. Jangan mendaftarkan event listener, mengubah DOM luar, atau memulai loop baru di dalamnya. Batasan ini membuat undo, eraser, fallback WebGL, dan screenshot test lebih mudah ditambahkan.
Use case yang masuk akal
Use case pertama adalah visualisasi data untuk artikel dan dashboard. Jejak real-time, titik padat, gelombang audio, peta bergerak, atau diagram edukasi sering lebih cocok memakai Canvas. Claude Code bisa membuat loop, tetapi loading, empty state, mobile layout, dan posisi CTA tetap harus dicek.
Use case kedua adalah anotasi gambar. Review screenshot, feedback desain, dan materi belajar membutuhkan garis, panah, kotak, label, dan undo. Pointer Events membantu menyatukan mouse, jari, dan stylus. Pada perangkat yang mendukung, pressure bisa memengaruhi ketebalan garis.
Use case ketiga adalah edukasi dan game ringan. Simulasi fisika, latihan mengetik, kartu kosakata, dan partikel cocok dengan update per frame. Risiko utamanya adalah lupa membatalkan requestAnimationFrame saat route berubah.
Use case keempat adalah halaman produk. Preview interaktif bisa membantu pembaca memahami opsi sebelum klik beli atau konsultasi. Namun Canvas tidak boleh mendorong CTA keluar dari layar pertama.
Contoh yang bisa dijalankan
Simpan contoh ini sebagai HTML dan buka di browser. Bagian pentingnya adalah ctx.setTransform, karena ia mengganti transform saat resize dan tidak menumpuk ctx.scale.
<style>
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #111827; }
canvas { width: min(100%, 720px); aspect-ratio: 16 / 9; display: block; background: #020617; border: 1px solid #374151; border-radius: 8px; touch-action: none; }
</style>
<canvas id="demo" aria-label="Canvas particle demo"></canvas>
<script type="module">
const canvas = document.querySelector("#demo");
const ctx = canvas.getContext("2d");
const state = { width: 1, height: 1, dpr: 1, last: 0, pointer: { x: 0, y: 0, down: false }, dots: [] };
function resize() {
const rect = canvas.getBoundingClientRect();
state.width = Math.max(1, rect.width);
state.height = Math.max(1, rect.height);
state.dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(state.width * state.dpr);
canvas.height = Math.round(state.height * state.dpr);
ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
}
function point(event) {
const rect = canvas.getBoundingClientRect();
return { x: event.clientX - rect.left, y: event.clientY - rect.top, pressure: event.pressure || 0.5 };
}
function emit(x, y, pressure = 0.5) {
for (let i = 0; i < 8; i += 1) {
const angle = Math.random() * Math.PI * 2;
const speed = 90 + Math.random() * 180;
state.dots.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 1, size: 4 + pressure * 8 });
}
state.dots = state.dots.slice(-360);
}
canvas.addEventListener("pointerdown", (event) => {
canvas.setPointerCapture(event.pointerId);
const p = point(event);
state.pointer = { x: p.x, y: p.y, down: true };
emit(p.x, p.y, p.pressure);
});
canvas.addEventListener("pointermove", (event) => {
const events = event.getCoalescedEvents ? event.getCoalescedEvents() : [event];
for (const item of events) {
const p = point(item);
state.pointer.x = p.x;
state.pointer.y = p.y;
if (state.pointer.down) emit(p.x, p.y, p.pressure);
}
});
canvas.addEventListener("pointerup", () => (state.pointer.down = false));
canvas.addEventListener("pointercancel", () => (state.pointer.down = false));
function frame(now) {
const dt = state.last ? Math.min((now - state.last) / 1000, 0.033) : 0;
state.last = now;
for (const dot of state.dots) {
dot.vy += 220 * dt;
dot.x += dot.vx * dt;
dot.y += dot.vy * dt;
dot.life -= dt;
}
state.dots = state.dots.filter((dot) => dot.life > 0);
ctx.clearRect(0, 0, state.width, state.height);
ctx.fillStyle = "#020617";
ctx.fillRect(0, 0, state.width, state.height);
for (const dot of state.dots) {
ctx.fillStyle = `rgba(56,189,248,${dot.life})`;
ctx.beginPath();
ctx.arc(dot.x, dot.y, dot.size * dot.life, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "#e5e7eb";
ctx.fillText(`dpr ${state.dpr.toFixed(2)} / dots ${state.dots.length}`, 16, 24);
requestAnimationFrame(frame);
}
new ResizeObserver(resize).observe(canvas);
resize();
requestAnimationFrame(frame);
</script>
State, mobile, dan verifikasi
Canvas tidak menyimpan stroke sebagai node DOM. Untuk undo, redo, tool aktif, warna, dan replay, simpan point dan metadata di state JavaScript. Minta Claude Code memisahkan update state dari rendering supaya perilakunya bisa diuji.
Di mobile, masalah paling sering adalah lebar tetap dan tinggi yang tidak jelas. width: 800px membuat horizontal scroll pada layar 375px; width: 100% tanpa aspect-ratio bisa membuat Canvas gepeng. Cek Canvas bersama teks, code block, iklan, artikel terkait, dan CTA.
import { expect, test } from "@playwright/test";
test("canvas renders on mobile", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/canvas-demo");
const canvas = page.locator("canvas").first();
await expect(canvas).toBeVisible();
const box = await canvas.boundingBox();
expect(box?.width ?? 0).toBeLessThanOrEqual(390);
const paintedPixels = await canvas.evaluate((node) => {
const context = node.getContext("2d");
if (!context) return 0;
const data = context.getImageData(0, 0, node.width, node.height).data;
let painted = 0;
for (let i = 3; i < data.length; i += 4) if (data[i] > 0) painted += 1;
return painted;
});
expect(paintedPixels).toBeGreaterThan(1000);
await expect(canvas).toHaveScreenshot("canvas-mobile.png", { maxDiffPixelRatio: 0.03 });
});
Pitfall dan monetisasi
Pitfall yang paling sering muncul adalah hanya mengubah ukuran CSS, menumpuk ctx.scale, hanya memakai mousemove, tidak membersihkan RAF loop, memakai lebar desktop tetap, dan menganggap screenshot hitam sebagai sukses. Masukkan daftar ini ke prompt review Claude Code.
Canvas membantu monetisasi saat ia menjelaskan sesuatu yang tidak bisa dijelaskan gambar statis: tutorial interaktif, alat anotasi, preview produk, atau visualisasi data yang mengarah ke template, pembelian, atau konsultasi. Canvas merugikan jika mendorong teks dan CTA ke bawah. Untuk menjadikannya workflow tim, lihat training dan konsultasi Claude Code agar prompt, aturan implementasi, dan Playwright check disusun bersama.
Saat alur ini dicoba, langkah paling berguna adalah meminta Claude Code melakukan review kedua yang hanya mencari failure case. Review itu menemukan fixed width, ctx.scale yang menumpuk, input touch yang belum lengkap, dan mobile scroll sebelum rilis. Hasilnya tidak sekadar demo menarik, tetapi UI yang lebih siap dipasang di halaman nyata.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission safety ladder Claude Code: perluas akses tanpa kehilangan kontrol
Naik dari read-only ke edit terbatas, command bukti, dan cek deploy dengan kontrol yang jelas.
Claude Code Small PR Proof Pack: perubahan kecil yang mudah direview
Paket bukti untuk PR Claude Code: diff, check, URL publik, jalur CTA, dan rollback.
Review gate Claude Code sebelum commit: diff, test, URL publik, dan CTA
Cara memakai Claude Code sebelum commit: diff scope, build, URL publik, link Gumroad, CTA konsultasi, missing test, dan file tidak terkait.