محرر صور بسيط على VueJS

أتيحت لي الفرصة مؤخرًا لكتابة خدمة لمتجر عبر الإنترنت من شأنه أن يساعد في تقديم طلب لطباعة صوري.

افترضت الخدمة محرر صور "بسيط" أود أن أشاركه في إنشائه. وكل ذلك لأنه من بين وفرة جميع أنواع الإضافات لم أجد وظائف مناسبة ، بالإضافة إلى الفروق الدقيقة في تحويلات CSS ، أصبحت فجأة مهمة غير تافهة بالنسبة لي.

الصورة

المهام الرئيسية:


  1. القدرة على تحميل الصور من جهازك و Google Drive و Instagram.
  2. تحرير الصور: تحريك ، تدوير ، انعكاس أفقي ورأسي ، تكبير ، محاذاة تلقائية للصورة لملء منطقة القص.

إذا تبين أن الموضوع مثير للاهتمام ، فسأصف في المنشور التالي بالتفصيل التكامل مع Google Drive و Instagram في الجزء الخلفي من التطبيق ، حيث تم استخدام حزمة NodeJS + Express الشهيرة.

لتنظيم الواجهة الأمامية ، اخترت إطار Vue الرائع. فقط لأنه يلهمني بعد رد فعل زاوي صعب ومزعج. أعتقد أنه ليس من المنطقي وصف الهندسة والطرق والمكونات الأخرى ، فلنذهب مباشرة إلى المحرر.

بالمناسبة ، يمكن عرض النسخة التجريبية من المحرر هنا .

سنحتاج إلى مكونين:

تحرير - سيحتوي على المنطق وعناصر التحكم الرئيسية
معاينة - ستكون مسؤولة عن عرض الصورة

تحرير قالب المكون:
<Edit> <Preview v-if="image" ref="preview" :matrix="matrix" :image="image" :transform="transform" @resized="areaResized" @loaded="imageLoaded" @moved="imageMoved" /> <input type="range" :min="minZoom" :max="maxZoom" step="any" @change="onZoomEnd" v-model.number="transform.zoom" :disabled="!imageReady" /> <button @click="rotateMinus" :disabled="!imageReady">Rotate left</button> <button @click="rotatePlus" :disabled="!imageReady">Rotate right</button> <button @click="flipY" :disabled="!imageReady">Flip horizontal</button> <button @click="flipX" :disabled="!imageReady">Flip vertical</button> </Edit> 


يمكن أن يشغّل مكوّن المعاينة 3 أحداث:

حدث تحميل صورة - تحميل
تغيير حجم - حدث تغيير حجم النافذة
حدث - صورة متحركة

معلمات:

صورة - رابط صورة
matrix - مصفوفة تحويل لخاصية تحويل CSS
تحويل - كائن يصف التحول

من أجل التحكم بشكل أفضل في موضع الصورة ، فإن img لها موضع مطلق ، ويتم تعيين خاصية التحويل إلى الأصل ، وهي النقطة المرجعية للتحويل ، على القيمة الأولية "0 0" ، والتي تقابل الأصل في الزاوية اليسرى العليا للصورة الأصلية (قبل التحويل!).

المشكلة الرئيسية التي واجهتها هي أنك تحتاج إلى التأكد من أن نقطة أصل التحويل دائمًا في وسط منطقة التحرير ، وإلا ، أثناء التحويل ، سيتم تغيير الجزء المحدد من الصورة. يساعد استخدام مصفوفة التحويل على حل هذه المشكلة.

تحرير المكون


