Editor de imágenes simple en VueJS

Recientemente, tuve la oportunidad de escribir un servicio para una tienda en línea que ayudaría a hacer un pedido para imprimir mis fotos.

El servicio asumió un editor de imágenes "simple", cuya creación me gustaría compartir. Y todo porque entre la abundancia de todo tipo de complementos no encontré la funcionalidad adecuada, además, los matices de las transformaciones CSS, de repente se convirtieron en una tarea muy trivial para mí.

imagen

Las tareas principales:


  1. Posibilidad de subir imágenes desde su dispositivo, Google Drive e Instagram.
  2. Edición de imágenes: movimiento, rotación, reflexión horizontal y vertical, zoom, alineación automática de imágenes para llenar el área de recorte.

Si el tema resulta interesante, en la próxima publicación describiré en detalle la integración con Google Drive e Instagram en la parte de back-end de la aplicación, donde se utilizó la popular combinación NodeJS + Express.

Para la organización de la interfaz, elegí el maravilloso marco Vue. Solo porque me inspira después de Reacciones angulares y molestas. Creo que no tiene sentido describir la arquitectura, las rutas y otros componentes, vayamos directamente al editor.

Por cierto, la demostración del editor se puede pinchar aquí .

Necesitaremos dos componentes:

Editar : contendrá la lógica principal y los controles
Vista previa : será responsable de mostrar la imagen

Edición de plantilla de componente:
<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> 


El componente Vista previa puede desencadenar 3 eventos:

cargado - evento de carga de imagen
redimensionado - evento de cambio de tamaño de ventana
movido - evento de imagen en movimiento

Parámetros:

imagen - enlace de imagen
matriz - matriz de transformación para transformar la propiedad CSS
transformar - un objeto que describe la transformación

Para controlar mejor la posición de la imagen, img tiene un posicionamiento absoluto, y la propiedad de origen de transformación , el punto de referencia de transformación , se establece en el valor inicial "0 0", que corresponde al origen en la esquina superior izquierda de la imagen original (¡antes de la transformación!).

El principal problema que encontré es que debe asegurarse de que el punto de origen de la transformación esté siempre en el centro del área de edición, de lo contrario, durante las transformaciones, la parte seleccionada de la imagen cambiará. El uso de esta matriz de transformación ayuda a resolver este problema.

Edición de componentes


Propiedades del componente Editar:
 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 }; } }, ... } 


El componente Vista previa nos pasa los valores de imageRect y areaRect, llamando a los métodos imageLoaded y areaResized, respectivamente, los objetos tienen la estructura:

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

Los valores centrales se pueden calcular cada vez, pero es más fácil escribirlos una vez.

La propiedad de matriz calculada es los mismos coeficientes de la matriz de transformación.

La primera tarea que debe resolverse es centrar la imagen con una relación de aspecto arbitraria en el área de recorte, mientras que la imagen debe poder ajustarse completamente, las áreas sin rellenar (solo) arriba y abajo, o (solo) izquierda y derecha son aceptables. Con cualquier transformación, esta condición debe mantenerse.

En primer lugar, limitaremos los valores de zoom, para esto verificaremos la relación de aspecto, teniendo en cuenta la orientación de la imagen.

Métodos componentes
  _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; }, 


Ahora pasemos a las transformaciones. Para comenzar, describimos los reflejos, porque no cambian la región visible de la imagen.

Métodos componentes
 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; }, 


Las transformaciones de zoom, rotación y desplazamiento ya requerirán ajustes de origen de transformación.

Métodos componentes
 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 el método _translate el responsable de todas las sutilezas de las transformaciones. Se deben introducir dos sistemas de referencia. El primero, llamémoslo cero, comienza en la esquina superior izquierda de la imagen, cuando multiplicamos las coordenadas por la matriz de transformación, vamos a otro sistema de coordenadas, lo llamamos local. En este caso, podemos revertir la transición de local a cero al encontrar la matriz de transformación inversa .

Resulta que necesitamos dos funciones.

El primero es ir de cero al sistema local, el navegador realiza las mismas conversiones cuando especificamos la propiedad de transformación css.

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

El segundo: para encontrar las coordenadas originales de la imagen, que ya ha transformado las coordenadas.

Es más conveniente escribir estas funciones utilizando métodos de una clase separada.

Transformar clase:
 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}; } } 


_ Método de traducción con comentarios detallados:
 _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); } }, 


La alineación crea el efecto de "pegar" la imagen a los bordes del recorte, evitando campos vacíos.

Componente de vista previa


La tarea principal de este componente es mostrar la imagen, aplicar transformaciones y responder al movimiento del botón del mouse sobre la imagen. Calculando el desplazamiento, actualizamos los parámetros transform.x y transform.y , al final del movimiento activamos el evento movido , diciéndole al componente Editar que necesitamos volver a calcular la posición del centro de transformación y ajustar transform.x y transform.y.

Vista previa de la plantilla del componente:
<div ref = "area" class = "edit-zone"
@ mousedown = "onMoveStart"
@ touchstart = "onMoveStart"
mouseup = "onMoveEnd"
@ touchend = "onMoveEnd"
@ mousemove = "onMove"
@ touchmove = "onMove">
<img
v-if = "imagen"
ref = "imagen"
load = "imageLoaded"
: src = "imagen"
: style = "{'transform': transformStyle, 'transform-origin': transformOrigin}">

La funcionalidad del editor está claramente separada del proyecto principal y se encuentra aquí .

Espero que este material te sea útil. Gracias

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


All Articles