Bab 17 dari Eloquent JavaScript. Sebelumnya: 16 Proyek Game Platform. Berikutnya: 18 HTTP dan Form.
Menggambar adalah penipuan.
— M.C. Escher, dikutip Bruno Ernst di The Magic Mirror of M.C. Escher

Browser ngasih kita beberapa cara buat nampilin grafis. Cara paling sederhana adalah pakai style buat memposisikan dan mewarnai elemen DOM biasa. Ini bisa bawa kita cukup jauh, kaya yang ditunjukin game di bab sebelumnya. Dengan nambahin gambar latar yang sebagian transparan ke node, kita bisa bikin mereka keliatan persis kaya yang kita mau. Bahkan bisa aja muter atau memiringkan node dengan style transform.
Tapi kita bakal pakai DOM buat sesuatu yang awalnya gak dia desain untuknya. Beberapa tugas, kaya menggambar garis antara titik sembarang, sangat canggung buat dilakuin dengan elemen HTML biasa.
Ada dua alternatif. Yang pertama berbasis DOM tapi memanfaatkan Scalable Vector Graphics (SVG) bukannya HTML. Bayangin SVG sebagai dialek markup-dokumen yang fokus pada shape bukannya teks. Kamu bisa nyematin dokumen SVG langsung di dokumen HTML atau nyertainnya dengan tag <img>.
Alternatif kedua disebut canvas. Canvas adalah satu elemen DOM yang mengenkapsulasi gambar. Dia nyediain interface pemrograman buat menggambar shape ke ruang yang dimakan node-nya. Perbedaan utama antara canvas dan gambar SVG adalah di SVG deskripsi asli shape-nya dipertahankan biar mereka bisa dipindah atau diubah ukurannya kapan aja. Canvas, di sisi lain, ngonversi shape-nya jadi piksel (titik berwarna di raster) begitu mereka digambar dan gak ingat apa yang diwakili piksel-piksel ini. Satu-satunya cara buat mindahin shape di canvas adalah ngebersihin canvas (atau bagian canvas di sekitar shape-nya) dan menggambar ulang dia dengan shape-nya di posisi baru.
SVG
Buku ini gak bakal masuk ke SVG secara detail, tapi saya bakal ngejelasin sebentar cara kerjanya. Di akhir bab, saya bakal balik ke trade-off yang harus kamu pertimbangkan waktu mutusin mekanisme menggambar mana yang tepat buat aplikasi tertentu.
Ini dokumen HTML dengan gambar SVG sederhana di dalamnya:
<p>Normal HTML here.</p>
<svg xmlns="http://www.w3.org/2000/svg">
<circle r="50" cx="50" cy="50" fill="red"/>
<rect x="120" y="5" width="90" height="90"
stroke="blue" fill="none"/>
</svg>Atribut xmlns ngubah elemen (dan anaknya) ke XML namespace yang berbeda. Namespace ini, diidentifikasi dengan URL, nentuin dialek yang lagi kita omongin. Tag <circle> dan <rect>, yang gak ada di HTML, emang punya makna di SVG, mereka menggambar shape pakai style dan posisi yang ditentuin atributnya.
Tag ini bikin elemen DOM, sama kaya tag HTML, yang bisa diajak interaksi script. Misalnya, ini ngubah elemen <circle> jadi berwarna cyan:
let circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");Elemen Canvas
Grafis canvas bisa digambar ke elemen <canvas>. Kamu bisa ngasih elemen kaya gitu atribut width dan height buat nentuin ukurannya dalam piksel.
Canvas baru itu kosong, artinya dia sepenuhnya transparan dan jadi muncul sebagai ruang kosong di dokumen.
Tag <canvas> dimaksudkan buat ngebolehin gaya menggambar yang berbeda. Buat dapet akses ke interface menggambar sebenarnya, kita pertama perlu bikin context (konteks), objek yang method-nya nyediain interface menggambarnya. Saat ini ada tiga gaya menggambar yang didukung luas: "2d" buat grafis dua dimensi, "webgl" buat grafis tiga dimensi lewat interface OpenGL, dan "webgpu", alternatif yang lebih modern dan fleksibel dari WebGL.
Buku ini gak bakal ngebahas WebGL atau WebGPU, kita bakal tetap di dua dimensi. Tapi kalau kamu tertarik sama grafis tiga dimensi, saya mendorong kamu ngelihat WebGPU. Dia nyediain interface langsung ke hardware grafis dan ngebolehin kamu me-render bahkan scene yang rumit dengan efisien, pakai JavaScript.
Kamu bikin context dengan method getContext di elemen DOM <canvas>.
<p>Before canvas.</p>
<canvas width="120" height="60"></canvas>
<p>After canvas.</p>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "red";
context.fillRect(10, 10, 100, 50);
</script>Setelah bikin objek context-nya, contohnya menggambar persegi panjang merah yang lebarnya 100 piksel dan tingginya 50 piksel, dengan sudut kiri atasnya di koordinat (10, 10).
Sama kaya di HTML (dan SVG), sistem koordinat yang dipakai canvas naruh (0, 0) di sudut kiri atas, dan sumbu-y positif pergi turun dari situ. Ini berarti (10, 10) adalah 10 piksel di bawah dan ke kanan dari sudut kiri atas.
Garis dan Bidang
Di interface canvas, shape bisa diisi (filled), artinya areanya dikasih warna atau pola tertentu, atau dia bisa di-stroke, yang berarti garis digambar sepanjang tepinya. SVG pakai terminologi yang sama.
Method fillRect ngisi persegi panjang. Dia ngambil pertama koordinat x dan y dari sudut kiri atas persegi panjangnya, lalu lebarnya, lalu tingginya. Method serupa bernama strokeRect menggambar garis luar persegi panjang.
Kedua method gak ngambil parameter lebih lanjut. Warna isian, ketebalan stroke, dan seterusnya, gak ditentuin oleh argumen ke method-nya, kaya yang mungkin wajar kamu harapkan, tapi malah oleh properti objek context-nya.
Properti fillStyle ngendaliin cara shape diisi. Dia bisa disetel ke string yang nentuin warna, pakai notasi warna yang dipakai CSS.
Properti strokeStyle bekerja serupa tapi nentuin warna yang dipakai buat garis yang di-stroke. Lebar garis itu ditentuin oleh properti lineWidth, yang bisa berisi angka positif apa pun.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.strokeStyle = "blue";
cx.strokeRect(5, 5, 50, 50);
cx.lineWidth = 5;
cx.strokeRect(135, 5, 50, 50);
</script>Waktu gak ada atribut width atau height yang ditentuin, kaya di contoh, elemen canvas dapet lebar default 300 piksel dan tinggi 150 piksel.
Path
Path adalah deretan garis. Interface canvas 2D ngambil pendekatan yang aneh buat menggambarkan path kaya gitu. Dia dilakuin sepenuhnya lewat side effect. Path bukan nilai yang bisa disimpan dan dioper-oper. Sebaliknya, kalau kamu mau ngelakuin sesuatu dengan path, kamu bikin deretan pemanggilan method buat menggambarkan bentuknya.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
for (let y = 10; y < 100; y += 10) {
cx.moveTo(10, y);
cx.lineTo(90, y);
}
cx.stroke();
</script>Contoh ini bikin path dengan sejumlah segmen garis horizontal lalu men-stroke-nya pakai method stroke. Setiap segmen yang dibikin dengan lineTo mulai di posisi saat ini path-nya. Posisi itu biasanya akhir segmen terakhir, kecuali moveTo dipanggil. Di kasus itu, segmen berikutnya bakal mulai di posisi yang dikasih ke moveTo.
Waktu ngisi path (pakai method fill), setiap shape diisi terpisah. Path bisa berisi beberapa shape, setiap gerakan moveTo mulai yang baru. Tapi path-nya perlu ditutup (artinya awal dan akhirnya di posisi yang sama) sebelum dia bisa diisi. Kalau path-nya belum ditutup, garis ditambahin dari akhirnya ke awalnya, dan shape yang dikelilingi path yang udah selesai itu diisi.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(50, 10);
cx.lineTo(10, 70);
cx.lineTo(90, 70);
cx.fill();
</script>Contoh ini menggambar segitiga terisi. Perhatiin bahwa cuma dua dari sisi segitiganya yang secara eksplisit digambar. Yang ketiga, dari sudut kanan bawah balik ke atas, tersirat dan gak bakal ada kalau kamu men-stroke path-nya.
Kamu juga bisa pakai method closePath buat secara eksplisit nutup path dengan nambahin segmen garis beneran balik ke awal path. Segmen ini emang digambar waktu men-stroke path-nya.
Kurva
Path juga bisa berisi garis lengkung. Ini sayangnya sedikit lebih rumit buat digambar.
Method quadraticCurveTo menggambar kurva ke titik tertentu. Buat nentuin kelengkungan garisnya, method-nya dikasih titik kontrol juga titik tujuan. Bayangin titik kontrol ini sebagai menarik garisnya, ngasih dia kurvanya. Garisnya gak bakal ngelewatin titik kontrolnya, tapi arahnya di titik awal dan akhir bakal begitu rupa sehingga garis lurus ke arah itu bakal nunjuk ke titik kontrolnya. Contoh berikut ngilustrasiin ini:
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// kontrol=(60, 10) tujuan=(90, 90)
cx.quadraticCurveTo(60, 10, 90, 90);
cx.lineTo(60, 10);
cx.closePath();
cx.stroke();
</script>Kita menggambar kurva kuadratik dari kiri ke kanan, dengan (60, 10) sebagai titik kontrol, lalu menggambar dua segmen garis yang ngelewatin titik kontrol itu dan balik ke awal garis. Hasilnya agak mirip lambang Star Trek. Kamu bisa lihat efek titik kontrolnya: garis yang ninggalin sudut bawah mulai ke arah titik kontrol lalu melengkung ke target mereka.
Method bezierCurveTo menggambar jenis kurva yang serupa. Bukannya satu titik kontrol, method ini punya dua, satu buat setiap titik akhir garisnya. Ini sketsa serupa buat ngilustrasiin perilaku kurva kaya gitu:
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// kontrol1=(10, 10) kontrol2=(90, 10) tujuan=(50, 90)
cx.bezierCurveTo(10, 10, 90, 10, 50, 90);
cx.lineTo(90, 10);
cx.lineTo(10, 10);
cx.closePath();
cx.stroke();
</script>Dua titik kontrolnya nentuin arah di kedua ujung kurva. Makin jauh mereka dari titik yang sesuai, makin banyak kurvanya bakal “menggembung” ke arah itu.
Kurva kaya gitu bisa susah dikerjain, gak selalu jelas cara nemuin titik kontrol yang nyediain bentuk yang kamu cari. Kadang kamu bisa ngitungnya, dan kadang kamu cuma harus nemuin nilai yang sesuai dengan trial and error.
Method arc adalah cara buat menggambar garis yang melengkung sepanjang tepi lingkaran. Dia ngambil sepasang koordinat buat pusat busurnya, radius, lalu sudut awal dan sudut akhir.
Dua parameter terakhir itu bikin mungkin buat menggambar cuma sebagian lingkaran. Sudutnya diukur dalam radian, bukan derajat. Ini berarti lingkaran penuh punya sudut 2π, atau 2 * Math.PI, yang sekitar 6,28. Sudutnya mulai dihitung di titik di kanan pusat lingkaran dan pergi searah jarum jam dari situ. Kamu bisa pakai awal 0 dan akhir lebih besar dari 2π (misalnya, 7) buat menggambar lingkaran penuh.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
// pusat=(50, 50) radius=40 sudut=0 sampai 7
cx.arc(50, 50, 40, 0, 7);
// pusat=(150, 50) radius=40 sudut=0 sampai ½π
cx.arc(150, 50, 40, 0, 0.5 * Math.PI);
cx.stroke();
</script>Gambar hasilnya berisi garis dari kanan lingkaran penuh (pemanggilan pertama ke arc) ke kanan seperempat lingkaran (pemanggilan kedua). Kaya method menggambar-path lain, garis yang digambar dengan arc terhubung ke segmen path sebelumnya. Kamu bisa manggil moveTo atau mulai path baru buat ngehindarin ini.
Menggambar Pie Chart
Bayangin kamu baru aja dapet kerja di EconomiCorp, Inc. Tugas pertamamu adalah menggambar pie chart dari hasil survei kepuasan pelanggannya.
Binding results berisi array objek yang mewakili respons survei.
const results = [
{name: "Satisfied", count: 1043, color: "lightblue"},
{name: "Neutral", count: 563, color: "lightgreen"},
{name: "Unsatisfied", count: 510, color: "pink"},
{name: "No comment", count: 175, color: "silver"}
];Buat menggambar pie chart, kita menggambar sejumlah irisan pie, masing-masing terdiri dari busur dan sepasang garis ke pusat busur itu. Kita bisa ngitung sudut yang dimakan setiap busur dengan ngebagi lingkaran penuh (2π) dengan total jumlah respons lalu ngaliin angka itu (sudut per respons) dengan jumlah orang yang milih pilihan tertentu.
<canvas width="200" height="200"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let total = results
.reduce((sum, {count}) => sum + count, 0);
// Mulai di atas
let currentAngle = -0.5 * Math.PI;
for (let result of results) {
let sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
// pusat=100,100, radius=100
// dari sudut saat ini, searah jarum jam sebesar sudut irisan
cx.arc(100, 100, 100,
currentAngle, currentAngle + sliceAngle);
currentAngle += sliceAngle;
cx.lineTo(100, 100);
cx.fillStyle = result.color;
cx.fill();
}
</script>Tapi chart yang gak ngasih tahu kita arti irisannya gak terlalu membantu. Kita butuh cara buat menggambar teks ke canvas.
Teks
Konteks menggambar canvas 2D nyediain method fillText dan strokeText. Yang belakangan bisa berguna buat menggarisi huruf, tapi biasanya fillText yang kamu butuhin. Dia bakal ngisi garis luar teks yang dikasih dengan fillStyle saat ini.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.font = "28px Georgia";
cx.fillStyle = "fuchsia";
cx.fillText("I can draw text, too!", 10, 50);
</script>Kamu bisa nentuin ukuran, gaya, dan font teksnya dengan properti font. Contoh ini cuma ngasih ukuran font dan nama family. Bisa juga nambahin italic atau bold ke awal string buat milih gaya.
Dua argumen terakhir ke fillText dan strokeText nyediain posisi tempat font-nya digambar. Secara default, mereka nunjukin posisi awal baseline alfabet teksnya, yaitu garis tempat huruf “berdiri”, gak ngitung bagian yang menggantung di huruf kaya j atau p. Kamu bisa ngubah posisi horizontalnya dengan nyetel properti textAlign ke "end" atau "center" dan posisi vertikalnya dengan nyetel textBaseline ke "top", "middle", atau "bottom".
Kita bakal balik ke pie chart kita, dan masalah ngelabeli irisannya, di latihan di akhir bab.
Gambar
Di grafis komputer, perbedaan sering dibuat antara grafis vektor dan grafis bitmap. Yang pertama adalah yang udah kita lakuin sejauh ini di bab ini, nentuin gambar dengan ngasih deskripsi logis dari shape. Grafis bitmap, di sisi lain, gak nentuin shape aktual tapi malah bekerja dengan data piksel (raster titik berwarna).
Method drawImage ngebolehin kita menggambar data piksel ke canvas. Data piksel ini bisa berasal dari elemen <img> atau dari canvas lain. Contoh berikut bikin elemen <img> yang terlepas dan ngeload file gambar ke dia. Tapi method-nya gak bisa langsung mulai menggambar dari gambar ini karena browser mungkin belum ngeload-nya. Buat nanganin ini, kita mendaftarkan event handler "load" dan ngelakuin menggambarnya setelah gambarnya dimuat.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let img = document.createElement("img");
img.src = "img/hat.png";
img.addEventListener("load", () => {
for (let x = 10; x < 200; x += 30) {
cx.drawImage(img, x, 10);
}
});
</script>Secara default, drawImage bakal menggambar gambar di ukuran aslinya. Kamu juga bisa ngasih dia dua argumen tambahan buat nentuin lebar dan tinggi gambar yang digambar, waktu itu gak sama dengan gambar asalnya.
Waktu drawImage dikasih sembilan argumen, dia bisa dipakai buat menggambar cuma fragmen dari gambar. Argumen kedua sampai kelima nunjukin persegi panjang (x, y, lebar, dan tinggi) di gambar sumber yang harus disalin, dan argumen keenam sampai kesembilan ngasih persegi panjang (di canvas) tempat dia harus disalin.
Ini bisa dipakai buat mengemas beberapa sprite (elemen gambar) ke satu file gambar lalu menggambar cuma bagian yang kamu butuhin. Misalnya, gambar ini berisi karakter game dalam beberapa pose:

Dengan bergantian pose mana yang kita gambar, kita bisa nampilin animasi yang keliatan kaya karakter yang berjalan.
Buat menganimasi gambar di canvas, method clearRect berguna. Dia mirip fillRect, tapi bukannya mewarnai persegi panjangnya, dia bikinnya transparan, ngehapus piksel yang digambar sebelumnya.
Kita tahu bahwa setiap sprite, setiap subgambar, lebarnya 24 piksel dan tingginya 30 piksel. Kode berikut ngeload gambarnya lalu nyiapin interval (timer berulang) buat menggambar frame berikutnya:
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let img = document.createElement("img");
img.src = "img/player.png";
let spriteW = 24, spriteH = 30;
img.addEventListener("load", () => {
let cycle = 0;
setInterval(() => {
cx.clearRect(0, 0, spriteW, spriteH);
cx.drawImage(img,
// persegi panjang sumber
cycle * spriteW, 0, spriteW, spriteH,
// persegi panjang tujuan
0, 0, spriteW, spriteH);
cycle = (cycle + 1) % 8;
}, 120);
});
</script>Binding cycle ngelacak posisi kita di animasinya. Buat setiap frame, dia dinaikin lalu dijepit balik ke rentang 0 sampai 7 dengan pakai operator sisa bagi. Binding ini lalu dipakai buat ngitung koordinat-x yang dimiliki sprite buat pose saat ini di gambarnya.
Transformasi
Gimana kalau kita mau karakter kita berjalan ke kiri bukannya ke kanan? Kita bisa menggambar set sprite lain, tentu aja. Tapi kita juga bisa nginstruksiin canvas buat menggambar gambarnya secara terbalik.
Manggil method scale bakal bikin apa pun yang digambar setelahnya diskalakan. Method ini ngambil dua parameter, satu buat nyetel skala horizontal dan satu buat nyetel skala vertikal.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
cx.scale(3, .5);
cx.beginPath();
cx.arc(50, 50, 40, 0, 7);
cx.lineWidth = 3;
cx.stroke();
</script>Menskalakan bakal bikin semua soal gambar yang digambar, termasuk lebar garis, diregangkan atau ditekan sesuai yang ditentuin. Menskalakan dengan jumlah negatif bakal ngebalik gambarnya. Pembalikannya terjadi di sekitar titik (0, 0), yang berarti dia juga bakal ngebalik arah sistem koordinat. Waktu penskalaan horizontal -1 diterapin, shape yang digambar di posisi x 100 bakal berakhir di apa yang dulunya posisi -100.
Buat mutar gambar, kita gak bisa cuma nambahin cx.scale(-1, 1) sebelum pemanggilan drawImage. Itu bakal mindahin gambar kita keluar dari canvas, di mana dia gak bakal terlihat. Kita bisa nyesuaiin koordinat yang dikasih ke drawImage buat mengompensasi ini dengan menggambar gambarnya di posisi x -50 bukannya 0. Solusi lain, yang gak butuh kode yang menggambar tahu soal perubahan skalanya, adalah nyesuaiin sumbu di sekitar mana penskalaan terjadi.
Ada beberapa method lain selain scale yang mempengaruhi sistem koordinat buat canvas. Kamu bisa muter shape yang digambar selanjutnya dengan method rotate dan mindahin mereka dengan method translate. Hal yang menarik, dan membingungkan, adalah transformasi ini menumpuk (stack), artinya masing-masing terjadi relatif terhadap transformasi sebelumnya.
Kalau kita men-translate sebesar 10 piksel horizontal dua kali, semuanya bakal digambar 20 piksel ke kanan. Kalau kita pertama mindahin pusat sistem koordinat ke (50, 50) lalu muter sebesar 20 derajat (sekitar 0,1π radian), rotasi itu bakal terjadi di sekitar titik (50, 50).
Tapi kalau kita pertama muter sebesar 20 derajat lalu men-translate sebesar (50, 50), translasinya bakal terjadi di sistem koordinat yang udah diputar dan jadi ngehasilin orientasi yang berbeda. Urutan transformasi diterapin itu penting.
Buat ngebalik gambar di sekitar garis vertikal di posisi x tertentu, kita bisa ngelakuin berikut:
function flipHorizontally(context, around) {
context.translate(around, 0);
context.scale(-1, 1);
context.translate(-around, 0);
}Kita mindahin sumbu-y ke tempat kita mau cermin kita, nerapin pencerminannya, dan terakhir mindahin sumbu-y balik ke tempatnya yang tepat di alam semesta yang tercermin. Gambar berikut ngejelasin kenapa ini jalan:
Ini nunjukin sistem koordinat sebelum dan sesudah pencerminan melintasi garis tengah. Segitiganya dinomori buat ngilustrasiin setiap langkah. Kalau kita menggambar segitiga di posisi x positif, dia bakal, secara default, di tempat segitiga 1 berada. Pemanggilan flipHorizontally pertama ngelakuin translasi ke kanan, yang bawa kita ke segitiga 2. Dia lalu menskalakan, ngebalik segitiganya ke posisi 3. Ini bukan tempatnya seharusnya, kalau dia dicerminkan di garis yang dikasih. Pemanggilan translate kedua memperbaiki ini, dia “membatalkan” translasi awalnya dan bikin segitiga 4 muncul persis di tempatnya seharusnya.
Kita sekarang bisa menggambar karakter yang tercermin di posisi (100, 0) dengan ngebalik dunia di sekitar pusat vertikal karakternya.
<canvas></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let img = document.createElement("img");
img.src = "img/player.png";
let spriteW = 24, spriteH = 30;
img.addEventListener("load", () => {
flipHorizontally(cx, 100 + spriteW / 2);
cx.drawImage(img, 0, 0, spriteW, spriteH,
100, 0, spriteW, spriteH);
});
</script>Menyimpan dan Menghapus Transformasi
Transformasi tetap nempel. Semua yang kita gambar setelah menggambar karakter tercermin itu juga bakal tercermin. Itu mungkin gak praktis.
Bisa aja nyimpen transformasi saat ini, ngelakuin beberapa menggambar dan mentransformasi, lalu memulihkan transformasi lamanya. Ini biasanya hal yang tepat buat dilakuin buat fungsi yang perlu sementara mentransformasi sistem koordinat. Pertama, kita nyimpen transformasi apa pun yang lagi dipakai kode yang manggil fungsinya. Lalu fungsinya ngelakuin tugasnya, nambahin lebih banyak transformasi di atas transformasi saat ini. Terakhir, kita balik ke transformasi yang kita mulai.
Method save dan restore di konteks canvas 2D ngelakuin manajemen transformasi ini. Mereka secara konseptual ngejaga stack keadaan transformasi. Waktu kamu manggil save, keadaan saat ini didorong ke atas stack, dan waktu kamu manggil restore, keadaan di atas stack diambil dan dipakai sebagai transformasi context saat ini. Kamu juga bisa manggil resetTransform buat sepenuhnya me-reset transformasinya.
Fungsi branch di contoh berikut ngilustrasiin apa yang bisa kamu lakuin dengan fungsi yang ngubah transformasi lalu manggil fungsi (di kasus ini dirinya sendiri), yang lanjut menggambar dengan transformasi yang dikasih.
Fungsi ini menggambar bentuk mirip pohon dengan menggambar garis, mindahin pusat sistem koordinat ke akhir garis, dan manggil dirinya sendiri dua kali, pertama diputar ke kiri lalu diputar ke kanan. Setiap pemanggilan ngurangin panjang cabang yang digambar, dan rekursinya berhenti waktu panjangnya turun di bawah 8.
<canvas width="600" height="300"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
function branch(length, angle, scale) {
cx.fillRect(0, 0, 1, length);
if (length < 8) return;
cx.save();
cx.translate(0, length);
cx.rotate(-angle);
branch(length * scale, angle, scale);
cx.rotate(2 * angle);
branch(length * scale, angle, scale);
cx.restore();
}
cx.translate(300, 0);
branch(60, 0.5, 0.8);
</script>Kalau pemanggilan save dan restore gak ada di situ, pemanggilan rekursif kedua ke branch bakal berakhir dengan posisi dan rotasi yang dibikin pemanggilan pertama. Dia bakal terhubung bukan ke cabang saat ini tapi malah ke cabang paling dalam dan paling kanan yang digambar pemanggilan pertama. Bentuk hasilnya mungkin juga menarik, tapi jelas bukan pohon.
Balik ke Game
Kita sekarang cukup tahu soal menggambar canvas buat mulai ngerjain sistem tampilan berbasis canvas buat game dari bab sebelumnya. Tampilan barunya gak bakal cuma nampilin kotak berwarna lagi. Sebagai gantinya, kita bakal pakai drawImage buat menggambar gambar yang mewakili elemen game-nya.
Kita definisikan jenis objek tampilan lain bernama CanvasDisplay, mendukung interface yang sama dengan DOMDisplay dari Bab 16, yaitu method syncState dan clear.
Objek ini nyimpen sedikit lebih banyak informasi dari DOMDisplay. Bukannya pakai posisi scroll elemen DOM-nya, dia ngelacak viewport-nya sendiri, yang ngasih tahu bagian level mana yang lagi kita lihat. Terakhir, dia nyimpen properti flipPlayer biar bahkan waktu pemain berdiri diam, dia tetap menghadap arah tempat dia terakhir bergerak.
class CanvasDisplay {
constructor(parent, level) {
this.canvas = document.createElement("canvas");
this.canvas.width = Math.min(600, level.width * scale);
this.canvas.height = Math.min(450, level.height * scale);
parent.appendChild(this.canvas);
this.cx = this.canvas.getContext("2d");
this.flipPlayer = false;
this.viewport = {
left: 0,
top: 0,
width: this.canvas.width / scale,
height: this.canvas.height / scale
};
}
clear() {
this.canvas.remove();
}
}Method syncState pertama ngitung viewport baru lalu menggambar scene game-nya di posisi yang tepat.
CanvasDisplay.prototype.syncState = function(state) {
this.updateViewport(state);
this.clearDisplay(state.status);
this.drawBackground(state.level);
this.drawActors(state.actors);
};Berbeda dengan DOMDisplay, gaya tampilan ini emang harus menggambar ulang latarnya di setiap update. Karena shape di canvas cuma piksel, setelah kita menggambar mereka gak ada cara bagus buat mindahin mereka (atau ngehapus mereka). Satu-satunya cara buat ngupdate tampilan canvas adalah ngebersihinnya dan menggambar ulang scene-nya. Kita mungkin juga udah nge-scroll, yang butuh latarnya di posisi berbeda.
Method updateViewport mirip method scrollPlayerIntoView DOMDisplay. Dia ngecek apakah pemain terlalu dekat ke pinggir layar dan mindahin viewport waktu ini kasusnya.
CanvasDisplay.prototype.updateViewport = function(state) {
let view = this.viewport, margin = view.width / 3;
let player = state.player;
let center = player.pos.plus(player.size.times(0.5));
if (center.x < view.left + margin) {
view.left = Math.max(center.x - margin, 0);
} else if (center.x > view.left + view.width - margin) {
view.left = Math.min(center.x + margin - view.width,
state.level.width - view.width);
}
if (center.y < view.top + margin) {
view.top = Math.max(center.y - margin, 0);
} else if (center.y > view.top + view.height - margin) {
view.top = Math.min(center.y + margin - view.height,
state.level.height - view.height);
}
};Pemanggilan Math.max dan Math.min mastiin viewport-nya gak berakhir nampilin ruang di luar level. Math.max(x, 0) mastiin angka hasilnya gak kurang dari nol. Math.min serupa menjamin nilai tetap di bawah batas tertentu.
Waktu ngebersihin tampilan, kita bakal pakai warna yang sedikit berbeda tergantung apakah game-nya menang (lebih terang) atau kalah (lebih gelap).
CanvasDisplay.prototype.clearDisplay = function(status) {
if (status == "won") {
this.cx.fillStyle = "rgb(68, 191, 255)";
} else if (status == "lost") {
this.cx.fillStyle = "rgb(44, 136, 214)";
} else {
this.cx.fillStyle = "rgb(52, 166, 251)";
}
this.cx.fillRect(0, 0,
this.canvas.width, this.canvas.height);
};Buat menggambar latarnya, kita ngejalanin tile yang terlihat di viewport saat ini, pakai trik yang sama yang dipakai di method touches dari bab sebelumnya.
let otherSprites = document.createElement("img");
otherSprites.src = "img/sprites.png";
CanvasDisplay.prototype.drawBackground = function(level) {
let {left, top, width, height} = this.viewport;
let xStart = Math.floor(left);
let xEnd = Math.ceil(left + width);
let yStart = Math.floor(top);
let yEnd = Math.ceil(top + height);
for (let y = yStart; y < yEnd; y++) {
for (let x = xStart; x < xEnd; x++) {
let tile = level.rows[y][x];
if (tile == "empty") continue;
let screenX = (x - left) * scale;
let screenY = (y - top) * scale;
let tileX = tile == "lava" ? scale : 0;
this.cx.drawImage(otherSprites,
tileX, 0, scale, scale,
screenX, screenY, scale, scale);
}
}
};Tile yang gak kosong digambar dengan drawImage. Gambar otherSprites berisi gambar yang dipakai buat elemen selain pemain. Dia berisi, dari kiri ke kanan, tile dinding, tile lava, dan sprite buat koin.
![]()
Tile latar ukurannya 20 kali 20 piksel, karena kita bakal pakai skala yang sama kaya di DOMDisplay. Jadi, offset buat tile lava adalah 20 (nilai binding scale), dan offset buat dinding adalah 0.
Kita gak repot-repot nunggu gambar sprite-nya dimuat. Manggil drawImage dengan gambar yang belum dimuat bakal cuma gak ngelakuin apa pun. Jadi, kita mungkin gagal menggambar game-nya dengan benar buat beberapa frame pertama sementara gambarnya masih dimuat, tapi itu bukan masalah serius. Karena kita terus ngupdate layar, scene yang benar bakal muncul begitu pemuatannya selesai.
Karakter berjalan yang ditunjukin sebelumnya bakal dipakai buat mewakili pemain. Kode yang menggambarnya perlu milih sprite dan arah yang tepat berdasarkan gerakan pemain saat ini. Delapan sprite pertama berisi animasi berjalan. Waktu pemain bergerak sepanjang lantai, kita bergilir lewat mereka berdasarkan waktu saat ini. Kita mau ngeganti frame setiap 60 milidetik, jadi waktunya dibagi 60 dulu. Waktu pemain berdiri diam, kita menggambar sprite kesembilan. Selama lompatan, yang dikenali oleh fakta bahwa kecepatan vertikalnya bukan nol, kita pakai sprite kesepuluh yang paling kanan.
Karena sprite-nya sedikit lebih lebar dari objek pemain (24 bukannya 16 piksel buat ngasih sedikit ruang buat kaki dan lengan), method-nya harus nyesuaiin koordinat-x dan lebarnya sebesar jumlah tertentu (playerXOverlap).
let playerSprites = document.createElement("img");
playerSprites.src = "img/player.png";
const playerXOverlap = 4;
CanvasDisplay.prototype.drawPlayer = function(player, x, y,
width, height){
width += playerXOverlap * 2;
x -= playerXOverlap;
if (player.speed.x != 0) {
this.flipPlayer = player.speed.x < 0;
}
let tile = 8;
if (player.speed.y != 0) {
tile = 9;
} else if (player.speed.x != 0) {
tile = Math.floor(Date.now() / 60) % 8;
}
this.cx.save();
if (this.flipPlayer) {
flipHorizontally(this.cx, x + width / 2);
}
let tileX = tile * width;
this.cx.drawImage(playerSprites, tileX, 0, width, height,
x, y, width, height);
this.cx.restore();
};Method drawPlayer dipanggil oleh drawActors, yang bertanggung jawab buat menggambar semua actor di game.
CanvasDisplay.prototype.drawActors = function(actors) {
for (let actor of actors) {
let width = actor.size.x * scale;
let height = actor.size.y * scale;
let x = (actor.pos.x - this.viewport.left) * scale;
let y = (actor.pos.y - this.viewport.top) * scale;
if (actor.type == "player") {
this.drawPlayer(actor, x, y, width, height);
} else {
let tileX = (actor.type == "coin" ? 2 : 1) * scale;
this.cx.drawImage(otherSprites,
tileX, 0, width, height,
x, y, width, height);
}
}
};Waktu menggambar sesuatu yang bukan pemain, kita ngelihat jenisnya buat nemuin offset dari sprite yang benar. Tile lava ditemuin di offset 20, dan sprite koin ditemuin di 40 (dua kali scale).
Kita harus ngurangin posisi viewport-nya waktu ngitung posisi actor-nya, karena (0, 0) di canvas kita sesuai dengan kiri atas viewport, bukan kiri atas level. Kita juga bisa pakai translate buat ini. Kedua cara jalan.
Dokumen ini nyolokin tampilan baru ke runGame:
<body>
<script>
runGame(GAME_LEVELS, CanvasDisplay);
</script>
</body>Memilih Interface Grafis
Waktu kamu perlu ngehasilin grafis di browser, kamu bisa milih antara HTML biasa, SVG, dan canvas. Gak ada satu pendekatan terbaik yang jalan di semua situasi. Setiap pilihan punya kekuatan dan kelemahan.
HTML biasa punya keuntungan jadi sederhana. Dia juga terintegrasi baik dengan teks. SVG maupun canvas ngebolehin kamu menggambar teks, tapi mereka gak bakal ngebantu kamu memposisikan teks itu atau membungkusnya waktu dia makan lebih dari satu baris. Di gambar berbasis HTML, jauh lebih gampang nyertain blok teks.
SVG bisa dipakai buat ngehasilin grafis tajam yang keliatan bagus di level zoom berapa pun. Berbeda dari HTML, dia didesain buat menggambar dan jadi lebih cocok buat tujuan itu.
SVG maupun HTML ngebangun struktur data (DOM) yang mewakili gambarmu. Ini bikin mungkin buat memodifikasi elemen setelah mereka digambar. Kalau kamu perlu berulang kali ngubah bagian kecil dari gambar besar sebagai respons ke apa yang lagi dilakuin pengguna atau sebagai bagian dari animasi, ngelakuinnya di canvas bisa jadi mahal yang gak perlu. DOM juga ngebolehin kita mendaftarkan event handler mouse di setiap elemen di gambar (bahkan di shape yang digambar dengan SVG). Kamu gak bisa ngelakuin itu dengan canvas.
Tapi pendekatan berorientasi-piksel canvas bisa jadi keuntungan waktu menggambar jumlah besar elemen kecil. Fakta bahwa dia gak ngebangun struktur data tapi cuma berulang kali menggambar ke permukaan piksel yang sama ngasih canvas biaya per shape yang lebih rendah. Ada juga efek yang cuma praktis dengan elemen canvas, kaya me-render scene satu piksel dalam satu waktu (misalnya, pakai ray tracer) atau mem-postprocess gambar dengan JavaScript (mengaburkan atau mendistorsinya).
Di beberapa kasus, kamu mungkin mau ngegabungin beberapa teknik ini. Misalnya, kamu mungkin menggambar grafik dengan SVG atau canvas tapi nampilin informasi tekstual dengan memposisikan elemen HTML di atas gambarnya.
Buat aplikasi yang gak menuntut, beneran gak terlalu penting interface mana yang kamu pilih. Tampilan yang kita bangun buat game kita di bab ini bisa aja diimplementasiin pakai salah satu dari tiga teknologi grafis ini, karena dia gak perlu menggambar teks, nanganin interaksi mouse, atau bekerja dengan jumlah elemen yang luar biasa besar.
Ringkasan
Di bab ini kita ngebahas teknik buat menggambar grafis di browser, fokus pada elemen <canvas>.
Node canvas mewakili area di dokumen yang boleh digambar program kita. Menggambar ini dilakuin lewat objek konteks menggambar, dibikin dengan method getContext.
Interface menggambar 2D ngebolehin kita mengisi dan men-stroke berbagai shape. Properti fillStyle context nentuin cara shape diisi. Properti strokeStyle dan lineWidth ngendaliin cara garis digambar.
Persegi panjang dan potongan teks bisa digambar dengan satu pemanggilan method. Method fillRect dan strokeRect menggambar persegi panjang, dan method fillText dan strokeText menggambar teks. Buat bikin shape kustom, kita harus pertama ngebangun path.
Manggil beginPath mulai path baru. Sejumlah method lain nambahin garis dan kurva ke path saat ini. Misalnya, lineTo bisa nambahin garis lurus. Waktu path selesai, dia bisa diisi dengan method fill atau di-stroke dengan method stroke.
Mindahin piksel dari gambar atau canvas lain ke canvas kita dilakuin dengan method drawImage. Secara default, method ini menggambar seluruh gambar sumber, tapi dengan ngasih dia lebih banyak parameter, kamu bisa nyalin area tertentu dari gambarnya. Kita pakai ini buat game kita dengan nyalin pose individual karakter game keluar dari gambar yang berisi banyak pose kaya gitu.
Transformasi ngebolehin kamu menggambar shape dalam berbagai orientasi. Konteks menggambar 2D punya transformasi saat ini yang bisa diubah dengan method translate, scale, dan rotate. Ini bakal mempengaruhi semua operasi menggambar berikutnya. Keadaan transformasi bisa disimpen dengan method save dan dipulihkan dengan method restore.
Waktu nampilin animasi di canvas, method clearRect bisa dipakai buat ngebersihin bagian canvas sebelum menggambar ulangnya.
Poin-poin pentingnya:
- Tiga cara grafis di browser: HTML biasa, SVG (vektor, punya DOM, bagus buat zoom + event), dan canvas (piksel/raster, cepat buat banyak elemen kecil).
- Canvas:
getContext("2d")buat konteks.fillStyle/strokeStyle/lineWidthngatur warna & tebal. - Shape:
fillRect/strokeRect/clearRect. Path:beginPath→moveTo/lineTo/quadraticCurveTo/bezierCurveTo/arc→fill/stroke/closePath. - Teks:
fillText/strokeText, aturfont,textAlign,textBaseline. - Gambar:
drawImage(bisa 3, 5, atau 9 argumen). 9 argumen buat ngambil potongan (sprite) dari satu gambar besar. - Transformasi menumpuk:
translate,scale(negatif = flip/cermin),rotate. Urutan penting.save/restore(stack) buat sementara mengubah transformasi. - Beda canvas vs DOM/SVG: canvas gak inget shape (harus digambar ulang tiap frame), gak ada event per shape, tapi lebih murah per shape.
Latihan
Shape
Tulis program yang menggambar shape berikut di canvas:
- Trapesium (persegi panjang yang lebih lebar di satu sisi)
- Belah ketupat merah (persegi panjang diputar 45 derajat atau ¼π radian)
- Garis zigzag
- Spiral yang terdiri dari 100 segmen garis lurus
- Bintang kuning

Waktu menggambar dua shape terakhir, kamu mungkin mau lihat penjelasan Math.cos dan Math.sin di Bab 14, yang menggambarkan cara dapet koordinat di lingkaran pakai fungsi-fungsi ini.
Saya nyaranin bikin fungsi buat setiap shape. Kasih posisi, dan opsional properti lain kaya ukuran atau jumlah titik, sebagai parameter. Alternatifnya, yaitu meng-hardcode angka di seluruh kodemu, cenderung bikin kodenya susah dibaca dan dimodifikasi tanpa perlu.
<canvas width="600" height="200"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
// Kodemu di sini.
</script>Petunjuk:
Trapesium (1) paling gampang digambar pakai path. Pilih koordinat pusat yang sesuai dan tambahin masing-masing dari empat sudut di sekitar pusatnya.
Belah ketupat (2) bisa digambar dengan cara lugas, dengan path, atau dengan cara menarik, dengan transformasi rotate. Buat pakai rotasi, kamu bakal harus nerapin trik yang mirip yang kita lakuin di fungsi flipHorizontally. Karena kamu mau muter di sekitar pusat persegi panjangmu dan bukan di sekitar titik (0, 0), kamu harus pertama translate ke sana, lalu muter, lalu translate balik. Pastiin kamu me-reset transformasinya setelah menggambar shape apa pun yang bikin transformasi.
Buat zigzag (3), gak praktis buat nulis pemanggilan lineTo baru buat setiap segmen garis. Sebagai gantinya, kamu harus pakai loop. Kamu bisa bikin setiap iterasi menggambar entah dua segmen garis (kanan lalu kiri lagi) atau satu, di kasus mana kamu harus pakai kegenapan (% 2) dari indeks loop buat nentuin apakah pergi ke kiri atau ke kanan.
Kamu juga bakal butuh loop buat spiral (4). Kalau kamu menggambar deretan titik, dengan setiap titik bergerak lebih jauh sepanjang lingkaran di sekitar pusat spiral, kamu dapet lingkaran. Kalau, selama loop, kamu memvariasikan radius lingkaran tempat kamu naruh titik saat ini dan berputar lebih dari sekali, hasilnya adalah spiral.
Bintang (5) yang digambar dibangun dari garis quadraticCurveTo. Kamu juga bisa menggambar satu dengan garis lurus. Bagi lingkaran jadi delapan potongan buat bintang dengan delapan titik, atau berapa pun potongan yang kamu mau. Gambar garis antara titik-titik ini, bikin mereka melengkung ke pusat bintang. Dengan quadraticCurveTo, kamu bisa pakai pusatnya sebagai titik kontrol.
Solusi:
<canvas width="600" height="200"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
function trapezoid(x, y) {
cx.beginPath();
cx.moveTo(x, y);
cx.lineTo(x + 50, y);
cx.lineTo(x + 70, y + 50);
cx.lineTo(x - 20, y + 50);
cx.closePath();
cx.stroke();
}
trapezoid(30, 30);
function diamond(x, y) {
cx.translate(x + 30, y + 30);
cx.rotate(Math.PI / 4);
cx.fillStyle = "red";
cx.fillRect(-30, -30, 60, 60);
cx.resetTransform();
}
diamond(140, 30);
function zigzag(x, y) {
cx.beginPath();
cx.moveTo(x, y);
for (let i = 0; i < 8; i++) {
cx.lineTo(x + 80, y + i * 8 + 4);
cx.lineTo(x, y + i * 8 + 8);
}
cx.stroke();
}
zigzag(240, 20);
function spiral(x, y) {
let radius = 50, xCenter = x + radius, yCenter = y + radius;
cx.beginPath();
cx.moveTo(xCenter, yCenter);
for (let i = 0; i < 300; i++) {
let angle = i * Math.PI / 30;
let dist = radius * i / 300;
cx.lineTo(xCenter + Math.cos(angle) * dist,
yCenter + Math.sin(angle) * dist);
}
cx.stroke();
}
spiral(340, 20);
function star(x, y) {
let radius = 50, xCenter = x + radius, yCenter = y + radius;
cx.beginPath();
cx.moveTo(xCenter + radius, yCenter);
for (let i = 1; i <= 8; i++) {
let angle = i * Math.PI / 4;
cx.quadraticCurveTo(xCenter, yCenter,
xCenter + Math.cos(angle) * radius,
yCenter + Math.sin(angle) * radius);
}
cx.fillStyle = "gold";
cx.fill();
}
star(440, 20);
</script>Pie Chart
Sebelumnya di bab, kita lihat program contoh yang menggambar pie chart. Modifikasi program ini biar nama setiap kategori ditunjukin di samping irisan yang mewakilinya. Coba nemuin cara yang enak dilihat buat otomatis memposisikan teks ini yang bakal jalan buat dataset lain juga. Kamu boleh berasumsi bahwa kategori cukup besar buat ninggalin cukup ruang buat label mereka.
Kamu mungkin butuh Math.sin dan Math.cos lagi, yang dijelasin di Bab 14.
<canvas width="600" height="300"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let total = results
.reduce((sum, {count}) => sum + count, 0);
let currentAngle = -0.5 * Math.PI;
let centerX = 300, centerY = 150;
// Tambahin kode buat menggambar label irisan di loop ini.
for (let result of results) {
let sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
cx.arc(centerX, centerY, 100,
currentAngle, currentAngle + sliceAngle);
currentAngle += sliceAngle;
cx.lineTo(centerX, centerY);
cx.fillStyle = result.color;
cx.fill();
}
</script>Petunjuk:
Kamu bakal perlu manggil fillText dan nyetel properti textAlign dan textBaseline context begitu rupa sehingga teksnya berakhir di tempat yang kamu mau.
Cara yang masuk akal buat memposisikan label adalah naruh teks di garis yang pergi dari pusat pie lewat tengah irisannya. Kamu gak mau naruh teksnya langsung nempel di sisi pie tapi malah mindahin teksnya keluar ke sisi pie sebesar jumlah piksel tertentu.
Sudut garis ini adalah currentAngle + 0.5 * sliceAngle. Kode berikut nemuin posisi di garis ini 120 piksel dari pusat:
let middleAngle = currentAngle + 0.5 * sliceAngle;
let textX = Math.cos(middleAngle) * 120 + centerX;
let textY = Math.sin(middleAngle) * 120 + centerY;Buat textBaseline, nilai "middle" mungkin cocok waktu pakai pendekatan ini. Apa yang dipakai buat textAlign tergantung sisi lingkaran mana kita berada. Di kiri, harusnya "right", dan di kanan, harusnya "left", biar teksnya diposisikan menjauh dari pie.
Kalau kamu gak yakin cara nyari tahu sisi lingkaran mana sudut tertentu berada, lihat penjelasan Math.cos di Bab 14. Kosinus dari sudut ngasih tahu kita koordinat-x yang dia sesuaikan, yang pada gilirannya ngasih tahu kita persis sisi lingkaran mana kita berada.
Solusi:
<canvas width="600" height="300"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let total = results.reduce(function(sum, choice) {
return sum + choice.count;
}, 0);
let currentAngle = -0.5 * Math.PI;
let centerX = 300, centerY = 150;
results.forEach(function(result) {
let sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
cx.arc(centerX, centerY, 100,
currentAngle, currentAngle + sliceAngle);
let middleAngle = currentAngle + 0.5 * sliceAngle;
let textX = Math.cos(middleAngle) * 120 + centerX;
let textY = Math.sin(middleAngle) * 120 + centerY;
cx.textBaseLine = "middle";
if (Math.cos(middleAngle) > 0) {
cx.textAlign = "left";
} else {
cx.textAlign = "right";
}
cx.font = "15px sans-serif";
cx.fillStyle = "black";
cx.fillText(result.name, textX, textY);
currentAngle += sliceAngle;
cx.lineTo(centerX, centerY);
cx.fillStyle = result.color;
cx.fill();
});
</script>Bola Memantul
Pakai teknik requestAnimationFrame yang kita lihat di Bab 14 dan Bab 16 buat menggambar kotak dengan bola memantul di dalamnya. Bola bergerak dengan kecepatan konstan dan memantul dari sisi kotak waktu dia kena mereka.
<canvas width="400" height="400"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let lastTime = null;
function frame(time) {
if (lastTime != null) {
updateAnimation(Math.min(100, time - lastTime) / 1000);
}
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
function updateAnimation(step) {
// Kodemu di sini.
}
</script>Petunjuk:
Kotak gampang digambar dengan strokeRect. Definisikan binding yang megang ukurannya, atau definisikan dua binding kalau lebar dan tinggi kotakmu beda. Buat bikin bola bundar, mulai path dan manggil arc(x, y, radius, 0, 7), yang bikin busur yang pergi dari nol sampai lebih dari satu lingkaran penuh. Lalu isi path-nya.
Buat memodelkan posisi dan kecepatan bola, kamu bisa pakai class Vec dari Bab 16. Kasih dia kecepatan awal, sebaiknya yang gak murni vertikal atau horizontal, dan buat setiap frame kaliin kecepatan itu dengan jumlah waktu yang berlalu. Waktu bola jadi terlalu dekat ke dinding vertikal, balik komponen x di kecepatannya. Sama juga, balik komponen y waktu dia kena dinding horizontal.
Setelah nemuin posisi dan kecepatan baru bola, pakai clearRect buat ngehapus scene-nya dan menggambar ulangnya pakai posisi baru.
Solusi:
<canvas width="400" height="400"></canvas>
<script>
let cx = document.querySelector("canvas").getContext("2d");
let lastTime = null;
function frame(time) {
if (lastTime != null) {
updateAnimation(Math.min(100, time - lastTime) / 1000);
}
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
let x = 100, y = 300;
let radius = 10;
let speedX = 100, speedY = 60;
function updateAnimation(step) {
cx.clearRect(0, 0, 400, 400);
cx.strokeStyle = "blue";
cx.lineWidth = 4;
cx.strokeRect(25, 25, 350, 350);
x += step * speedX;
y += step * speedY;
if (x < 25 + radius || x > 375 - radius) speedX = -speedX;
if (y < 25 + radius || y > 375 - radius) speedY = -speedY;
cx.fillStyle = "red";
cx.beginPath();
cx.arc(x, y, radius, 0, 7);
cx.fill();
}
</script>Pencerminan yang Dihitung Sebelumnya
Satu hal yang disayangkan soal transformasi adalah mereka memperlambat menggambar bitmap. Posisi dan ukuran setiap piksel harus ditransformasi, dan walaupun mungkin browser bakal jadi lebih cerdas soal transformasi di masa depan, mereka saat ini bikin peningkatan yang bisa diukur di waktu yang dibutuhin buat menggambar bitmap.
Di game kaya kita, di mana kita cuma menggambar satu sprite yang ditransformasi, ini bukan masalah. Tapi bayangin bahwa kita perlu menggambar ratusan karakter atau ribuan partikel berputar dari ledakan.
Pikirin cara buat menggambar karakter yang terbalik tanpa ngeload file gambar tambahan dan tanpa harus bikin pemanggilan drawImage yang ditransformasi setiap frame.
Petunjuk:
Kunci solusinya adalah fakta bahwa kita bisa pakai elemen canvas sebagai gambar sumber waktu pakai drawImage. Bisa aja bikin elemen <canvas> ekstra, tanpa nambahinnya ke dokumen, dan menggambar sprite terbalik kita ke dia, sekali. Waktu menggambar frame beneran, kita cuma nyalin sprite yang udah terbalik ke canvas utama.
Beberapa kehati-hatian bakal dibutuhin karena gambar gak dimuat seketika. Kita ngelakuin menggambar terbalik cuma sekali, dan kalau kita ngelakuinnya sebelum gambarnya dimuat, dia gak bakal menggambar apa pun. Handler "load" di gambarnya bisa dipakai buat menggambar gambar terbalik ke canvas ekstra. Canvas ini bisa dipakai sebagai sumber menggambar langsung (dia bakal cuma kosong sampai kita menggambar karakternya ke dia).