Einfacher Bildeditor auf VueJS

Vor kurzem hatte ich die Gelegenheit, einen Service für einen Online-Shop zu schreiben, mit dem ich eine Bestellung zum Drucken meiner Fotos aufgeben kann.

Der Dienst ging von einem „einfachen“ Bildeditor aus, dessen Erstellung ich teilen möchte. Und das alles, weil ich unter der Fülle aller Arten von Plugins keine geeignete Funktionalität gefunden habe. Außerdem wurden die Nuancen von CSS-Transformationen für mich plötzlich zu einer nicht ganz trivialen Aufgabe.

Bild

Die Hauptaufgaben:


  1. Möglichkeit zum Hochladen von Bildern von Ihrem Gerät, Google Drive und Instagram.
  2. Bildbearbeitung: Bewegen, Drehen, horizontale und vertikale Reflexion, Zoomen, automatische Bildausrichtung zum Ausfüllen des Zuschneidebereichs.

Wenn sich das Thema als interessant herausstellt, werde ich in der nächsten Veröffentlichung die Integration mit Google Drive und Instagram im Backend-Teil der Anwendung, in dem das beliebte NodeJS + Express-Bundle verwendet wurde, ausführlich beschreiben.

Für die Organisation des Frontends habe ich das wunderbare Vue-Framework gewählt. Nur weil es mich nach harter Winkel- und nerviger Reaktion inspiriert. Ich denke, es macht keinen Sinn, die Architektur, Routen und andere Komponenten zu beschreiben. Gehen wir direkt zum Editor.

Übrigens kann die Demo des Editors hier gestochert werden .

Wir werden zwei Komponenten benötigen:

Bearbeiten - enthält die Hauptlogik und Steuerelemente
Vorschau - ist für die Anzeige des Bildes verantwortlich

Bearbeiten von Komponentenvorlagen:
<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> 


Die Vorschau-Komponente kann 3 Ereignisse auslösen:

geladen - Bildladeereignis
resized - Fenstergröße ändern Ereignis
Bewegtbild - Bewegtereignis

Parameter:

Bild - Bild Link
Matrix - Transformationsmatrix für die Transformation der CSS-Eigenschaft
transform - ein Objekt, das die Transformation beschreibt

Um die Position des Bildes besser steuern zu können, hat img eine absolute Positionierung, und die Eigenschaft transform-origin , der Transformationsreferenzpunkt , wird auf den Anfangswert „0 0“ gesetzt, der dem Ursprung in der oberen linken Ecke des Originalbilds (vor der Transformation!) Entspricht.

Das Hauptproblem, auf das ich gestoßen bin, besteht darin, dass Sie sicherstellen müssen, dass sich der Transformationsursprungspunkt immer in der Mitte des Bearbeitungsbereichs befindet. Andernfalls verschiebt sich während der Transformationen der ausgewählte Teil des Bildes. Die Verwendung dieser Transformationsmatrix hilft, dieses Problem zu lösen.

Komponentenbearbeitung


Eigenschaften der Edit-Komponente:
 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 }; } }, ... } 


Die Werte von imageRect und areaRect werden von der Preview-Komponente an uns übergeben. Dabei werden die Methoden imageLoaded und areaResized aufgerufen. Objekte haben die folgende Struktur:

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

Die Mittelwerte könnten jedes Mal berechnet werden, es ist jedoch einfacher, sie einmal aufzuschreiben.

Die berechnete Matrixeigenschaft entspricht den Koeffizienten der Transformationsmatrix.

Die erste Aufgabe, die gelöst werden muss, besteht darin, das Bild mit einem beliebigen Seitenverhältnis im Zuschneidebereich zu zentrieren, während das Bild vollständig passen sollte. Ungefüllte Bereiche (nur) oben und unten oder (nur) links und rechts sind akzeptabel. Bei allen Transformationen muss dieser Zustand beibehalten werden.

Zunächst begrenzen wir die Werte für das Zoomen. Dazu überprüfen wir das Seitenverhältnis unter Berücksichtigung der Ausrichtung des Bildes.

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


Kommen wir nun zu den Transformationen. Zunächst beschreiben wir die Reflexionen, da sie den sichtbaren Bereich des Bildes nicht verschieben.

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


Transformationen von Zoomen, Drehen und Verschieben erfordern bereits Anpassungen des Transformationsursprungs.

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


Es ist die _translate- Methode , die für alle Feinheiten von Transformationen verantwortlich ist. Es müssen zwei Referenzsysteme eingeführt werden. Das erste, nennen wir es Null, beginnt in der oberen linken Ecke des Bildes. Wenn wir die Koordinaten mit der Transformationsmatrix multiplizieren, gehen wir zu einem anderen Koordinatensystem und nennen es lokal. In diesem Fall können wir den Übergang von lokal zu Null umkehren, indem wir die inverse Transformationsmatrix finden.

Es stellt sich heraus, dass wir zwei Funktionen benötigen.

Die erste besteht darin, von Null auf das lokale System zu wechseln. Der Browser führt dieselben Konvertierungen durch, wenn wir die CSS-Transformationseigenschaft angeben.

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

Die zweite - um die ursprünglichen Koordinaten des Bildes zu finden, nachdem bereits Koordinaten transformiert wurden.

Es ist am bequemsten, diese Funktionen mit Methoden einer separaten Klasse zu schreiben.

Klasse transformieren:
 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}; } } 


_Übersetzungsmethode mit detaillierten Kommentaren:
 _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); } }, 


Durch die Ausrichtung wird das Bild an den Rändern des Zuschnitts „aufgeklebt“, wodurch leere Felder vermieden werden.

Vorschau-Komponente


Die Hauptaufgabe dieser Komponente besteht darin, das Bild anzuzeigen, Transformationen anzuwenden und auf die Bewegung der Maustaste über dem Bild zu reagieren. Bei der Berechnung des Offsets aktualisieren wir die Parameter transform.x und transform.y . Am Ende der Bewegung lösen wir das verschobene Ereignis aus und teilen der Edit-Komponente mit, dass wir die Position des Transformationszentrums neu berechnen und transform.x und transform.y anpassen müssen.

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

Die Funktionalität des Editors ist sauber vom Hauptprojekt getrennt und liegt hier .

Ich hoffe, dieses Material wird für Sie nützlich sein. Vielen Dank!

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


All Articles