Maison  >  Article  >  interface Web  >  Comment implémenter un composant de recadrage d'image à l'aide du terminal mobile Vue

Comment implémenter un composant de recadrage d'image à l'aide du terminal mobile Vue

亚连
亚连original
2018-06-23 18:06:432820parcourir

Cet article vous présente la fonction du composant de recadrage d'image du terminal mobile basée sur Vue. Parce que le terminal mobile utilise Vue, il est écrit comme un composant Vue Ci-dessous, je parlerai de certaines de mes idées d'implémentation. référez-vous à lui. Suivant

Un projet récent consiste à créer une fonction de reconnaissance de plaque d'immatriculation. Au départ, je pensais que c'était très simple, il suffit de jeter l'image en arrière-plan, mais après tests, le taux de reconnaissance n'était que de 20 à 40 %. Par conséquent, le produit recommande qu'après avoir pris une photo, vous puissiez faire glisser et zoomer sur l'image, puis recadrer la partie de la plaque d'immatriculation et la télécharger en arrière-plan pour améliorer le taux de reconnaissance. Au début, j'ai vérifié Baidu pour voir s'il y avait des composants prêts à l'emploi, mais je n'ai pas pu en trouver un qui me convienne. Heureusement, je n'étais pas très inquiet à propos de cette fonction, alors je l'ai étudiée à la maison le week-end.

Adresse de démonstration : https://vivialex.github.io/demo/imageClipper/index.html

Adresse de téléchargement : https://github.com/vivialex/vue-imageClipper

Étant donné que le terminal mobile utilise Vue, je l'ai écrit en tant que composant Vue Parlons de certaines de mes idées d'implémentation (j'ai des compétences limitées, veuillez comprendre. De plus, le code affiché n'est pas nécessairement complet pour une certaine fonction. . code), jetez d'abord un œil à l'effet :

 1. Paramètres d'initialisation du composant

1. Image img (url ou data-url base64)

2. Largeur de la capture d'écran clipperImgWidth

3. Hauteur de la capture d'écran clipperImgHeight

props: {
  img: String, //url或dataUrl
  clipperImgWidth: {
    type: Number,
    default: 500
  },
  clipperImgHeight: {
    type: Number,
    default: 200
  }
}

2. Disposition

En regardant dans la direction de l'axe Z, elle est principalement composée de 4 couches. Le premier calque est un canevas (appelé cCanvas) qui occupe tout le conteneur ; le deuxième calque est un calque de masque transparent, le troisième calque est la zone recadrée (la zone blanche dans l'image d'exemple), qui contient un canevas de tailles égales ; (appelé pCanvas) ; le quatrième calque est un masque de geste de calque transparent, utilisé pour lier les événements touchstart, touchmove, touchend. Les deux toiles chargeront la même image, mais les coordonnées de départ sont différentes. Pourquoi avons-nous besoin de deux toiles ? Parce que je veux créer l'effet que lorsque le doigt quitte l'écran, une partie de la surface en dehors de la zone de recadrage aura un effet de calque de masque, qui peut mettre en évidence le contenu de la zone de recadrage.

<p class="cut-container" ref="cut">
  <canvas ref="canvas"></canvas>
  <!-- 裁剪部分 -->
  <p class="cut-part">
    <p class="pCanvas-container">
      <canvas ref="pCanvas"></canvas>
    </p>
  </p>
  <!-- 底部操作栏 -->
  <p class="action-bar">
    <button class="btn-cancel" @click="_cancel">取消</button>
    <button class="btn-ok" @click="_cut">确认</button>
  </p>
  <!-- 背景遮罩 -->
  <p class="mask" :class="{opacity: maskShow}"></p>
  <!-- 手势操作层 -->
  <p class="gesture-mask" ref="gesture"></p>
</p>

  3. Initialiser la toile

L'image dessinée par la toile apparaîtra floue sur l'écran HDPI. Les raisons spécifiques ne le seront pas. analysé ici. Vous pouvez vous y référer ici. Ce que je fais ici, c'est faire en sorte que la largeur et la hauteur du canevas soient multipliées par le devicePixelRatio de sa largeur/hauteur CSS, et les paramètres transmis lors de l'appel de l'API du canevas doivent être multipliés par window.devicePixelRatio. Enfin, enregistrez la différence x, y entre les deux origines des coordonnées du canevas (originXDiff et originYDiff). Comme suit

_ratio(size) {
  return parseInt(window.devicePixelRatio * size);
},
_initCanvas() {
  let $canvas = this.$refs.canvas,
    $pCanvas = this.$refs.pCanvas,
    clipperClientRect = this.$refs.clipper.getBoundingClientRect(),
    clipperWidth = parseInt(this.clipperImgWidth / window.devicePixelRatio),
    clipperHeight = parseInt(this.clipperImgHeight / window.devicePixelRatio);

  this.ctx = $canvas.getContext(&#39;2d&#39;);
  this.pCtx = $pCanvas.getContext(&#39;2d&#39;);

  //判断clipperWidth与clipperHeight有没有超过容器值
  if (clipperWidth < 0 || clipperWidth > clipperClientRect.width) {
    clipperWidth = 250
  }

  if (clipperHeight < 0 || clipperHeight > clipperClientRect.height) {
    clipperHeight = 100
  }

  //因为canvas在手机上会被放大,因此里面的内容会模糊,这里根据手机的devicePixelRatio来放大canvas,然后再通过设置css来收缩,因此关于canvas的所有值或坐标都要乘以devicePixelRatio
  $canvas.style.width = clipperClientRect.width + &#39;px&#39;;
  $canvas.style.height = clipperClientRect.height + &#39;px&#39;;
  $canvas.width = this._ratio(clipperClientRect.width);
  $canvas.height = this._ratio(clipperClientRect.height);

  $pCanvas.style.width = clipperWidth + &#39;px&#39;;
  $pCanvas.style.height = clipperHeight + &#39;px&#39;;
  $pCanvas.width = this._ratio(clipperWidth);
  $pCanvas.height = this._ratio(clipperHeight);

  //计算两个canvas原点的x y差值
  let cClientRect = $canvas.getBoundingClientRect(),
    pClientRect = $pCanvas.getBoundingClientRect();

  this.originXDiff = pClientRect.left - cClientRect.left;
  this.originYDiff = pClientRect.top - cClientRect.top;
  this.cWidth = cClientRect.width;
  this.cHeight = cClientRect.height;
}

IV. Chargement d'images

Le chargement d'images est relativement simple. Tout d'abord, créez un objet Image et écoutez le chargement. événement (car l'image chargée peut être inter-domaines, définissez donc son attribut crossOrigin sur Anonyme, puis définissez l'en-tête de réponse Access-Control-Allow-Origin sur le serveur). Si la largeur et la hauteur de l'image chargée sont supérieures à la largeur et à la hauteur du conteneur, elles doivent être réduites. Enfin, l'affichage de centrage vertical et horizontal () (notez ici qu'il faut enregistrer la valeur de largeur et de hauteur avant que l'image ne soit dessinée, car la future mise à l'échelle de l'image sera basée sur cette valeur puis multipliée par le facteur de zoom, prenez ici imgStartWidth, imgStartHeight) comme suit

_loadImg() {
  if (this.imgLoading || this.loadImgQueue.length === 0) {
    return;
  }
  let img = this.loadImgQueue.shift();
  if (!img) {
    return;
  }
  let $img = new Image(),
    onLoad = e => {
      $img.removeEventListener(&#39;load&#39;, onLoad, false);
      this.$img = $img;
      this.imgLoaded = true;
      this.imgLoading = false;
      this._initImg($img.width, $img.height);
      this.$emit(&#39;loadSuccess&#39;, e);
      this.$emit(&#39;loadComplete&#39;, e);
      this._loadImg();
    },
    onError = e => {
      $img.removeEventListener(&#39;error&#39;, onError, false);
      this.$img = $img = null;
      this.imgLoading = false;
      this.$emit(&#39;loadError&#39;, e);
      this.$emit(&#39;loadComplete&#39;, e);
      this._loadImg();
    };
  this.$emit(&#39;beforeLoad&#39;);
  this.imgLoading = true;
  this.imgLoaded = false;
  $img.src = this.img;
  $img.crossOrigin = &#39;Anonymous&#39;; //因为canvas toDataUrl不能操作未经允许的跨域图片,这需要服务器设置Access-Control-Allow-Origin头
  $img.addEventListener(&#39;load&#39;, onLoad, false);
  $img.addEventListener(&#39;error&#39;, onError, false);
}
_initImg(w, h) {
  let eW = null,
    eH = null,
    maxW = this.cWidth,
    maxH = this.cHeight - this.actionBarHeight;
  //如果图片的宽高都少于容器的宽高,则不做处理
  if (w <= maxW && h <= maxH) {
    eW = w;
    eH = h;
  } else if (w > maxW && h <= maxH) {
    eW = maxW;
    eH = parseInt(h / w * maxW);
  } else if (w <= maxW && h > maxH) {
    eW = parseInt(w / h * maxH);
    eH = maxH;
  } else {
    //判断是横图还是竖图
    if (h > w) {
      eW = parseInt(w / h * maxH);
      eH = maxH;
    } else {
      eW = maxW;
      eH = parseInt(h / w * maxW);
    }
  }
  if (eW <= maxW && eH <= maxH) {
    //记录其初始化的宽高,日后的缩放功能以此值为基础
    this.imgStartWidth = eW;
    this.imgStartHeight = eH;
    this._drawImage((maxW - eW) / 2, (maxH - eH) / 2, eW, eH);
  } else {
    this._initImg(eW, eH);
  }
}

  5. Dessinez une image

Le _drawImage suivant a quatre paramètres, qui sont le x, Coordonnées y de l'image correspondant à cCanvas et la largeur actuelle de l'image High w, h. La fonction effacera d'abord le contenu des deux toiles en réinitialisant la largeur et la hauteur de la toile. Mettez ensuite à jour la valeur correspondante dans l'instance du composant, et enfin appelez le drawImage des deux canevas pour dessiner l'image. Pour pCanvas, les valeurs de coordonnées de l'image dessinée sont x et y moins les originXDiff et originYDiff correspondants (en fait, cela équivaut à changer l'affichage du système de coordonnées, il vous suffit donc de soustraire la différence x et y entre les origines du deux systèmes de coordonnées) ). Jetez un œil au code

_drawImage(x, y, w, h) {
  this._clearCanvas();
  this.imgX = parseInt(x);
  this.imgY = parseInt(y);
  this.imgCurrentWidth = parseInt(w);
  this.imgCurrentHeight = parseInt(h);
  //更新canvas
  this.ctx.drawImage(this.$img, this._ratio(x), this._ratio(y), this._ratio(w), this._ratio(h));
  //更新pCanvas,只需要减去两个canvas坐标原点对应的差值即可
  this.pCtx.drawImage(this.$img, this._ratio(x - this.originXDiff), this._ratio(y - this.originYDiff), this._ratio(w), this._ratio(h));
},
_clearCanvas() {
  let $canvas = this.$refs.canvas,
    $pCanvas = this.$refs.pCanvas;
  $canvas.width = $canvas.width;
  $canvas.height = $canvas.height;
  $pCanvas.width = $pCanvas.width;
  $pCanvas.height = $pCanvas.height;
}

  6. Images animées

La mise en œuvre des images animées est très simple. Commencez par lier touchstart et. touchmove to geste-mask , événement touchend, le contenu de ces trois événements est présenté ci-dessous

Définissez d'abord quatre variables scx, scy (les coordonnées de départ du doigt), iX, iY (les coordonnées actuelles de l'image , par rapport à cCanvas).

1. touchstart

La méthode est très simple, il suffit d'obtenir la pageX et la pageY de touches[0] pour mettre à jour scx et scy et mettre à jour iX et iY

2. touchmove

 Récupérez la pageX de touches[0], déclarez la variable f1x pour la stocker, la coordonnée x après le mouvement est égale à iX + f1x - scx, la coordonnée y est la même, et enfin appelez _drawImage à mettre à jour l'image.

Jetons un coup d'œil au code

_initEvent() {
  let $gesture = this.$refs.gesture,
    scx = 0,
    scy = 0;
  let iX = this.imgX,
    iY = this.imgY;
  $gesture.addEventListener(&#39;touchstart&#39;, e => {
    if (!this.imgLoaded) {
      return;
    }
    let finger = e.touches[0];
      scx = finger.pageX;
      scy = finger.pageY;
      iX = this.imgX;
      iY = this.imgY;  
  }, false);
  $gesture.addEventListener(&#39;touchmove&#39;, e => {
    e.preventDefault();
    if (!this.imgLoaded) {
      return;
    }
    let f1x = e.touches[0].pageX,
      f1y = e.touches[0].pageY;
      this._drawImage(iX + f1x - scx, iY + f1y - scy, this.imgCurrentWidth, this.imgCurrentHeight);
  }, false);
}

   七、缩放图片(这里不作特别说明的坐标都是相对于cCanvas坐标系)

  绘制缩放后的图片无非需要4个参数,缩放后图片左上角的坐标以及宽高。求宽高相对好办,宽高等于imgStartWidth * 缩放比率与imgstartHeight * 缩放倍率(imgStartWidth ,imgstartHeight 上文第四节有提到)。接下来就是求缩放倍率的问题了,首先在touchstart事件上求取两手指间的距离d1;然后在touchmove事件上继续求取两手指间的距离d2,当前缩放倍率= 初始缩放倍率 + (d2-d1) / 步长(例如每60px算0.1),touchend事件上让初始缩放倍率=当前缩放倍率。

  至于如何求取缩放后图片左上角的坐标值,在草稿纸上画来画去,画了很久......终于有点眉目。首先要找到一个缩放中心(这里做法是取双指的中点坐标,但是这个坐标必须要位于图片上,如果不在图片上,则取图片上离该中点坐标最近的点),然后存在下面这个等式

  (缩放中心x坐标 - 缩放后图片左上角x坐标)/ 缩放后图片的宽度 = (缩放中心x坐标 - 缩放前图片左上角x坐标)/ 缩放前图片的宽度;(y坐标同理)

  接下来看看下面这个例子(在visio找了很久都没有画坐标系的功能,所以只能手工画了)

  

  绿色框是一张10*5的图片,蓝色框是宽高放大两倍后的图片20*10,根据上面的公式推算的x2 = sx - w2(sx - x1) / w1,y2 = sy - h2(sy - y1) / h1。

  坚持...继续看看代码吧

_initEvent() {
  let $gesture = this.$refs.gesture,
    cClientRect = this.$refs.canvas.getBoundingClientRect(),
    scx = 0, //对于单手操作是移动的起点坐标,对于缩放是图片距离两手指的中点最近的图标。
    scy = 0,
    fingers = {}; //记录当前有多少只手指在触控屏幕
  //one finger
  let iX = this.imgX,
    iY = this.imgY;
  //two finger
  let figureDistance = 0,
    pinchScale = this.imgScale;
  $gesture.addEventListener(&#39;touchstart&#39;, e => {
    if (!this.imgLoaded) {
      return;
    }
    if (e.touches.length === 1) {
      let finger = e.touches[0];
      scx = finger.pageX;
      scy = finger.pageY;
      iX = this.imgX;
      iY = this.imgY;
      fingers[finger.identifier] = finger;
    } else if (e.touches.length === 2) {
      let finger1 = e.touches[0],
        finger2 = e.touches[1],
        f1x = finger1.pageX - cClientRect.left,
        f1y = finger1.pageY - cClientRect.top,
        f2x = finger2.pageX - cClientRect.left,
        f2y = finger2.pageY - cClientRect.top;
      scx = parseInt((f1x + f2x) / 2);
      scy = parseInt((f1y + f2y) / 2);
      figureDistance = this._pointDistance(f1x, f1y, f2x, f2y);
      fingers[finger1.identifier] = finger1;
      fingers[finger2.identifier] = finger2;
      //判断变换中点是否在图片中,如果不是则去离图片最近的点
      if (scx < this.imgX) {
        scx = this.imgX;
      }
      if (scx > this.imgX + this.imgCurrentWidth) {
        scx = this.imgX + this.imgCurrentHeight;
      }
      if (scy < this.imgY) {
        scy = this.imgY;
      }
      if (scy > this.imgY + this.imgCurrentHeight) {
        scy = this.imgY + this.imgCurrentHeight;
      }
    }
  }, false);
  $gesture.addEventListener(&#39;touchmove&#39;, e => {
    e.preventDefault();
    if (!this.imgLoaded) {
      return;
    }
    this.maskShowTimer && clearTimeout(this.maskShowTimer);
    this.maskShow = false;
    if (e.touches.length === 1) {
      let f1x = e.touches[0].pageX,
        f1y = e.touches[0].pageY;
      this._drawImage(iX + f1x - scx, iY + f1y - scy, this.imgCurrentWidth, this.imgCurrentHeight);
    } else if (e.touches.length === 2) {
      let finger1 = e.touches[0],
        finger2 = e.touches[1],
        f1x = finger1.pageX - cClientRect.left,
        f1y = finger1.pageY - cClientRect.top,
        f2x = finger2.pageX - cClientRect.left,
        f2y = finger2.pageY - cClientRect.top,
        newFigureDistance = this._pointDistance(f1x, f1y, f2x, f2y),
        scale = this.imgScale + parseFloat(((newFigureDistance - figureDistance) / this.imgScaleStep).toFixed(1));
      fingers[finger1.identifier] = finger1;
      fingers[finger2.identifier] = finger2;
      if (scale !== pinchScale) {
        //目前缩放的最小比例是1,最大是5
        if (scale < this.imgMinScale) {
          scale = this.imgMinScale;
        } else if (scale > this.imgMaxScale) {
          scale = this.imgMaxScale;
        }
        pinchScale = scale;
        this._scale(scx, scy, scale);
      }
    }
  }, false);
  $gesture.addEventListener(&#39;touchend&#39;, e => {
    if (!this.imgLoaded) {
      return;
    }
    this.imgScale = pinchScale;
    //从finger删除已经离开的手指
    let touches = Array.prototype.slice.call(e.changedTouches, 0);
    touches.forEach(item => {
      delete fingers[item.identifier];
    });
    //迭代fingers,如果存在finger则更新scx,scy,iX,iY,因为可能缩放后立即单指拖动
    let i,
      fingerArr = [];
    for(i in fingers) {
      if (fingers.hasOwnProperty(i)) {
        fingerArr.push(fingers[i]);
      }
    }
    if (fingerArr.length > 0) {
      scx = fingerArr[0].pageX;
      scy = fingerArr[0].pageY;
      iX = this.imgX;
      iY = this.imgY;
    } else {
      this.maskShowTimer = setTimeout(() => {
        this.maskShow = true;
      }, 300);
    }
    //做边界值检测
    let x = this.imgX,
      y = this.imgY,
      pClientRect = this.$refs.pCanvas.getBoundingClientRect();
    if (x > pClientRect.left + pClientRect.width) {
      x = pClientRect.left
    } else if (x + this.imgCurrentWidth < pClientRect.left) {
      x = pClientRect.left + pClientRect.width - this.imgCurrentWidth;
    }
    if (y > pClientRect.top + pClientRect.height) {
      y = pClientRect.top;
    } else if (y + this.imgCurrentHeight < pClientRect.top) {
      y = pClientRect.top + pClientRect.height - this.imgCurrentHeight;
    }
    if (this.imgX !== x || this.imgY !== y) {
      this._drawImage(x, y, this.imgCurrentWidth, this.imgCurrentHeight);
    }
  });
},
_scale(x, y, scale) {
  let newPicWidth = parseInt(this.imgStartWidth * scale),
    newPicHeight = parseInt(this.imgStartHeight * scale),
    newIX = parseInt(x - newPicWidth * (x - this.imgX) / this.imgCurrentWidth),
    newIY = parseInt(y - newPicHeight * (y - this.imgY) / this.imgCurrentHeight);
  this._drawImage(newIX, newIY, newPicWidth, newPicHeight);
},
_pointDistance(x1, y1, x2, y2) {
  return parseInt(Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)));
}

  说明一下fingers是干嘛的,是用来记录当前有多少只手指在屏幕上触摸。可能会出现这种情况,双指缩放后,其中一只手指移出显示屏,而另外一个手指在显示屏上移动。针对这种情况,要在touchend事件上根据e.changedTouches来移除fingers里已经离开显示屏的finger,如果此时fingers里只剩下一个finger,则更新scx,scy,iX,iY为移动图片做初始化准备。

  八、裁剪图片

  这里很简单,就调用pCanvas的toDataURL方法就可以了

_clipper() {
  let imgData = null;
  try {
    imgData = this.$refs.pCanvas.toDataURL();
  } catch (e) {
    console.error(&#39;请在response header加上Access-Control-Allow-Origin,否则canvas无法裁剪未经许可的跨域图片&#39;);
  }
  this.$emit(&#39;sure&#39;, imgData);
}

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

在Bootstrap框架里使用treeview如何实现动态加载数据

在Nginx中如何配置多站点vhost

在vue中如何实现跳转到之前页面

在express+mockjs中如何实现后台数据发送

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn