Editor gambar sederhana di VueJS

Baru-baru ini, saya berkesempatan menulis layanan untuk toko online yang akan membantu memesan untuk mencetak foto saya.

Layanan diasumsikan sebagai editor gambar "sederhana", yang ciptaannya ingin saya bagikan. Dan semua karena di antara banyaknya jenis plugin saya tidak menemukan fungsi yang sesuai, selain itu, nuansa transformasi CSS, tiba-tiba menjadi tugas yang sangat sepele bagi saya.

gambar

Tugas utama:


  1. Kemampuan untuk mengunggah gambar dari perangkat Anda, Google Drive dan Instagram.
  2. Pengeditan gambar: memindahkan, memutar, refleksi horizontal dan vertikal, zoom, penyelarasan gambar otomatis untuk mengisi area krop.

Jika topiknya ternyata menarik, dalam publikasi berikutnya saya akan menjelaskan secara rinci integrasi dengan Google Drive dan Instagram di bagian belakang aplikasi, di mana bundel NodeJS + Express yang populer digunakan.

Untuk organisasi frontend, saya memilih kerangka kerja Vue yang indah. Hanya karena itu mengilhami saya setelah React keras dan menjengkelkan. Saya pikir tidak masuk akal untuk menggambarkan arsitektur, rute, dan komponen lainnya, mari kita langsung ke editor.

Ngomong-ngomong, demo editor bisa dicolek di sini .

Kami membutuhkan dua komponen:

Sunting - akan berisi logika dan kontrol utama
Pratinjau - akan bertanggung jawab untuk menampilkan gambar

Edit Template Komponen:
<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> 


Komponen Pratinjau dapat memicu 3 peristiwa:

dimuat - acara pemuatan gambar
diubah ukurannya - acara pengubahan ukuran jendela
dipindahkan - acara pemindahan gambar

Parameter:

gambar - tautan gambar
matrix - transformation matrix untuk mengubah properti CSS
transform - sebuah objek yang menggambarkan transformasi

Untuk lebih mengontrol posisi gambar, img memiliki posisi absolut, dan properti transform-origin , titik referensi transformasi , diatur ke nilai awal "0 0", yang sesuai dengan asal di sudut kiri atas gambar asli (sebelum transformasi!).

Masalah utama yang saya temui adalah bahwa Anda perlu memastikan bahwa titik asal-transformasinya selalu berada di tengah area pengeditan, jika tidak, selama transformasi, bagian gambar yang dipilih akan bergeser. Menggunakan matriks transformasi ini membantu menyelesaikan masalah ini.

Edit Komponen


Properti komponen Edit:
 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 }; } }, ... } 


Nilai-nilai dari imageRect dan areaRect dikirimkan kepada kami oleh komponen Preview, memanggil metode imageLoaded dan areaResized, masing-masing, objek memiliki struktur:

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

Nilai pusat dapat dihitung setiap kali, tetapi lebih mudah untuk menuliskannya satu kali.

Properti matriks yang dihitung adalah koefisien yang sama dari matriks transformasi.

Tugas pertama yang perlu dipecahkan adalah memusatkan gambar dengan rasio aspek sewenang-wenang di area krop, sedangkan gambar harus dapat sepenuhnya pas, area yang tidak terisi (hanya) di atas dan di bawah, atau (hanya) kiri dan kanan dapat diterima. Dengan transformasi apa pun, kondisi ini harus dipertahankan.

Pertama, kami akan membatasi nilai untuk pembesaran, untuk ini kami akan memeriksa rasio aspek, dengan mempertimbangkan orientasi gambar.

Metode Komponen:
  _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; }, 


Sekarang mari kita beralih ke transformasi. Untuk mulai dengan, kami menggambarkan refleksi, karena mereka tidak menggeser wilayah gambar yang terlihat.

Metode Komponen:
 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; }, 


Transformasi pembesaran, rotasi, dan perpindahan sudah membutuhkan penyesuaian transformasi-asal.

Metode Komponen:
 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(); }, 


Ini adalah metode _translate yang bertanggung jawab atas semua seluk beluk transformasi. Dua sistem referensi harus diperkenalkan. Yang pertama, sebut saja nol, dimulai di sudut kiri atas gambar, ketika kita mengalikan koordinat dengan matriks transformasi, kita pergi ke sistem koordinat lain, menyebutnya lokal. Dalam hal ini, kita dapat membalikkan transisi dari lokal ke nol dengan menemukan matriks transformasi terbalik .

Ternyata kita membutuhkan dua fungsi.

Yang pertama adalah beralih dari nol ke sistem lokal, browser melakukan konversi yang sama ketika kita menentukan properti css transform.

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

Yang kedua - untuk menemukan koordinat asli dari gambar, setelah mengubah koordinat.

Sangat mudah untuk menulis fungsi-fungsi ini menggunakan metode kelas yang terpisah.

Transform Kelas:
 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}; } } 


Metode _Translate dengan komentar terperinci:
 _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); } }, 


Alignment menciptakan efek "menempel" gambar ke tepi tanaman, menghindari bidang kosong.

Komponen pratinjau


Tugas utama komponen ini adalah menampilkan gambar, menerapkan transformasi, dan merespons gerakan tombol mouse yang diapit di atas gambar. Menghitung offset, kami memperbarui parameter transform.x dan transform.y , pada akhir gerakan kami memicu acara dipindahkan , memberi tahu komponen Edit bahwa kami perlu menghitung ulang posisi pusat transformasi dan menyesuaikan transform.x dan transform.y.

Pratinjau templat komponen:
<div ref = "area" class = "edit-zone"
@ mousedown = "onMoveStart"
@ touchstart = "onMoveStart"
mouseup = "onMoveEnd"
@ touchend = "onMoveEnd"
@ mousemove = "onMove"
@ touchmove = "onMove">
<img
v-if = "image"
ref = "image"
load = "imageLoaded"
: src = "image"
: style = "{'transform': transformStyle, 'transform-origin': transformOrigin}">

Fungsionalitas editor dipisahkan dengan rapi dari proyek utama dan terletak di sini .

Semoga materi ini bermanfaat bagi Anda. Terima kasih

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


All Articles