


Comment implémenter un composant de recadrage d'image à l'aide du terminal mobile Vue
Jun 23, 2018 pm 06:06 PMCet 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">確認(rèn)</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('2d'); this.pCtx = $pCanvas.getContext('2d'); //判斷clipperWidth與clipperHeight有沒有超過容器值 if (clipperWidth < 0 || clipperWidth > clipperClientRect.width) { clipperWidth = 250 } if (clipperHeight < 0 || clipperHeight > clipperClientRect.height) { clipperHeight = 100 } //因為canvas在手機上會被放大,因此里面的內(nèi)容會模糊,這里根據(jù)手機的devicePixelRatio來放大canvas,然后再通過設(shè)置css來收縮,因此關(guān)于canvas的所有值或坐標(biāo)都要乘以devicePixelRatio $canvas.style.width = clipperClientRect.width + 'px'; $canvas.style.height = clipperClientRect.height + 'px'; $canvas.width = this._ratio(clipperClientRect.width); $canvas.height = this._ratio(clipperClientRect.height); $pCanvas.style.width = clipperWidth + 'px'; $pCanvas.style.height = clipperHeight + 'px'; $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('load', onLoad, false); this.$img = $img; this.imgLoaded = true; this.imgLoading = false; this._initImg($img.width, $img.height); this.$emit('loadSuccess', e); this.$emit('loadComplete', e); this._loadImg(); }, onError = e => { $img.removeEventListener('error', onError, false); this.$img = $img = null; this.imgLoading = false; this.$emit('loadError', e); this.$emit('loadComplete', e); this._loadImg(); }; this.$emit('beforeLoad'); this.imgLoading = true; this.imgLoaded = false; $img.src = this.img; $img.crossOrigin = 'Anonymous'; //因為canvas toDataUrl不能操作未經(jīng)允許的跨域圖片,這需要服務(wù)器設(shè)置Access-Control-Allow-Origin頭 $img.addEventListener('load', onLoad, false); $img.addEventListener('error', 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) { //記錄其初始化的寬高,日后的縮放功能以此值為基礎(chǔ) 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坐標(biāo)原點對應(yīng)的差值即可 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('touchstart', 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('touchmove', 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); }
七、縮放圖片(這里不作特別說明的坐標(biāo)都是相對于cCanvas坐標(biāo)系)
繪制縮放后的圖片無非需要4個參數(shù),縮放后圖片左上角的坐標(biāo)以及寬高。求寬高相對好辦,寬高等于imgStartWidth * 縮放比率與imgstartHeight * 縮放倍率(imgStartWidth ,imgstartHeight 上文第四節(jié)有提到)。接下來就是求縮放倍率的問題了,首先在touchstart事件上求取兩手指間的距離d1;然后在touchmove事件上繼續(xù)求取兩手指間的距離d2,當(dāng)前縮放倍率= 初始縮放倍率 + (d2-d1) / 步長(例如每60px算0.1),touchend事件上讓初始縮放倍率=當(dāng)前縮放倍率。
至于如何求取縮放后圖片左上角的坐標(biāo)值,在草稿紙上畫來畫去,畫了很久......終于有點眉目。首先要找到一個縮放中心(這里做法是取雙指的中點坐標(biāo),但是這個坐標(biāo)必須要位于圖片上,如果不在圖片上,則取圖片上離該中點坐標(biāo)最近的點),然后存在下面這個等式
?。s放中心x坐標(biāo) - 縮放后圖片左上角x坐標(biāo))/ 縮放后圖片的寬度 = (縮放中心x坐標(biāo) - 縮放前圖片左上角x坐標(biāo))/ 縮放前圖片的寬度;(y坐標(biāo)同理)
接下來看看下面這個例子(在visio找了很久都沒有畫坐標(biāo)系的功能,所以只能手工畫了)
綠色框是一張10*5的圖片,藍色框是寬高放大兩倍后的圖片20*10,根據(jù)上面的公式推算的x2 = sx - w2(sx - x1) / w1,y2 = sy - h2(sy - y1) / h1。
堅持...繼續(xù)看看代碼吧
_initEvent() { let $gesture = this.$refs.gesture, cClientRect = this.$refs.canvas.getBoundingClientRect(), scx = 0, //對于單手操作是移動的起點坐標(biāo),對于縮放是圖片距離兩手指的中點最近的圖標(biāo)。 scy = 0, fingers = {}; //記錄當(dāng)前有多少只手指在觸控屏幕 //one finger let iX = this.imgX, iY = this.imgY; //two finger let figureDistance = 0, pinchScale = this.imgScale; $gesture.addEventListener('touchstart', 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('touchmove', 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('touchend', e => { if (!this.imgLoaded) { return; } this.imgScale = pinchScale; //從finger刪除已經(jīng)離開的手指 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是干嘛的,是用來記錄當(dāng)前有多少只手指在屏幕上觸摸。可能會出現(xiàn)這種情況,雙指縮放后,其中一只手指移出顯示屏,而另外一個手指在顯示屏上移動。針對這種情況,要在touchend事件上根據(jù)e.changedTouches來移除fingers里已經(jīng)離開顯示屏的finger,如果此時fingers里只剩下一個finger,則更新scx,scy,iX,iY為移動圖片做初始化準(zhǔn)備。
八、裁剪圖片
這里很簡單,就調(diào)用pCanvas的toDataURL方法就可以了
_clipper() { let imgData = null; try { imgData = this.$refs.pCanvas.toDataURL(); } catch (e) { console.error('請在response header加上Access-Control-Allow-Origin,否則canvas無法裁剪未經(jīng)許可的跨域圖片'); } this.$emit('sure', imgData); }
上面是我整理給大家的,希望今后會對大家有幫助。
相關(guān)文章:
在Bootstrap框架里使用treeview如何實現(xiàn)動態(tài)加載數(shù)據(jù)
在express+mockjs中如何實現(xiàn)后臺數(shù)據(jù)發(fā)送
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!

Outils d'IA chauds

Undress AI Tool
Images de déshabillage gratuites

Undresser.AI Undress
Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover
Outil d'IA en ligne pour supprimer les vêtements des photos.

Clothoff.io
Dissolvant de vêtements AI

Video Face Swap
échangez les visages dans n'importe quelle vidéo sans effort grace à notre outil d'échange de visage AI entièrement gratuit?!

Article chaud

Outils chauds

Bloc-notes++7.3.1
éditeur de code facile à utiliser et gratuit

SublimeText3 version chinoise
Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1
Puissant environnement de développement intégré PHP

Dreamweaver CS6
Outils de développement Web visuel

SublimeText3 version Mac
Logiciel d'édition de code au niveau de Dieu (SublimeText3)

Pour développer une application Web Python complète, suivez ces étapes: 1. Choisissez le cadre approprié, tel que Django ou Flask. 2. Intégrez les bases de données et utilisez des orms tels que Sqlalchemy. 3. Concevez le frontal et utilisez Vue ou React. 4. Effectuez le test, utilisez Pytest ou Unittest. 5. Déployer les applications, utiliser Docker et des plates-formes telles que Heroku ou AWS. Grace à ces étapes, des applications Web puissantes et efficaces peuvent être construites.

Le c?ur du système de routage frontal est de mapper les URL aux composants. VUerouter et Reactrouter réalisent la commutation de page sans rafra?chissement en écoutant les modifications de l'URL et en chargeant des composants correspondants. Les méthodes de configuration incluent: 1. Routing imbriqué, permettant aux composants enfants imbriqués dans le composant parent; 2. Routing dynamique, chargeant différents composants en fonction des paramètres URL; 3. Route Guard, effectuer une logique telle que les vérifications d'autorisation avant et après la commutation de l'itinéraire.

ReactivitytransforminVue3aimedtosimplifyhandlingreactivedatabyautomaticallytrackingandmanagingreactivitywithoutrequiringmanualref()or.valueusage.Itsoughttoreduceboilerplateandimprovecodereadabilitybytreatingvariableslikeletandconstasautomaticallyreac

Les différences de base entre Vue.js et React dans le développement des composants sont: 1) Vue.js utilise la syntaxe du modèle et l'option API, tandis que React utilise JSX et les composants fonctionnels; 2) Vue.js utilise des systèmes réactifs, React utilise des données immuables et un DOM virtuel; 3) Vue.js fournit plusieurs crochets de cycle de vie, tandis que React utilise plus de crochets d'effet d'utilisation.

