JavaScript使用canvas實現(xiàn)錨點摳圖功能
效果展示
體驗地址
http://jyeontu.xyz/JDemo/#/imgCut
代碼實現(xiàn)
一、圖片上傳
想要進行摳圖的話我們得先有圖片是吧,所以要有個圖片上傳的功能。
1、本地圖片上傳
這里我們使用簡單的點擊按鈕上傳,前面也有文章介紹過了拖拽上傳功能的實現(xiàn),這里就不贅述了,有興趣的可以看下這篇文章:《文件拖拽上傳功能已經(jīng)爛大街了,你還不會嗎?》
這里我們直接使用input
標簽來實現(xiàn)上傳功能即可:
<label for="file-upload" class="custom-file-upload"> <i class="fas fa-cloud-upload-alt"></i> 選擇文件 </label> <input v-show="false" id="file-upload" type="file" accept="image/*" @change="handleFileUpload" />
handleFileUpload(e) { let file = e.target.files[0]; if (!file) return; this.srcLink = ""; const reader = new FileReader(); reader.onload = event => { const img = new Image(); img.onload = () => { this.image = img; this.width = img.width; this.height = img.height; this.originWidth = img.width; this.originHeight = img.height; this.drawCanvas(); }; img.src = event.target.result; }; reader.readAsDataURL(file); }
2、在線鏈接圖片
使用Input輸入在線圖片鏈接:
<input type="input" @change="inputSrc" placeholder="輸入圖片在線地址" v-model="srcLink" class="input-style" style="width: 100%;" />
getImageBase64FromURL(url, callback) { return new Promise(resove => { const xhr = new XMLHttpRequest(); xhr.onload = function() { const reader = new FileReader(); reader.onloadend = function() { resove(reader.result); }; reader.readAsDataURL(xhr.response); }; xhr.open("GET", url); xhr.responseType = "blob"; xhr.send(); }); }, async inputSrc() { const src = await this.getImageBase64FromURL(this.srcLink); const img = new Image(); img.onload = () => { this.image = img; this.width = img.width; this.height = img.height; this.drawCanvas(); }; img.src = src; }
3、將上傳的圖片繪制到canvas中
drawCanvas() { setTimeout(() => { if (!this.image || !this.ctx) { return; } this.ctx.clearRect(0, 0, this.width, this.height); this.ctx.save(); this.ctx.translate(this.width / 2, this.height / 2); this.ctx.drawImage( this.image, -this.width / 2, -this.height / 2, this.width, this.height ); this.ctx.restore(); this.realPoints.forEach(point => { this.drawPoint(point.x, point.y); }); this.connectPoints(); // 每次繪制canvas后連接所有點 }, 100); }
使用ctx.clearRect()
方法清除整個畫布,以便在重新繪制之前清空之前的內容。然后,使用ctx.save()
方法保存當前的繪圖狀態(tài)。
通過ctx.translate()
方法將繪圖原點移動到畫布的中心位置(this.width / 2, this.height / 2),這樣可以方便地繪制圖像和點的坐標。
使用ctx.drawImage()
方法繪制圖像,參數(shù)分別為圖像對象this.image
、圖像左上角的x和y坐標(-this.width / 2, -this.height / 2),以及圖像的寬度和高度(this.width, this.height)。這樣就在畫布上繪制了圖像。
接著使用ctx.restore()
方法恢復之前保存的繪圖狀態(tài)。
然后,通過forEach
循環(huán)遍歷this.realPoints
數(shù)組中的每個點,調用this.drawPoint()
方法繪制每個點。
最后,調用this.connectPoints()
方法連接所有的點,以繪制線條。
二、錨點選擇與撤銷
1、監(jiān)聽鼠標點擊
這里我們使用canvas來展示圖片:
<canvas ref="canvas" id="example-canvas" :width="width" :height="height" @click="canvasClick" tabindex="0" ></canvas>
監(jiān)聽canvas的點擊事件并保存點擊坐標
canvasClick(event) { if (!this.image || !this.ctx) { return; } const x = event.offsetX / (this.width / this.originWidth); const y = event.offsetY / (this.height / this.originHeight); this.points.push({ x, y }); // 將坐標添加到數(shù)組中 const point = this.tranPoint({ x, y }); this.drawPoint(point.x, point.y); },
2、繪制錨點
前面我們獲取到點擊坐標了,這里我們需要在該坐標上繪制上錨點:
drawPoint(x, y) { // 繪制一個小圓點 this.ctx.beginPath(); this.ctx.arc(x, y, 4, 0, 2 * Math.PI); this.ctx.fillStyle = "red"; this.ctx.fill(); this.ctx.closePath(); this.connectPoints(); // 每次點擊后連接所有點 },
使用beginPath()方法創(chuàng)建路徑,然后使用arc()方法繪制圓形,參數(shù)解釋如下:
- x: 圓心的x軸坐標
- y: 圓心的y軸坐標
- 4: 圓的半徑
- 0, 2 * Math.PI: 圓弧的起始角度和結束角度,這里表示繪制一個完整的圓
接下來設置fillStyle屬性為紅色,使用fill()方法填充圓形區(qū)域,并使用closePath()方法關閉路徑。
3、連接錨點
用虛線將所有錨點按順序連接起來:
connectPoints() { if (this.realPoints.length <= 1) { return; } this.ctx.beginPath(); this.ctx.moveTo(this.realPoints[0].x, this.realPoints[0].y); for (let i = 1; i < this.realPoints.length; i++) { this.ctx.lineTo(this.realPoints[i].x, this.realPoints[i].y); } this.ctx.setLineDash([5, 5]); this.ctx.strokeStyle = "blue"; this.ctx.lineWidth = 2; this.ctx.stroke(); this.ctx.closePath(); }
如果realPoints數(shù)組長度大于1,接著使用beginPath()方法開始創(chuàng)建新的路徑,并通過moveTo()方法將畫筆移動到第一個點的位置(this.realPoints[0].x, this.realPoints[0].y)。隨后使用for循環(huán)遍歷realPoints數(shù)組中的每個點,使用lineTo()方法將畫筆移動到下一個點的位置(this.realPoints[i].x, this.realPoints[i].y),從而連接所有的點。
在繪制線條之前,通過setLineDash()方法設置虛線的樣式,這里是一個5像素的實線和5像素的空白,表示虛線的樣式。然后設置線條的顏色為藍色,線寬為2像素,最后通過stroke()方法繪制連接線條。最后使用closePath()方法關閉路徑。
4、錨點撤銷功能
平時我們都習慣了通過Ctrl+Z
來撤銷上一步操作,這里我們也加上,通過監(jiān)聽鍵盤按鍵事件來實現(xiàn)當用戶按下Ctrl+Z
組合鍵時,撤銷最后一步錨點操作,也就是將錨點列表的最后一個刪除即可:
document.addEventListener("keydown", event => { if (event.ctrlKey && event.key === "z") { event.preventDefault(); that.undoPoint(); } }); undoPoint() { if (this.points.length > 0) { this.points.pop(); this.drawCanvas(); } },
5、獲取錨點集合
這里我們在右邊預留了一個展示錨點列表的文本域
<textarea v-model="pointsStr" class="points-list"></textarea>
computed: { pointsStr() { return JSON.stringify(this.realPoints); } }
大家覺得這里輸出錨點集合可以做什么?這里先賣個關子,下一篇博客就會需要用到這里的錨點集合了。
三、尺寸修改
頁面上我們可以對圖片尺寸進行修改,便于獲取不同比例下的錨點集:
1、頁面圖片尺寸修改
<label class="label-style">寬</label> <input type="number" v-model="width" @input="resizeImage($event, 'width')" @keydown.ctrl.z.prevent class="input-style" /> <label class="label-style">高</label> <input type="number" v-model="height" @input="resizeImage($event, 'height')" @keydown.ctrl.z.prevent class="input-style" /> <label class="label-style">按比例縮放</label> <input type="checkbox" v-model="aspectRatio" class="checkbox-style" />
resizeImageByWidth(event) { this.width = event.target.value ? parseInt(event.target.value) : null; if (this.aspectRatio && this.width) { this.height = Math.round( (this.width / this.originWidth) * this.originHeight ); } }, resizeImageByHeight(event) { this.height = event.target.value ? parseInt(event.target.value) : null; if (this.aspectRatio && this.height) { this.width = Math.round( (this.height / this.originHeight) * this.originWidth ); } }, resizeImage(event, dimension) { if (!this.image) { return; } if (dimension === "width") { this.resizeImageByWidth(event); } else if (dimension === "height") { this.resizeImageByHeight(event); } if ( this.aspectRatio && (!event || event.target !== document.activeElement) ) { const aspectRatio = this.originWidth / this.originHeight; if (this.width && !this.height) { this.height = Math.round(this.originWidth / aspectRatio); } else if (!this.width && this.height) { this.width = Math.round(this.originHeight * aspectRatio); } else if (this.width / aspectRatio < this.height) { this.width = Math.round(this.originHeight * aspectRatio); } else { this.height = Math.round(this.originWidth / aspectRatio); } } this.$refs.canvas.width = this.width ? this.width : null; this.$refs.canvas.height = this.height ? this.height : null; this.image.width = this.width; this.image.height = this.height; this.drawCanvas(); }
根據(jù) dimension
的值(可能是 "width" 或 "height"),調用相應的方法來調整圖像的寬度或高度。
resizeImageByWidth(event)
方法用于根據(jù)給定的寬度調整圖像的大小。它首先將 event.target.value
轉換為整數(shù),并將結果賦值給 this.width
。然后,如果啟用了縱橫比 (this.aspectRatio
) 并且 this.width
有值,則計算出相應的高度,使得調整后的圖像與原始圖像保持相同的縱橫比。
resizeImageByHeight(event)
方法用于根據(jù)給定的高度調整圖像的大小。它的邏輯與 resizeImageByWidth(event)
類似,只是操作的是 this.height
和寬高比的計算方式不同。
接下來,如果啟用了縱橫比 (this.aspectRatio
) 并且沒有通過鍵盤事件觸發(fā)該方法,則根據(jù)原始圖像的寬高比 (this.originWidth / this.originHeight
) 進行額外的調整。具體的調整邏輯如下:
- 如果只設置了寬度 (
this.width
) 而沒有設置高度 (this.height
),則根據(jù)原始圖像的寬高比計算出相應的高度。 - 如果只設置了高度 (
this.height
) 而沒有設置寬度 (this.width
),則根據(jù)原始圖像的寬高比計算出相應的寬度。 - 如果設置了寬度和高度,并且根據(jù)當前的寬高比計算出的寬度小于當前的高度,則根據(jù)原始圖像的寬高比計算出相應的寬度。
- 否則,根據(jù)原始圖像的寬高比計算出相應的高度。
最后,根據(jù)調整后的寬度和高度,更新畫布(this.$refs.canvas.width
和 this.$refs.canvas.height
),以及圖像的寬度和高度 (this.image.width
和 this.image.height
)。然后調用 drawCanvas()
方法重新繪制畫布。
2、錨點根據(jù)縮放比例進行修改
圖片縮放之后,錨點位置也要進行對應的縮放。
tranPoint(point) { let { x, y } = point; x = x * (this.width / this.originWidth); y = y * (this.height / this.originHeight); return { x, y }; }
四、摳圖預覽
1、圖片預覽組件
這里我們簡單編寫一個圖片預覽彈窗組件:
<template> <div> <div class="preview-overlay" @click="hidePreview"> <img :src="currentImage" alt="preview image" class="preview-image" /> <div class="export-button" @click.stop="handleExport"> <span>導出圖片</span> <span class="shine"></span> </div> </div> </div> </template> <script> export default { name: "previewImg", props: { imageList: { type: Array, default: () => [] }, currentImage: { type: String, default: "" } }, data() { return {}; }, methods: { hidePreview() { this.$emit("close"); }, handleExport() { this.$emit("export", this.currentImage); } } }; </script> <style> .preview-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 999; } .preview-image { max-width: 80%; max-height: 80%; object-fit: contain; } .export-button { position: absolute; bottom: 20px; padding: 10px; background-color: #00aaff; color: white; border-radius: 5px; cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 16px; font-weight: bold; text-align: center; box-shadow: 0 0 10px #00aaff; overflow: hidden; } .export-button:hover { background-color: #00e5ff; } .shine { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-image: linear-gradient( 45deg, #ffffff 10%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0) 100% ); animation: exportButtonShine 2s linear infinite; } @keyframes exportButtonShine { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } </style>
模板部分包含了一個遮罩層和圖片預覽,以及一個導出按鈕。當用戶點擊遮罩層時,會觸發(fā) hidePreview
方法,關閉預覽。圖片預覽部分使用了動態(tài)綁定的 :src
屬性來顯示當前的圖片,而導出按鈕則綁定了 handleExport
方法,在點擊時會觸發(fā)導出操作。
腳本部分定義了名為 "previewImg" 的組件,其中包括了兩個屬性 imageList
和 currentImage
,分別用于接收圖片列表和當前顯示的圖片。在方法部分,定義了 hidePreview
方法用于關閉預覽,并通過 $emit
向父組件發(fā)送 "close" 事件,以通知父組件關閉預覽。另外還有 handleExport
方法,用于處理導出操作,并通過 $emit
向父組件發(fā)送 "export" 事件,并傳遞當前圖片的路徑。
2、摳圖操作
cutImg() { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!this.image || !ctx) { return; } const image = this.image; canvas.width = image.width; canvas.height = image.height; // 定義剪切路徑 const cutPath = this.realPoints; ctx.beginPath(); ctx.moveTo(cutPath[0].x, cutPath[0].y); for (let i = 1; i < cutPath.length; i++) { ctx.lineTo(cutPath[i].x, cutPath[i].y); } ctx.closePath(); ctx.clip(); // 繪制圖片 ctx.drawImage(image, 0, 0, this.width, this.height); // 將Canvas元素轉換為PNG圖像 const imgData = canvas.toDataURL("image/png"); this.currentImage = imgData; this.showImg = true; }
獲取要剪切的圖片對象,并根據(jù)該圖片的寬度和高度設置 <canvas>
的寬度和高度。
然后,定義剪切路徑,通過遍歷 cutPath
數(shù)組中的點坐標,使用 ctx.lineTo()
方法繪制路徑。最后使用 ctx.closePath()
方法閉合路徑,并調用 ctx.clip()
方法將剪切路徑應用于上下文。
接著,使用 ctx.drawImage()
方法繪制剪切后的圖片。傳入的參數(shù)包括原始圖片對象、剪切后的起始點坐標以及剪切后的寬度和高度。
最后,使用 canvas.toDataURL()
方法將 <canvas>
元素轉換為 base64 編碼的 PNG 圖像數(shù)據(jù),并將該數(shù)據(jù)賦值給 imgData
變量。然后將 imgData
賦值給 currentImage
屬性,將剪切后的圖片顯示出來(通過在模板中綁定 currentImage
)。
五、導出摳圖圖片
downloadImg(imgData) { // 創(chuàng)建一個鏈接元素,將圖像數(shù)據(jù)作為URL設置給它 const link = document.createElement("a"); link.download = "myImage.png"; link.href = imgData; // 觸發(fā)鏈接的下載事件 link.click(); }
首先,通過 document.createElement("a")
創(chuàng)建一個 <a>
元素,并將該元素賦值給 link
變量。
然后,將要下載的圖片的文件名設置為 "myImage.png",可以根據(jù)實際需要修改。
接下來,將圖片數(shù)據(jù) imgData
設置為鏈接元素的 href
屬性,這樣點擊鏈接時會下載該圖片。
最后,通過調用 link.click()
方法觸發(fā)鏈接的點擊事件,從而觸發(fā)下載操作。
以上就是JavaScript使用canvas實現(xiàn)錨點摳圖功能的詳細內容,更多關于JavaScript canvas錨點摳圖的資料請關注腳本之家其它相關文章!
相關文章
讓innerText在firefox火狐和IE瀏覽器都能用的寫法
下面的代碼主要是用來解決firefox瀏覽器不支持innerText的問題,需要的朋友可以參考下。2011-05-05微信小程序點擊圖片實現(xiàn)長按預覽、保存、識別帶參數(shù)二維碼、轉發(fā)等功能
這篇文章主要介紹了微信小程序點擊圖片實現(xiàn)長按預覽、保存、識別帶參數(shù)二維碼、轉發(fā)等功能,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-07-07JS將json字符串轉換為JsonObject的多種實現(xiàn)方法
在Web前端開發(fā)中,JSON(JavaScript Object Notation)作為數(shù)據(jù)交換格式被廣泛使用,它輕量級且易于人類閱讀和編寫,同時也易于機器解析和生成,當從服務器接收數(shù)據(jù)時,我們需要將其轉換為JSON對象以便于操作,本文將深入探討如何利用JavaScript實現(xiàn)這一轉換過程2025-02-02JavaScript函數(shù)及其prototype詳解
這篇文章主要介紹了JavaScript函數(shù)及其prototype詳解的相關資料,需要的朋友可以參考下2023-03-03