خصائص مكون التحرير:
 export default { components: { Preview }, data () { return { image: null, imageReady: false, imageRect: {}, //   areaRect: {}, //   minZoom: 1, //   maxZoom: 1, //   //   transform: { center: { x: 0, y: 0, }, zoom: 1, rotate: 0, flip: false, flop: false, x: 0, y: 0 } } }, computed: { matrix() { let scaleX = this.transform.flop ? -this.transform.zoom : this.transform.zoom; let scaleY = this.transform.flip ? -this.transform.zoom : this.transform.zoom; let tx = this.transform.x; let ty = this.transform.y; const cos = Math.cos(this.transform.rotate * Math.PI / 180); const sin = Math.sin(this.transform.rotate * Math.PI / 180); let a = Math.round(cos)*scaleX; let b = Math.round(sin)*scaleX; let c = -Math.round(sin)*scaleY; let d = Math.round(cos)*scaleY; return { a, b, c, d, tx, ty }; } }, ... } 


يتم تمرير قيم imageRect و areaRect إلينا من قبل مكون المعاينة ، مستدعين الأساليب imageLoaded و areaResized ، على التوالي ، الكائنات لها البنية:

 { size: { width: 100, height: 100 }, center: { x: 50, y: 50 } } 

يمكن حساب قيم المركز في كل مرة ، ولكن من الأسهل تدوينها مرة واحدة.

إن خاصية المصفوفة المحسوبة هي نفس معاملات مصفوفة التحويل.

المهمة الأولى التي تحتاج إلى حل هي توسيط الصورة بنسبة عرضية تعسفية في منطقة الاقتصاص ، بينما يجب أن تكون الصورة قادرة على احتواء المناطق غير المكتملة تمامًا (فقط) أعلى وأسفل ، أو (فقط) اليسار واليمين مقبولة. مع أي تحويلات ، يجب الحفاظ على هذا الشرط.

أولاً ، سنحدد قيم التكبير ، ولهذا سنتحقق من نسبة العرض إلى الارتفاع ، مع مراعاة اتجاه الصورة.

طرق المكون:
  _setMinZoom(){ let rotate = this.matrix.c !== 0; let horizontal = this.imageRect.size.height < this.imageRect.size.width; let areaSize = (horizontal && !rotate || !horizontal && rotate) ? this.areaRect.size.width : this.areaRect.size.height; let imageSize = horizontal ? this.imageRect.size.width : this.imageRect.size.height; this.minZoom = areaSize/imageSize; if(this.transform.zoom < this.minZoom) this.transform.zoom = this.minZoom; }, _setMaxZoom(){ this.maxZoom = this.areaRect.size.width/config.image.minResolution; if(this.transform.zoom > this.maxZoom) this.transform.zoom = this.maxZoom; }, 


الآن دعنا ننتقل إلى التحولات. بادئ ذي بدء ، نصف الانعكاسات ، لأنها لا تغير المنطقة المرئية من الصورة.

طرق المكون:
 flipX(){ this.matrix.b == 0 && this.matrix.c == 0 ? this.transform.flip = !this.transform.flip : this.transform.flop = !this.transform.flop; }, flipY(){ this.matrix.b == 0 && this.matrix.c == 0 ? this.transform.flop = !this.transform.flop : this.transform.flip = !this.transform.flip; }, 


سوف تتطلب تحويلات التكبير والتدوير والإزاحة بالفعل تعديلات أصل التحويل.

طرق المكون:
 onZoomEnd(){ this._translate(); }, rotatePlus(){ this.transform.rotate += 90; this._setMinZoom(); this._translate(); }, rotateMinus(){ this.transform.rotate -= 90; this._setMinZoom(); this._translate(); }, imageMoved(translate){ this._translate(); }, 


إنها طريقة _translate المسؤولة عن كل التفاصيل الدقيقة للتحولات. يجب إدخال نظامين مرجعيين. الأولى ، دعنا نسميها صفر ، تبدأ في الزاوية اليسرى العليا من الصورة ، عندما نضرب الإحداثيات في مصفوفة التحويل ، ننتقل إلى نظام إحداثي آخر ، نسميه محليًا. في هذه الحالة ، يمكننا عكس الانتقال من المحلي إلى الصفر من خلال إيجاد مصفوفة التحويل العكسي .

اتضح أننا بحاجة إلى وظيفتين.

الأول هو الانتقال من صفر إلى النظام المحلي ، يقوم المتصفح بإجراء نفس التحويلات عندما نحدد خاصية تحويل css.

 img { transform: matrix(a, b, c, d, tx, ty); } 

والثاني - للعثور على الإحداثيات الأصلية للصورة ، بعد تحويل الإحداثيات بالفعل.

من الأنسب كتابة هذه الوظائف باستخدام طرق فصل منفصل.

فئة التحويل:
 class Transform { constructor(center, matrix){ this.init(center, matrix); } init(center, matrix){ if(center) this.center = Object.assign({},center); if(matrix) this.matrix = Object.assign({},matrix); } getOrigins(current){ //     let tr = {x: current.x - this.center.x, y: current.y - this.center.y}; //         const det = 1/(this.matrix.a*this.matrix.d - this.matrix.c*this.matrix.b); const x = ( this.matrix.d*(tr.x - this.matrix.tx) - this.matrix.c*(tr.y - this.matrix.ty) ) * det + this.center.x; const y = (-this.matrix.b*(tr.x - this.matrix.tx) + this.matrix.a*(tr.y - this.matrix.ty) ) * det + this.center.y; return {x, y}; } translate(current){ //     const origin = {x: current.x - this.center.x, y: current.y - this.center.y}; //        let x = this.matrix.a*origin.x + this.matrix.c*origin.y + this.matrix.tx + this.center.x; let y = this.matrix.b*origin.x + this.matrix.d*origin.y + this.matrix.ty + this.center.y; return {x, y}; } } 


طريقة الترجمة مع التعليقات التفصيلية:
 _translate(checkAlign = true){ const tr = new Transform(this.transform.center, this.matrix); // , ,  ,       const newCenter = tr.getOrigins(this.areaRect.center); this.transform.center = newCenter; //      this.transform.x = this.areaRect.center.x - newCenter.x; this.transform.y = this.areaRect.center.y - newCenter.y; //   tr.init(this.transform.center, this.matrix); //        ,      let x0y0 = tr.translate({x: 0, y: 0}); let x1y1 = tr.translate({x: this.imageRect.size.width, y: this.imageRect.size.height}); //  (  )       let result = { left: x1y1.x - x0y0.x > 0 ? x0y0.x : x1y1.x, top: x1y1.y - x0y0.y > 0 ? x0y0.y : x1y1.y, width: Math.abs(x1y1.x - x0y0.x), height: Math.abs(x1y1.y - x0y0.y) }; //       ,   "" let rightOffset = this.areaRect.size.width - (result.left + result.width); let bottomOffset = this.areaRect.size.height - (result.top + result.height); let alignedCenter; //   if(this.areaRect.size.width - result.width > 1){ //align center X alignedCenter = tr.getOrigins({x: result.left + result.width/2, y: this.areaRect.center.y}); }else{ //align left if(result.left > 0){ alignedCenter = tr.getOrigins({x: result.left + this.areaRect.center.x, y: this.areaRect.center.y}); //align right }else if(rightOffset > 0){ alignedCenter = tr.getOrigins({x: this.areaRect.center.x - rightOffset, y: this.areaRect.center.y}); } } if(alignedCenter){ this.transform.center = alignedCenter; this.transform.x = this.areaRect.center.x - alignedCenter.x; this.transform.y = this.areaRect.center.y - alignedCenter.y; tr.init(this.transform.center, this.matrix); } //   if(this.areaRect.size.height - result.height > 1){ //align center Y alignedCenter = tr.getOrigins({x: this.areaRect.center.x, y: result.top + result.height/2}); }else{ //align top if(result.top > 0){ alignedCenter = tr.getOrigins({x: this.areaRect.center.x, y: result.top + this.areaRect.center.y}); //align bottom }else if(bottomOffset > 0){ alignedCenter = tr.getOrigins({x: this.areaRect.center.x, y: this.areaRect.center.y - bottomOffset}); } } if(alignedCenter){ this.transform.center = alignedCenter; this.transform.x = this.areaRect.center.x - alignedCenter.x; this.transform.y = this.areaRect.center.y - alignedCenter.y; tr.init(this.transform.center, this.matrix); } }, 


تعمل المحاذاة على إنشاء تأثير "لصق" الصورة على حواف الاقتصاص ، وتجنب الحقول الفارغة.

مكون المعاينة


تتمثل المهمة الرئيسية لهذا المكون في عرض الصورة وتطبيق التحولات والاستجابة لحركة زر الماوس المحصور فوق الصورة. من خلال حساب الإزاحة ، نقوم بتحديث المعلمات convert.x و convert.y ، في نهاية الحركة ، نشغل الحدث الذي تم نقله ، ونخبر مكون التحرير أننا بحاجة إلى إعادة حساب موضع مركز التحويل وضبط convert.x و convert.y.

قالب مكون المعاينة:
<div ref = "area" class = "edit-zone"
@ mousedown = "onMoveStart"
@ touchstart = "onMoveStart"
mouseup = "onMoveEnd"
@ touchend = "onMoveEnd"
@ mousemove = "onMove"
@ touchmove = "onMove">
<img
v-if = "image"
المرجع = "image"
تحميل = "imageLoaded"
: src = "image"
: style = "{'convert': convertStyle، 'convert-origin': convertOrigin}">

يتم فصل وظائف المحرر بدقة عن المشروع الرئيسي ويكمن هنا .

آمل أن تكون هذه المواد مفيدة لك. شكرا لك!

Source: https://habr.com/ru/post/ar417697/


All Articles