Internationalisation et olocalisation dans la variation des acquis

Lorsque Vue.js gère les mises à jour du tableau, la vue n'est pas mise à jour car object.defineproperty ne peut pas écouter directement les modifications du tableau. Les solutions incluent: 1. Utilisez la méthode Vue.Set pour modifier l'index du tableau; 2. Réaffectez l'intégralité du tableau; 3. Utilisez la méthode de mutation réécrite de Vue pour faire fonctionner le tableau.

En utilisant: keyattributewithv-forrinvuesessentialforperformanceAndCorrectBehavior.Ort, itHelpsvueTrackEachElementation efficace par ce que

Les méthodes pour optimiser les performances des grandes listes et des composants complexes dans Vue incluent: 1. Utilisez la directive V-ONCE pour traiter le contenu statique pour réduire les mises à jour inutiles; 2. Implémentez le défilement virtuel et ne rendez que le contenu de la zone visuelle, comme l'utilisation de la bibliothèque Vue-Virtual-Croller; 3. Composants de cache via Keep-Alive ou V-ONCE pour éviter les supports en double; 4. Utilisez des propriétés et des auditeurs calculés pour optimiser la logique réactive pour réduire la plage de réinstallation; 5. Suivez les meilleures pratiques, comme l'utilisation de clés uniques en V-FOR, en évitant les fonctions en ligne dans les modèles et en utilisant des outils d'analyse des performances pour localiser les goulots d'étranglement. Ces stratégies peuvent améliorer efficacement la ma?trise de l'application.
