لقد كنت أعمل مؤخرًا على مشروع يتعلق بمعالجة صور صفحات الويب، والذي يمكن اعتباره تجربتي الأولى مع القماش. تتضمن متطلبات المشروع وظيفة لإضافة علامات مائية إلى الصور. نحن نعلم أن الطريقة المعتادة لإضافة علامات مائية إلى الصور على جانب المتصفح هي استخدام طريقة drawImage
الخاصة canvas
. بالنسبة للتوليف العادي (مثل تركيب الصورة الأساسية وصورة العلامة المائية PNG)، يكون مبدأ التنفيذ العام كما يلي:
var Canvas = document.getElementById(canvas);var ctx = Canvas.getContext('2d');// img: الصورة الأساسية // watermarkImg: صورة العلامة المائية // x, y هي إحداثيات وضع img على اللوحة القماشية ctx. drawImage( img, x, y);ctx.drawImage(watermarkImg, x, y);
ما عليك سوى استخدام drawImage()
بشكل مباشر ومستمر لرسم الصورة المقابلة على canvas
.
ما ورد أعلاه هو مقدمة الخلفية. ولكن الأمر المزعج بعض الشيء هو أن هناك وظيفة أخرى يجب تنفيذها في الحاجة إلى إضافة علامة مائية، وهي أنه يمكن للمستخدم تبديل موضع العلامة المائية. من الطبيعي أن نفكر فيما إذا كان بإمكاننا تنفيذ وظيفة undo
في canvas
. عندما يقوم المستخدم بتبديل موضع العلامة المائية، قم أولاً بالتراجع عن عملية drawImage
السابقة، ثم إعادة رسم موضع صورة العلامة المائية.
restore
/ save
؟
الطريقة الأكثر فعالية وملاءمة هي التحقق مما إذا كانت واجهة برمجة التطبيقات الأصلية canvas 2D
تحتوي على هذه الوظيفة. بعد إجراء بعض البحث، ظهر زوج من واجهات برمجة التطبيقات restore
/ save
. دعونا أولاً نلقي نظرة على أوصاف هاتين الواجهتين API:
CanvasRenderingContext2D.restore() هي طريقة Canvas 2D API لاستعادة اللوحة القماشية إلى حالتها المحفوظة مؤخرًا عن طريق إظهار الحالة العليا في مكدس حالة الرسم. إذا لم تكن هناك حالة محفوظة، فلن تقوم هذه الطريقة بإجراء أي تغييرات.
CanvasRenderingContext2D.save() هي طريقة Canvas 2D API لحفظ حالة اللوحة القماشية بالكامل عن طريق وضع الحالة الحالية في المكدس.
للوهلة الأولى يبدو أنه يلبي الاحتياجات. دعونا نلقي نظرة على نموذج التعليمات البرمجية الرسمي:
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، Font، textAlign، textBaseline، Direction، imageSmoothingEnabled.
حسنًا، التغييرات التي تم إجراؤها على اللوحة القماشية بعد عملية drawImage
غير موجودة في حالة الرسم على الإطلاق. ولذلك، فإن استخدام resolve
/ save
لا يمكن أن يحقق وظيفة التراجع التي نحتاجها.
نظرًا لأن مكدس واجهة برمجة التطبيقات الأصلية لحفظ حالة الرسم لا يمكنه تلبية الاحتياجات، فمن الطبيعي أن نفكر في محاكاة مكدس لحفظ عمليات الحفظ بأنفسنا. والسؤال التالي هو: ما هي البيانات التي يجب حفظها على المكدس بعد كل عملية رسم؟ كما ذكرنا من قبل، ما نريده هو أن نكون قادرين على حفظ لقطة من اللوحة القماشية الحالية بعد كل عملية رسم. إذا تمكنا من الحصول على بيانات اللقطة واستخدام بيانات اللقطة لاستعادة اللوحة القماشية، فسيتم حل المشكلة.
لحسن الحظ، يوفر canvas 2D
أصلاً واجهات برمجة التطبيقات للحصول على اللقطات واستعادة اللوحة القماشية من خلال اللقطات - getImageData
/ putImageData
. فيما يلي وصف واجهة برمجة التطبيقات:
/* * @param { Number } sx الإحداثي x للزاوية اليسرى العليا للمنطقة المستطيلة من بيانات الصورة المراد استخراجها * @param { Number } sy الإحداثي y للركن الأيسر العلوي للمنطقة المستطيلة بيانات الصورة المراد استخراجها * @param { Number } sw سيكون عرض المساحة المستطيلة لبيانات الصورة المراد استخراجها * @param { Number } sh ارتفاع المساحة المستطيلة لبيانات الصورة المراد استخراجها * @return { Object } تحتوي بيانات الصورة على قماش نظرا لبيانات الصورة المستطيلة */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } كائن بيانات الصورة الذي يحتوي على قيم البكسل * @param { Number } dx بيانات الصورة المصدر في اللوحة القماشية المستهدفة إزاحة الموضع (الإزاحة في اتجاه المحور السيني) * @param { Number } dy إزاحة الموضع لبيانات الصورة المصدر في اللوحة القماشية المستهدفة (الإزاحة في اتجاه المحور الصادي) */ ctx.putImageData(imagedata, dx, dy);
دعونا نلقي نظرة على تطبيق بسيط:
class WrappedCanvas { builder (canvas) { 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); (); this.ctx.putImageData(imgData, 0, 0));
نقوم بتغليف طريقة drawImage
الخاصة canvas
، ونحفظ لقطة من الحالة السابقة في المكدس المحاكى قبل كل استدعاء لهذه الطريقة. عند إجراء عملية undo
، قم بإزالة أحدث لقطة محفوظة من المكدس ثم أعد رسم اللوحة القماشية لتنفيذ عملية التراجع. كما حقق الاختبار الفعلي التوقعات.
في القسم السابق، قمنا بتنفيذ وظيفة التراجع في canvas
بشكل تقريبي للغاية. لماذا تقول الخام؟ أحد الأسباب الواضحة هو أن أداء هذا الحل ضعيف. الحل الذي نقدمه يعادل إعادة رسم اللوحة القماشية بأكملها في كل مرة. بافتراض أن هناك العديد من خطوات التشغيل، سنحفظ الكثير من بيانات الصور المخزنة مسبقًا في مكدس المحاكاة، وهو الذاكرة. بالإضافة إلى ذلك، عندما يكون رسم الصور معقدًا للغاية، فإن الطريقتين getImageData
و putImageData
ستسببان مشاكل خطيرة في الأداء. هناك مناقشة مفصلة حول تدفق المكدس: لماذا يكون putImageData بطيئًا جدًا؟ يمكننا أيضًا التحقق من ذلك من خلال بيانات حالة الاختبار هذه على jsperf. ذكر Taobao FED أيضًا في Canvas أفضل الممارسات التي تحاول عدم استخدام طريقة putImageData
في الرسوم المتحركة. بالإضافة إلى ذلك، ذكرت المقالة أيضًا أنه يجب استدعاء واجهات برمجة التطبيقات ذات الحمل الأقل للعرض قدر الإمكان. يمكننا أن نبدأ من هنا للتفكير في كيفية التحسين.
كما ذكرنا من قبل، نقوم بتسجيل كل عملية عن طريق حفظ لقطة من اللوحة القماشية بأكملها. وبالتفكير في الأمر من زاوية أخرى، إذا قمنا بحفظ كل إجراء رسم في مصفوفة، في كل مرة يتم فيها تنفيذ عملية تراجع، يتم مسح اللوحة القماشية أولاً، ثم بعد ذلك. يمكن لإعادة رسم مصفوفة إجراءات الرسم هذه أيضًا تنفيذ وظيفة التراجع عن العملية. من حيث الجدوى، أولاً وقبل كل شيء، يمكن أن يقلل هذا من كمية البيانات المحفوظة في الذاكرة، وثانيًا، يتجنب استخدام putImageData
يحتوي على حمل عرض أعلى. بأخذ drawImage
ككائن مقارنة والنظر إلى حالة الاختبار هذه على jsperf، يوجد اختلاف في الحجم في الأداء بين الاثنين.
لذلك، نعتقد أن هذا الحل الأمثل ممكن.
طريقة التطبيق المحسنة هي تقريبًا كما يلي:
class WrappedCanvas { builder (canvas) { this.ctx = Canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } drawImage (...params) { this.executionArray.push({ الطريقة: 'drawImage'، المعلمات: المعلمات });this.ctx.drawImage(...params); } ClearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height } undo () { if (this.executionArray. length > 0) { // امسح اللوحة القماشية this.clearCanvas(); // احذف العملية الحالية this.executionArray.pop(); // تنفيذ إجراءات الرسم واحدًا تلو الآخر لإعادة الرسم (اسمح لـ exe بـ this.executionArray) { this[exe.method](...exe.params) } } }}
إذا كنت جديدًا على اللوحة القماشية، فيرجى الإشارة إلى أي أخطاء أو أوجه قصور. ما ورد أعلاه هو المحتوى الكامل لهذه المقالة وآمل أن يكون مفيدًا لدراسة الجميع وآمل أيضًا أن يدعم الجميع شبكة VeVb Wulin.