Heim  >  Artikel  >  Web-Frontend  >  So implementieren Sie die Komponente zum Zuschneiden von Bildern mit dem mobilen Vue-Terminal

So implementieren Sie die Komponente zum Zuschneiden von Bildern mit dem mobilen Vue-Terminal

亚连
亚连Original
2018-06-23 18:06:432820Durchsuche

Dieser Artikel stellt Ihnen die Funktion zum Zuschneiden von Bildern für mobile Endgeräte vor, die auf Vue basiert. Hier sind einige meiner Implementierungsideen . Weiter

In einem aktuellen Projekt möchte ich eine Kennzeichenerkennungsfunktion bauen. Ursprünglich dachte ich, es sei sehr einfach, das Bild einfach in den Hintergrund zu werfen, aber nach dem Test betrug die Erkennungsrate nur 20-40 %. Daher empfiehlt das Produkt, dass Sie nach dem Aufnehmen eines Bildes das Bild ziehen und zoomen, dann den Nummernschildteil zuschneiden und in den Hintergrund hochladen können, um die Erkennungsrate zu verbessern. Zu Beginn habe ich Baidu überprüft, um zu sehen, ob es fertige Komponenten gibt, aber ich konnte keine passende finden. Zum Glück war ich nicht sehr besorgt über diese Funktion, also habe ich sie am Wochenende zu Hause studiert.

Demo-Adresse: https://vivialex.github.io/demo/imageClipper/index.html

Download-Adresse: https://github.com/vivialex/vue-imageClipper

Da das mobile Endgerät Vue verwendet, habe ich es als Vue-Komponente geschrieben. Lassen Sie uns über einige meiner Implementierungsideen sprechen (ich habe nur begrenzte Fähigkeiten, bitte haben Sie Verständnis. Außerdem ist der angezeigte Code für eine bestimmte Funktion nicht unbedingt vollständig .code), schauen Sie sich zuerst den Effekt an:

 1. Initialisierungsparameter der Komponente

1. Bildbild (URL oder Base64-Daten-URL)

2. Screenshot-Breite clipperImgWidth

3. Screenshot-Höhe clipperImgHeight

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

2. , Layout

In Richtung der Z-Achse betrachtet besteht es hauptsächlich aus 4 Schichten. Die erste Ebene ist eine Leinwand (cCanvas genannt), die den gesamten Container einnimmt; die zweite Ebene ist eine transparente Maskenebene (das weiße Feld im Beispielbild), die eine Leinwand mit gleichen Flächengrößen enthält (genannt pCanvas); die vierte Ebene ist eine transparente Ebenen-Gestenmaske, die zum Binden von Touchstart-, Touchmove- und Touchend-Ereignissen verwendet wird. Auf beiden Leinwänden wird dasselbe Bild geladen, die Startkoordinaten sind jedoch unterschiedlich. Warum brauchen wir zwei Leinwände? Weil ich den Effekt erzeugen möchte, dass, wenn der Finger den Bildschirm verlässt, ein Teil der Oberfläche außerhalb des Zuschneidebereichs einen Maskenebeneneffekt hat, der den Inhalt des Zuschneidebereichs hervorheben kann.

<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. Leinwand initialisieren

Mit der Leinwand gezeichnete Bilder werden auf dem HDPI-Display verschwommen angezeigt. Die spezifischen Gründe werden hier nicht analysiert. Den Abschnitt „Aussteigen“ finden Sie hier. Was ich hier mache, ist, die Breite und Höhe der Leinwand mit dem devicePixelRatio ihrer CSS-Breite/Höhe zu multiplizieren, und die beim Aufruf der Canvas-API übergebenen Parameter müssen mit window.devicePixelRatio multipliziert werden. Zeichnen Sie abschließend die x- und y-Differenz zwischen den beiden Canvas-Koordinatenursprüngen (originXDiff und originYDiff) auf. Wie folgt

_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;
}

  Bilder laden

Erstellen Sie zunächst ein Bildobjekt und hören Sie sich das Onload-Ereignis an ( weil das geladene Bild möglicherweise domänenübergreifend ist. Setzen Sie daher das Attribut „crossOrigin“ auf „Anonymous“ und legen Sie dann den Antwortheader „Access-Control-Allow-Origin“ auf dem Server fest. Sind Breite und Höhe des geladenen Bildes größer als die Breite und Höhe des Containers, müssen diese reduziert werden. Abschließend vertikal und horizontal zentrierte Anzeige () (Hinweis hier ist, dass der Breiten- und Höhenwert vor dem Zeichnen des Bildes gespeichert wird, da die zukünftige Skalierung des Bildes auf diesem Wert basiert und dann mit dem Zoomfaktor multipliziert wird. Hier wird imgStartWidth verwendet , imgStartHeight) wie folgt

_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. Zeichnen Sie ein Bild

Das folgende _drawImage hat vier Parameter, die die x- und y-Koordinaten von sind das Bild, das cCanvas entspricht, und die aktuelle Breite und Höhe w des Bildes. Die Funktion löscht zunächst den Inhalt der beiden Leinwände, indem sie die Breite und Höhe der Leinwände zurücksetzt. Aktualisieren Sie dann den entsprechenden Wert in der Komponenteninstanz und rufen Sie schließlich das drawImage der beiden Leinwände auf, um das Bild zu zeichnen. Für pCanvas sind die Koordinatenwerte des gezeichneten Bildes x und y abzüglich der entsprechenden originXDiff und originYDiff (tatsächlich entspricht dies dem Umschalten der Koordinatensystemanzeige, sodass Sie nur die x- und y-Differenz zwischen den Ursprüngen subtrahieren müssen zwei Koordinatensysteme) ). Schauen Sie sich den Code an

_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. Bewegte Bilder

Zuerst binden Sie touchstart, touchmove,. Touchend zu Gestenmaske Ereignisse, der Inhalt dieser drei Ereignisse wird unten vorgestellt

Definieren Sie zunächst die vier Variablen scx, scy (die Startkoordinaten des Fingers), iX, iY (die aktuellen Koordinaten des Bildes, relativ). zu cCanvas).

1. touchstart

Die Methode ist sehr einfach, holen Sie sich einfach pageX und pageY von touches[0], um scx und scy zu aktualisieren und iX und iY zu aktualisieren

2. touchmove

 Rufen Sie die Seite Aktualisieren Sie das Bild.

Werfen wir einen Blick auf den 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中如何实现后台数据发送

Das obige ist der detaillierte Inhalt vonSo implementieren Sie die Komponente zum Zuschneiden von Bildern mit dem mobilen Vue-Terminal. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn