Baru-baru ini saya sedang mengerjakan proyek yang berkaitan dengan pemrosesan gambar halaman web, yang dapat dianggap sebagai pengalaman pertama saya dengan kanvas. Persyaratan proyek mencakup fungsi untuk menambahkan tanda air ke gambar. Kita tahu bahwa cara biasa untuk menambahkan tanda air ke gambar di sisi browser adalah dengan menggunakan metode drawImage
di canvas
. Untuk sintesis biasa (seperti sintesis gambar dasar dan gambar tanda air PNG), prinsip penerapan umumnya adalah sebagai berikut:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img: gambar dasar // watermarkImg: gambar watermark // x, y adalah koordinat penempatan img pada kanvas ctx. drawImage( img, x, y);ctx.drawImage(tanda airImg, x, y);
Cukup gunakan drawImage()
secara langsung dan terus menerus untuk menggambar gambar yang sesuai ke canvas
.
Di atas adalah latar belakang pengenalan. Namun yang sedikit merepotkan adalah ada fungsi lain yang perlu diterapkan dalam perlunya menambahkan watermark, yaitu pengguna dapat berpindah posisi dari watermark tersebut. Kami secara alami memikirkan apakah kami dapat mengimplementasikan fungsi undo
pada canvas
. Saat pengguna mengganti posisi tanda air, pertama-tama batalkan operasi drawImage
sebelumnya, lalu gambar ulang posisi gambar tanda air.
restore
/ save
?
Cara paling efisien dan nyaman adalah memeriksa apakah API asli canvas 2D
memiliki fungsi ini. Setelah beberapa pencarian, pasangan restore
/ save
API mulai terlihat. Mari kita lihat terlebih dahulu deskripsi kedua API ini:
CanvasRenderingContext2D.restore() adalah metode Canvas 2D API untuk memulihkan kanvas ke keadaan terakhir disimpan dengan memunculkan keadaan teratas dalam tumpukan keadaan gambar. Jika tidak ada status tersimpan, metode ini tidak membuat perubahan.
CanvasRenderingContext2D.save() adalah metode Canvas 2D API untuk menyimpan seluruh status kanvas dengan memasukkan status saat ini ke dalam tumpukan.
Sekilas sepertinya memenuhi kebutuhan. Mari kita lihat kode contoh resminya:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext(2d);ctx.save(); // Simpan keadaan default ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //Kembalikan ke keadaan default terakhir yang disimpan ctx.fillRect(150, 75, 100, 100);
Hasilnya ditunjukkan di bawah ini:
Anehnya, sepertinya tidak sesuai dengan hasil yang kami harapkan. Hasil yang kita inginkan adalah dapat menyimpan snapshot dari kanvas saat ini setelah memanggil metode save
, dan dapat sepenuhnya kembali ke keadaan snapshot terakhir yang disimpan setelah memanggil metode resolve
.
Mari kita lihat lebih dekat API-nya. Ternyata kita melewatkan sebuah konsep penting: drawing state
, yaitu drawing state. Status gambar yang disimpan ke tumpukan berisi bagian-bagian berikut:
Nilai saat ini dari properti berikut: strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, arah, imageSmoothingEnabled.
Nah, perubahan pada kanvas setelah operasi drawImage
tidak ada sama sekali dalam status draw. Oleh karena itu, menggunakan resolve
/ save
tidak dapat mencapai fungsi undo yang kita perlukan.
Karena tumpukan API asli untuk menyimpan status gambar tidak dapat memenuhi kebutuhan, tentu saja kita akan memikirkan untuk mensimulasikan sendiri tumpukan untuk menyimpan operasi. Pertanyaan selanjutnya adalah: Data apa yang harus disimpan di tumpukan setelah setiap operasi menggambar? Seperti disebutkan sebelumnya, yang kita inginkan adalah bisa menyimpan snapshot dari kanvas saat ini setelah setiap operasi menggambar. Jika kita bisa mendapatkan data snapshot dan menggunakan data snapshot untuk memulihkan kanvas, masalahnya akan terpecahkan.
Untungnya, canvas 2D
secara asli menyediakan API untuk memperoleh snapshot dan memulihkan kanvas melalui snapshot - getImageData
/ putImageData
. Berikut deskripsi API-nya:
/* * @param { Bilangan } sx Koordinat x pojok kiri atas luas persegi panjang data gambar yang akan diekstraksi * @param { Bilangan } sy Koordinat y pojok kiri atas luas persegi data gambar yang akan diekstraksi * @param { Angka } sw Akan menjadi Lebar luas persegi dari data gambar yang akan diekstraksi * @param { Angka } sh Tinggi dari luas persegi dari data gambar yang akan diekstraksi * @return { Objek } ImageData berisi kanvas Diberikan data gambar persegi panjang */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } objek imagedata yang berisi nilai piksel * @param { Number } dx data gambar sumber di kanvas target offset posisi (offset pada arah sumbu x) * @param { Angka } dy Offset posisi data gambar sumber pada kanvas target (offset pada arah sumbu y) */ batal ctx.putImageData(data gambar, dx, dy);
Mari kita lihat aplikasi sederhana:
kelas WrappedCanvas { konstruktor (kanvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; ]; } drawImage (...params) { const imgData = ini.ctx.getImageData(0, 0, ini.lebar, ini.tinggi); ini.imgStack.push(imgData);ini.ctx.drawImage(...params); } batalkan () { jika (ini.imgStack.panjang > 0) { const imgData = ini.imgStack.pop (); ini.ctx.putImageData(imgData, 0, 0);
Kami merangkum metode drawImage
dari canvas
, dan menyimpan snapshot dari keadaan sebelumnya ke tumpukan simulasi sebelum setiap panggilan ke metode ini. Saat melakukan operasi undo
, hapus snapshot terakhir yang disimpan dari tumpukan, lalu gambar ulang kanvas untuk menerapkan operasi pembatalan. Pengujian sebenarnya juga memenuhi harapan.
Pada bagian sebelumnya, kita menerapkan fungsi undo pada canvas
dengan sangat kasar. Kenapa kamu bilang kasar? Salah satu alasan yang jelas adalah kinerja solusi ini buruk. Solusi kami setara dengan menggambar ulang seluruh kanvas setiap saat. Dengan asumsi ada banyak langkah operasi, kita akan menyimpan banyak data gambar yang disimpan sebelumnya di tumpukan simulasi, yaitu memori. Selain itu, jika menggambar terlalu rumit, kedua metode getImageData
dan putImageData
akan menyebabkan masalah kinerja yang serius. Ada diskusi mendetail tentang stackoverflow: Mengapa putImageData sangat lambat? Kami juga dapat memverifikasi ini dari data test case ini di jsperf. Taobao FED juga menyebutkan praktik terbaik Canvas yang mencoba untuk tidak menggunakan metode putImageData
dalam animasi. Selain itu, artikel tersebut juga menyebutkan bahwa API dengan overhead rendering yang lebih rendah harus dipanggil sesering mungkin. Kita bisa mulai dari sini memikirkan cara mengoptimalkannya.
Seperti disebutkan sebelumnya, kami merekam setiap operasi dengan menyimpan cuplikan seluruh kanvas. Memikirkannya dari sudut lain, jika kami menyimpan setiap tindakan menggambar dalam array, setiap kali operasi pembatalan dilakukan, kanvas terlebih dahulu dibersihkan, lalu kemudian Menggambar ulang susunan tindakan menggambar ini juga dapat mengimplementasikan fungsi membatalkan operasi. Dari segi kelayakan, pertama, hal ini dapat mengurangi jumlah data yang disimpan ke memori, dan kedua, menghindari penggunaan putImageData
memiliki overhead rendering lebih tinggi. Mengambil drawImage
sebagai objek perbandingan dan melihat kasus uji ini di jsperf, terdapat perbedaan besar dalam kinerja di antara keduanya.
Oleh karena itu, kami yakin solusi pengoptimalan ini layak dilakukan.
Metode penerapan yang ditingkatkan kira-kira sebagai berikut:
kelas WrappedCanvas { konstruktor (kanvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; ]; } drawImage (...params) { this.executionArray.push({ metode: 'drawImage', params: params });ini.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height); length > 0) { // Hapus kanvas this.clearCanvas(); // Hapus operasi saat ini this.executionArray.pop(); // Jalankan tindakan menggambar satu per satu untuk menggambar ulang (biarkan exe dari this.executionArray) { ini[exe.metode](...exe.params) } } }}
Jika Anda baru mengenal kanvas, harap tunjukkan kesalahan atau kekurangan apa pun. Di atas adalah keseluruhan isi artikel ini, saya harap dapat bermanfaat untuk pembelajaran semua orang. Saya juga berharap semua orang mendukung VeVb Wulin Network.