Editor de imagens simples no VueJS

Recentemente, tive a oportunidade de escrever um serviço para uma loja online que ajudaria a fazer um pedido para imprimir minhas fotos.

O serviço assumiu um editor de imagens "simples", cuja criação eu gostaria de compartilhar. E tudo porque, entre a abundância de todos os tipos de plugins, não encontrei funcionalidade adequada; além disso, as nuances das transformações de CSS, de repente se tornaram uma tarefa não trivial para mim.

imagem

As principais tarefas:


  1. Capacidade de fazer upload de imagens do seu dispositivo, Google Drive e Instagram.
  2. Edição de imagem: movimento, rotação, reflexão horizontal e vertical, zoom, alinhamento automático da imagem para preencher a área de corte.

Se o tópico for interessante, na próxima publicação, descreverei em detalhes a integração com o Google Drive e o Instagram na parte de back-end do aplicativo, onde o popular pacote NodeJS + Express foi usado.

Para a organização do front-end, escolhi o maravilhoso framework Vue. Só porque me inspira após um React angular e irritante. Eu acho que não faz sentido descrever a arquitetura, rotas e outros componentes, vamos direto ao editor.

A propósito, a demonstração do editor pode ser cutucada aqui .

Vamos precisar de dois componentes:

Editar - conterá a lógica e os controles principais
Visualizar - será responsável por exibir a imagem

Edição de modelo 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> 


O componente Visualizar pode disparar 3 eventos:

carregado - evento de carregamento de imagem
redimensionado - evento de redimensionamento de janela
moving - evento em movimento da imagem

Parâmetros:

imagem - link da imagem
matrix - matriz de transformação para a propriedade CSS de transformação
transformar - um objeto que descreve a transformação

Para controlar melhor a posição da imagem, img possui posicionamento absoluto, e a propriedade transform-origin , o ponto de referência da transformação , é definida como o valor inicial "0 0", que corresponde à origem no canto superior esquerdo da imagem original (antes da transformação!).

O principal problema que encontrei é que você precisa garantir que o ponto de origem da transformação esteja sempre no centro da área de edição; caso contrário, durante as transformações, a parte selecionada da imagem será alterada. O uso dessa matriz de transformação ajuda a resolver esse problema.

Edição de componentes


Propriedades do 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 }; } }, ... } 


Os valores de imageRect e areaRect são transmitidos a nós pelo componente Preview, chamando os métodos imageLoaded e areaResized, respectivamente, os objetos têm a estrutura:

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

Os valores centrais podem ser calculados a cada vez, mas é mais fácil anotá-los uma vez.

A propriedade da matriz calculada é os mesmos coeficientes da matriz de transformação.

A primeira tarefa que precisa ser resolvida é centralizar a imagem com uma proporção arbitrária na área de corte, enquanto a imagem deve caber completamente, áreas não preenchidas (somente) acima e abaixo ou (somente) esquerda e direita são aceitáveis. Com quaisquer transformações, essa condição deve ser mantida.

Primeiramente, limitaremos os valores para o zoom, para isso verificaremos a proporção, levando em consideração a orientação da imagem.

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


Agora vamos para as transformações. Para começar, descrevemos as reflexões, porque elas não mudam a região visível da imagem.

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


As transformações de zoom, rotação e deslocamento já exigirão ajustes na origem da transformação.

Métodos de 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(); }, 


É o método _translate que é responsável por todas as sutilezas das transformações. Dois sistemas de referência devem ser introduzidos. O primeiro, vamos chamá-lo de zero, começa no canto superior esquerdo da imagem, quando multiplicamos as coordenadas pela matriz de transformação, vamos para outro sistema de coordenadas, local. Nesse caso, podemos reverter a transição do local para o zero, localizando a matriz de transformação inversa .

Acontece que precisamos de duas funções.

O primeiro é ir do zero ao sistema local, o navegador executa as mesmas conversões quando especificamos a propriedade css transform.

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

O segundo - para encontrar as coordenadas originais da imagem, já tendo transformadas as coordenadas.

É mais conveniente escrever essas funções usando métodos de uma classe separada.

Classe de transformação:
 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 método com comentários detalhados:
 _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); } }, 


O alinhamento cria o efeito de "colar" a imagem nas bordas do recorte, evitando campos vazios.

Componente de visualização


A principal tarefa desse componente é exibir a imagem, aplicar transformações e responder ao movimento do botão do mouse sobre a imagem. Calculando o deslocamento, atualizamos os parâmetros transform.x e transform.y , no final do movimento, acionamos o evento movido , informando ao componente Edit que precisamos recalcular a posição do centro de transformação e ajustar transform.x e transform.y.

Modelo de componente de visualização:
<div ref = "area" class = "zona de edição"
@ mousedown = "onMoveStart"
@ touchstart = "onMoveStart"
mouseup = "onMoveEnd"
@ touchend = "onMoveEnd"
@ mousemove = "onMove"
@ touchmove = "onMove">
<img
v-if = "imagem"
ref = "imagem"
load = "imageLoaded"
: src = "imagem"
: style = "{'transform': transformStyle, 'transform-origin': transformOrigin}">

A funcionalidade do editor é bem separada do projeto principal e está aqui .

Espero que este material seja útil para você. Obrigada

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


All Articles