เมื่อเร็วๆ นี้ ฉันได้ทำงานในโครงการที่เกี่ยวข้องกับการประมวลผลภาพหน้าเว็บ ซึ่งถือได้ว่าเป็นประสบการณ์ครั้งแรกของฉันกับ Canvas ข้อกำหนดของโปรเจ็กต์ประกอบด้วยฟังก์ชันในการเพิ่มลายน้ำให้กับรูปภาพ เรารู้ว่าวิธีปกติในการเพิ่มลายน้ำให้กับรูปภาพทางฝั่งเบราว์เซอร์คือการใช้วิธี drawImage
ของ canvas
สำหรับการสังเคราะห์แบบธรรมดา (เช่น การสังเคราะห์ภาพฐานและภาพลายน้ำ PNG) หลักการใช้งานทั่วไปมีดังนี้:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img: base image // watermarkImg: watermark image // x, y คือพิกัดของการวาง img บนผืนผ้าใบ ctx DrawImage( img, x, y);ctx.drawImage(ลายน้ำImg, x, y);
เพียงใช้ drawImage()
โดยตรงและต่อเนื่องเพื่อวาดภาพที่เกี่ยวข้องลงบน canvas
ข้างต้นเป็นการแนะนำพื้นหลัง แต่ที่ลำบากนิดหน่อยคือมีอีกฟังก์ชั่นที่ต้องใส่ลงไปคือต้องใส่ลายน้ำคือผู้ใช้สามารถสลับตำแหน่งของลายน้ำได้ โดยธรรมชาติแล้วเราจะคิดว่าเราสามารถใช้ฟังก์ชัน undo
ของ canvas
ได้หรือไม่ เมื่อผู้ใช้เปลี่ยนตำแหน่งลายน้ำ ให้เลิกทำการดำเนินการ drawImage
ก่อนหน้า จากนั้นจึงวาดตำแหน่งรูปภาพลายน้ำใหม่
restore
/ save
?
วิธีที่มีประสิทธิภาพและสะดวกที่สุดคือตรวจสอบว่า canvas 2D
Native API มีฟังก์ชันนี้หรือไม่ หลังจากค้นหาแล้ว คู่ของ API restore
/ save
ก็ปรากฏขึ้น ก่อนอื่น เรามาดูรายละเอียดของ API ทั้งสองนี้กันก่อน:
CanvasRenderingContext2D.restore() คือวิธี Canvas 2D API เพื่อคืนค่าแคนวาสเป็นสถานะที่บันทึกไว้ล่าสุดโดยการเปิดสถานะบนสุดในสแต็กสถานะการวาด หากไม่มีสถานะที่บันทึกไว้ วิธีการนี้จะไม่มีการเปลี่ยนแปลง
CanvasRenderingContext2D.save() เป็นวิธีการของ Canvas 2D API เพื่อบันทึกสถานะทั้งหมดของ Canvas โดยใส่สถานะปัจจุบันลงในสแต็ก
ดูเผินๆก็ดูเหมือนจะสนองความต้องการ มาดูโค้ดตัวอย่างอย่างเป็นทางการกัน:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext(2d);ctx.save(); // บันทึกสถานะเริ่มต้น ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //เรียกคืนสถานะเริ่มต้นที่บันทึกไว้ล่าสุด ctx.fillRect(150, 75, 100, 100);
ผลลัพธ์แสดงไว้ด้านล่าง:
น่าแปลกที่ดูเหมือนว่าจะไม่สอดคล้องกับผลลัพธ์ที่เราคาดหวัง ผลลัพธ์ที่เราต้องการคือสามารถบันทึกสแน็ปช็อตของแคนวาสปัจจุบันได้หลังจากเรียกใช้วิธี save
และสามารถกลับสู่สถานะของสแน็ปช็อตที่บันทึกไว้ล่าสุดได้อย่างสมบูรณ์หลังจากเรียกใช้วิธี resolve
มาดู API กันดีกว่า ปรากฎว่าเราพลาดแนวคิดสำคัญ: drawing state
ซึ่งเป็นสถานะการวาด สถานะการวาดที่บันทึกลงในสแต็กประกอบด้วยส่วนต่างๆ ดังต่อไปนี้:
ค่าปัจจุบันของคุณสมบัติต่อไปนี้: strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, แบบอักษร, textAlign, textBaseline, ทิศทาง, imageSmoothingEnabled
การเปลี่ยนแปลงผืนผ้าใบหลังจากการดำเนินการ drawImage
ไม่มีอยู่ในสถานะการวาดเลย ดังนั้นการใช้ resolve
/ save
จึงไม่สามารถบรรลุฟังก์ชันเลิกทำที่เราต้องการได้
เนื่องจากสแต็กของ API ดั้งเดิมสำหรับการบันทึกสถานะรูปวาดไม่สามารถตอบสนองความต้องการได้ โดยปกติแล้ว เราจะคิดถึงการจำลองสแต็กสำหรับการบันทึกการดำเนินการด้วยตนเอง คำถามต่อไปนี้คือ ข้อมูลใดที่ควรบันทึกไว้ในสแต็กหลังจากการดำเนินการวาดแต่ละครั้ง ตามที่กล่าวไว้ก่อนหน้านี้ สิ่งที่เราต้องการคือสามารถบันทึก สแน็ปช็อต ของผืนผ้าใบปัจจุบันได้หลังจากการดำเนินการวาดแต่ละครั้ง หากเราสามารถรับข้อมูลสแน็ปช็อตและใช้ข้อมูลสแน็ปช็อตเพื่อกู้คืนผืนผ้าใบ ปัญหาจะได้รับการแก้ไข
โชคดีที่ canvas 2D
มี API สำหรับการรับสแน็ปช็อตและกู้คืน Canvas ผ่านสแน็ปช็อต - getImageData
/ putImageData
ต่อไปนี้เป็นคำอธิบาย API:
/* * @param { Number } sx พิกัด x ของมุมซ้ายบนของพื้นที่สี่เหลี่ยมของข้อมูลรูปภาพที่จะแยกออกมา * @param { Number } sy พิกัด y ของมุมซ้ายบนของพื้นที่สี่เหลี่ยมของ ข้อมูลรูปภาพที่จะแยก * @param { Number } sw จะเป็น ความกว้างของพื้นที่สี่เหลี่ยมของข้อมูลรูปภาพที่จะแยก * @param { Number } sh ความสูงของพื้นที่สี่เหลี่ยมของข้อมูลรูปภาพที่จะแยก * @return { Object } ImageData มีแคนวาส กำหนดข้อมูลภาพสี่เหลี่ยม */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } imagedata object ที่มีค่าพิกเซล * @param { Number } dx source image data in the target canvas ออฟเซ็ตตำแหน่ง (ออฟเซ็ตในทิศทางแกน x) * @param { หมายเลข } dy ออฟเซ็ตตำแหน่งของข้อมูลรูปภาพต้นฉบับในพื้นที่เป้าหมาย (ออฟเซ็ตในทิศทางแกน y) */ เป็นโมฆะ ctx.putImageData (imagedata, dx, dy);
มาดูแอปพลิเคชันง่ายๆ:
class WrappedCanvas { ตัวสร้าง (ผ้าใบ) { this.ctx = canvas.getContext ('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } DrawImage (...params) { const imgData = this.ctx.getImageData(0, 0, this.width, this.height); this.imgStack.push(imgData);this.ctx.drawImage(...params); } เลิกทำ () { if (this.imgStack.length > 0) { const imgData = this.imgStack.pop (); this.ctx.putImageData(imgData, 0, 0);
เราสรุปวิธี drawImage
ของ canvas
และบันทึกสแน็ปช็อตของสถานะก่อนหน้าลงในสแต็กจำลองก่อนที่จะเรียกใช้วิธีนี้แต่ละครั้ง เมื่อดำเนินการ undo
ทำ ให้ลบสแน็ปช็อตที่บันทึกไว้ล่าสุดออกจากสแต็ก จากนั้นวาดภาพแคนวาสใหม่เพื่อใช้การดำเนินการเลิกทำ การทดสอบจริงก็เป็นไปตามความคาดหวังเช่นกัน
ในส่วนก่อนหน้านี้ เราได้ใช้ฟังก์ชันเลิกทำของ canvas
อย่างคร่าว ๆ ทำไมคุณถึงบอกว่าหยาบ? เหตุผลที่ชัดเจนประการหนึ่งก็คือโซลูชันนี้ทำงานได้ไม่ดี โซลูชันของเราเทียบเท่ากับการวาดผืนผ้าใบใหม่ทั้งหมดทุกครั้ง สมมติว่ามีขั้นตอนการดำเนินการหลายขั้นตอน เราจะบันทึกข้อมูลรูปภาพที่เก็บไว้ล่วงหน้าจำนวนมากไว้ในสแต็กการจำลองซึ่งก็คือหน่วยความจำ นอกจากนี้ เมื่อการวาดภาพซับซ้อนเกินไป สองวิธี getImageData
และ putImageData
จะทำให้เกิดปัญหาประสิทธิภาพการทำงานที่ร้ายแรง มีการอภิปรายโดยละเอียดเกี่ยวกับ stackoverflow: เหตุใด putImageData จึงช้ามาก นอกจากนี้เรายังสามารถตรวจสอบสิ่งนี้ได้จากข้อมูลของกรณีทดสอบนี้บน jsperf Taobao FED ยังกล่าวถึงแนวทางปฏิบัติที่ดีที่สุดของ Canvas ที่พยายามไม่ใช้วิธี putImageData
ในภาพเคลื่อนไหว นอกจากนี้ บทความยังกล่าวด้วยว่าควรเรียกใช้ API ที่มีค่าใช้จ่ายในการเรนเดอร์ต่ำกว่าให้มากที่สุดเท่าที่จะเป็นไปได้ เราสามารถเริ่มต้นจากที่นี่เพื่อคิดถึงวิธีเพิ่มประสิทธิภาพได้
ตามที่กล่าวไว้ก่อนหน้านี้ เราบันทึกแต่ละการดำเนินการโดยบันทึกสแน็ปช็อตของผืนผ้าใบทั้งหมด ลองคิดถึงมันจากอีกมุมหนึ่ง ถ้าเราบันทึกการดำเนินการวาดแต่ละครั้งในอาร์เรย์ แต่ละครั้งที่มีการดำเนินการเลิกทำ ผืนผ้าใบจะถูกล้างก่อน จากนั้น การวาดอาร์เรย์การดำเนินการการวาดใหม่นี้ยังสามารถใช้ฟังก์ชันการยกเลิกการดำเนินการได้อีกด้วย ในแง่ของความเป็นไปได้ ประการแรก สิ่งนี้สามารถลดปริมาณข้อมูลที่บันทึกไว้ในหน่วยความจำ และประการที่สอง จะหลีกเลี่ยงการใช้ putImageData
มีค่าใช้จ่ายในการเรนเดอร์ที่สูงกว่า การใช้ drawImage
เป็นวัตถุเปรียบเทียบและดูกรณีทดสอบนี้บน jsperf มีประสิทธิภาพที่แตกต่างกันตามลำดับขนาด
ดังนั้นเราจึงเชื่อว่าโซลูชันการปรับให้เหมาะสมนี้เป็นไปได้
วิธีการสมัครที่ได้รับการปรับปรุงมีดังนี้:
class WrappedCanvas { ตัวสร้าง (ผ้าใบ) { this.ctx = canvas.getContext ('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } DrawImage (...params) { this.executionArray.push({ วิธีการ: 'drawImage', พารามิเตอร์: params });this.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height); } เลิกทำ () { ถ้า (this.executionArray. length > 0) { // ล้างผ้าใบ this.clearCanvas(); // ลบการดำเนินการปัจจุบัน this.executionArray.pop(); // ดำเนินการวาดทีละรายการเพื่อวาดใหม่ (ให้ exe ของ this.executionArray) { นี้[exe.method](...exe.params) } } }}
หากคุณยังใหม่กับ Canvas โปรดชี้ให้เห็นข้อผิดพลาดหรือข้อบกพร่อง ข้างต้นคือเนื้อหาทั้งหมดของบทความนี้ ฉันหวังว่ามันจะเป็นประโยชน์ต่อการศึกษาของทุกคน ฉันหวังว่าทุกคนจะสนับสนุน VeVb Wulin Network