在Web開發中,經常需要處理使用者上傳的圖片,其中一個常見的需求是允許使用者選擇並裁剪圖片。本文將介紹如何使用HTML、CSS和JavaScript實現一個簡單的圖片裁剪工具。
步驟概覽
- 建立HTML結構,包含檔案上傳控制元件、裁剪前的圖片顯示區域,選擇裁剪區域、Canvas和顯示裁剪後圖片的標籤。
- 在
uploadFile.onchange
中建立一個Image
物件,並在onload
事件中獲取圖片的實際尺寸(image.width
和image.height
),用於計算並設定圖片在裁剪容器中的顯示尺寸,以保持圖片的寬高比例。 - 在圖片上實現裁剪功能,其實就是一個拖拽,滑鼠按下可以拖動裁剪區域,鬆開就禁止拖拽。使用者選擇完裁剪區域後,我們先計算出裁剪框在原圖片中的位置和尺寸,根據這些資訊繪製裁剪後的圖片到Canvas上。
- 根據裁剪框在原圖片中的位置和尺寸與Canvas上圖片尺寸根據比列關聯起來。
- 將裁剪後的圖片轉換為Blob物件,並顯示在頁面上。
- 將Blob物件上傳到伺服器。
一.HTML和CSS結構
首先,我們需要一個HTML結構來容納上傳的圖片和裁剪區域。以下是一個基本的HTML結構:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> * { margin: 0; } body { color: #fff; background: rgba(0, 0, 0, 0.8); } .wrap { width: 500px; margin: 50px; } canvas { display: none; border: 1px solid red; } .upload { width: 150px; height: 30px; border: 1px solid; margin: 20px auto; position: relative; } .upload-file { position: absolute; left: 0; top: 0; width: 100%; height: 100%; opacity: 0; z-index: 1; cursor: pointer; } .upload-btn { position: absolute; left: 0; top: 0; width: 100%; height: 100%; text-align: center; line-height: 1.5; font-size: 20px; color: #fff; border-radius: 5px; } .clip-area { text-align: center; } .clip-wrap { display: none; width: 500px; height: 500px; background: #000; position: relative; } .clip-wrap .clip-img { position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; } .clip-box { display: none; position: absolute; width: 300px; height: 300px; background: rgba(0, 0, 0, 0.5); box-sizing: border-box; border: 1px solid #fff; } .clip-after { text-align: center; } .clip-after h1 { margin: 20px 0; } .clip-after-img { width: 300px; } .clip-after-btn { width: 100px; height: 30px; line-height: 30px; text-align: center; color: #fff; background: transparent; border: 1px solid #fff; cursor: pointer; margin-top: 20px; } </style> </head> <body> <div class="wrap"> <div class="upload"> <input type="file" class="upload-file"> <div class="upload-btn">圖片上傳</div> </div> <!-- 顯示上傳的圖片並選擇裁剪區域 --> <div class="clip-area"> <h1>裁剪區域</h1> <div class="clip-wrap"> <img src="" class="clip-img" style="width: 300px;"> <div class="clip-box"></div> </div> </div> <!-- 裁剪後的圖片 --> <div class="clip-after"> <h1> 裁剪後的圖片</h1> <div class="clip-after-content"> <img src="" class="clip-after-img"> </div> <canvas id="canvas"></canvas> <button class="clip-after-btn">確認</button> </div> </div> </body> </html>
二.JS邏輯
1、實現選擇圖片並把圖片等比例顯示出來
實現一個簡單的選擇圖片,再建立了一個圖片例項 image
,然後使用 FileReader
對使用者上傳的圖片進行讀取,並將讀取結果賦值給 image
的 src
屬性。接著,透過獲取 image
的寬度和高度,我們可以得知原始圖片的尺寸。基於這些尺寸資訊,在裁剪前先把圖片等比例顯示出來
const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 300; canvas.height = 300; const uploadFile = document.querySelector('.upload-file'); const clipAfterImg = document.querySelector('.clip-after-img'); let imageBlob; //上傳圖片 uploadFile.onchange = (e) => { clipWrap.style.display = 'block'; const fileData = e.target.files[0]; const reader = new FileReader(); const image = new Image(); reader.readAsDataURL(fileData); // 非同步讀取檔案內容,結果用data:url的字串形式表示 reader.onload = function (e) { const imgUrl = this.result; image.src = this.result; image.onload = function () { // 計算圖片繪製區域,確保圖片保持比例 const aspectRatio = image.width / image.height; computeSize(aspectRatio, imgUrl); }; } } //計算上傳圖片要顯示的尺寸 function computeSize(aspectRatio, imgUrl) { let drawWidth, drawHeight; if (aspectRatio > 1) { // 圖片更寬 drawWidth = clipWrap.offsetWidth; drawHeight = drawWidth / aspectRatio; } else { // 圖片更高 drawHeight = clipWrap.offsetHeight; drawWidth = clipWrap.offsetHeight * aspectRatio; } clipImg.src = imgUrl clipImg.style.width = `${drawWidth}px`; clipImg.style.height = `${drawHeight}px` }
每次選擇完圖片後顯示選擇區域並預設居中。
//裁剪選擇塊的位置居中 function centerClipBox() { clipBox.style.left = `${(clipWrap.clientWidth - clipBox.clientWidth) / 2}px`; clipBox.style.top = `${(clipWrap.clientHeight - clipBox.clientHeight) / 2}px`; clipBox.style.display = 'block'; }
2、實現拖拽功能,用拖拽選擇區域來選擇內容
只有在滑鼠按下 mousedown 事件才允許拖拽,滑鼠鬆開 mouseup 禁止拖拽,再滑鼠移動事件 mousemove 中計算拖拽的位置。並新增範圍限制。
// 拖拽選擇裁剪區域 const clipWrap = document.querySelector('.clip-wrap'); const clipImg = document.querySelector('.clip-img'); const clipBox = document.querySelector('.clip-box'); let isTtrue = false; let initX; let initY; //初始化裁剪選擇塊的位置 centerClipBox() //裁剪選擇塊的位置居中 function centerClipBox() { clipBox.style.left = `${(clipWrap.clientWidth - clipBox.clientWidth) / 2}px`; clipBox.style.top = `${(clipWrap.clientHeight - clipBox.clientHeight) / 2}px`; clipBox.style.display = 'block'; } clipBox.addEventListener('mousedown', (e) => { isTtrue = true; initX = e.clientX - clipWrap.offsetLeft - clipImg.offsetLeft - clipBox.offsetLeft; initY = e.clientY - clipWrap.offsetLeft - clipImg.offsetTop - clipBox.offsetTop; }) //滑鼠移動選擇裁剪區域,並新增節流最佳化 document.addEventListener('mousemove', (e) => { if (isTtrue) { moveX = e.clientX - initX; let moveY = e.clientY - initY; let maxX = clipImg.offsetWidth - clipBox.offsetWidth + clipImg.offsetLeft; let maxY = clipImg.offsetHeight - clipBox.offsetHeight + clipImg.offsetTop; let minX = clipImg.offsetLeft; let minY = clipImg.offsetTop; moveX = Math.max(minX, Math.min(moveX, maxX)); moveY = Math.max(minY, Math.min(moveY, maxY)); clipBox.style.left = moveX + 'px'; clipBox.style.top = moveY + 'px'; } }) document.addEventListener('mouseup', () => { isTtrue = false; });
3、計算選擇區域在圖片上的位置和尺寸
drawImage
函式繪製裁剪後的圖片,並將其顯示在頁面上。首先獲取裁剪框和圖片的位置和尺寸資訊,然後根據這些資訊繪製裁剪後的圖片到Canvas上。最後透過 canvas.toBlob
方法將Canvas內容轉換為Blob物件,建立圖片URL並設定給裁剪後的圖片元素。
function drawImage() { // 獲取裁剪框和圖片的位置和尺寸資訊 const clipRect = clipBox.getBoundingClientRect(); const imgRect = clipImg.getBoundingClientRect(); const scaleX = clipImg.naturalWidth / imgRect.width; const scaleY = clipImg.naturalHeight / imgRect.height; const cropX = (clipRect.left - imgRect.left) * scaleX; const cropY = (clipRect.top - imgRect.top) * scaleY; const cropWidth = clipBox.clientWidth * scaleX; const cropHeight = clipBox.clientHeight * scaleY; // 調整畫布尺寸 canvas.width = cropWidth; canvas.height = cropHeight; // 繪製裁剪後的圖片 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage( clipImg, cropX, cropY, cropWidth, cropHeight, 0, 0, canvas.width, canvas.height ); // 將畫布內容轉換為Blob物件 canvas.toBlob(function (blob) { // 建立圖片URL並設定給裁剪後的圖片元素 const url = URL.createObjectURL(blob); clipAfterImg.src = url; imageBlob = blob; }, 'image/png'); }
透過 getBoundingClientRect () 獲取圖片和選擇塊的位置,再透過圖片的原始尺寸和顯示的尺寸算出寬高比,移動距離和 canvas 繪製的圖片都乘以寬高比,算出從哪裡開始裁剪和畫布的尺寸大小。
5、再圖片 onchange 和拖拽觸發 mousemove 呼叫drawImage來繪製圖片,並新增節流事件防止頻繁觸發。
//計算上傳圖片要顯示的尺寸 function computeSize(aspectRatio, imgUrl) { let drawWidth, drawHeight; if (aspectRatio > 1) { // 圖片更寬 drawWidth = clipWrap.offsetWidth; drawHeight = drawWidth / aspectRatio; } else { // 圖片更高 drawHeight = clipWrap.offsetHeight; drawWidth = clipWrap.offsetHeight * aspectRatio; } clipImg.src = imgUrl clipImg.style.width = `${drawWidth}px`; clipImg.style.height = `${drawHeight}px` clipImg.onload = () => { // 在計算完大小後居中顯示clipBox centerClipBox(); drawImage() } } document.addEventListener('mousemove', throttle((e) => { if (isTtrue) { moveX = e.clientX - initX; let moveY = e.clientY - initY; let maxX = clipImg.offsetWidth - clipBox.offsetWidth + clipImg.offsetLeft; let maxY = clipImg.offsetHeight - clipBox.offsetHeight + clipImg.offsetTop; let minX = clipImg.offsetLeft; let minY = clipImg.offsetTop; moveX = Math.max(minX, Math.min(moveX, maxX)); moveY = Math.max(minY, Math.min(moveY, maxY)); clipBox.style.left = moveX + 'px'; clipBox.style.top = moveY + 'px'; //裁剪區域移動重新繪製圖片 drawImage(); } }, 50)); // 節流 function throttle(fn, delay = 300) { let timer; return function (...args) { if (timer) return; timer = setTimeout(() => { fn(...args); timer = null; }, delay); } }
效果
全部程式碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> * { margin: 0; } body { color: #fff; background: rgba(0, 0, 0, 0.8); } .wrap { width: 500px; margin: 50px; } canvas { display: none; border: 1px solid red; } .upload { width: 150px; height: 30px; border: 1px solid; margin: 20px auto; position: relative; } .upload-file { position: absolute; left: 0; top: 0; width: 100%; height: 100%; opacity: 0; z-index: 1; cursor: pointer; } .upload-btn { position: absolute; left: 0; top: 0; width: 100%; height: 100%; text-align: center; line-height: 1.5; font-size: 20px; color: #fff; border-radius: 5px; } .clip-area { text-align: center; } .clip-wrap { display: none; width: 500px; height: 500px; background: #000; position: relative; } .clip-wrap .clip-img { position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; } .clip-box { display: none; position: absolute; width: 300px; height: 300px; background: rgba(0, 0, 0, 0.5); box-sizing: border-box; border: 1px solid #fff; } .clip-after { text-align: center; } .clip-after h1 { margin: 20px 0; } .clip-after-img { width: 300px; } .clip-after-btn { width: 100px; height: 30px; line-height: 30px; text-align: center; color: #fff; background: transparent; border: 1px solid #fff; cursor: pointer; margin-top: 20px; } </style> </head> <body> <div class="wrap"> <div class="upload"> <input type="file" class="upload-file"> <div class="upload-btn">圖片上傳</div> </div> <!-- 顯示上傳的圖片並選擇裁剪區域 --> <div class="clip-area"> <h1>裁剪區域</h1> <div class="clip-wrap"> <img src="" class="clip-img" style="width: 300px;"> <div class="clip-box"></div> </div> </div> <!-- 裁剪後的圖片 --> <div class="clip-after"> <h1> 裁剪後的圖片</h1> <div class="clip-after-content"> <img src="" class="clip-after-img"> </div> <canvas id="canvas"></canvas> <button class="clip-after-btn">確認</button> </div> </div> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 300; canvas.height = 300; const uploadFile = document.querySelector('.upload-file'); const clipAfterImg = document.querySelector('.clip-after-img'); let imageBlob; //上傳圖片 uploadFile.onchange = (e) => { clipWrap.style.display = 'block'; const fileData = e.target.files[0]; const reader = new FileReader(); const image = new Image(); reader.readAsDataURL(fileData); // 非同步讀取檔案內容,結果用data:url的字串形式表示 reader.onload = function (e) { const imgUrl = this.result; image.src = this.result; image.onload = function () { // 計算圖片繪製區域,確保圖片保持比例 const aspectRatio = image.width / image.height; computeSize(aspectRatio, imgUrl); }; } } //計算上傳圖片要顯示的尺寸 function computeSize(aspectRatio, imgUrl) { let drawWidth, drawHeight; if (aspectRatio > 1) { // 圖片更寬 drawWidth = clipWrap.offsetWidth; drawHeight = drawWidth / aspectRatio; } else { // 圖片更高 drawHeight = clipWrap.offsetHeight; drawWidth = clipWrap.offsetHeight * aspectRatio; } clipImg.src = imgUrl clipImg.style.width = `${drawWidth}px`; clipImg.style.height = `${drawHeight}px` clipImg.onload = () => { // 在計算完大小後居中顯示clipBox centerClipBox(); drawImage() } } // 拖拽選擇裁剪區域 const clipWrap = document.querySelector('.clip-wrap'); const clipImg = document.querySelector('.clip-img'); const clipBox = document.querySelector('.clip-box'); let isTtrue = false; let initX; let initY; //初始化裁剪選擇塊的位置 centerClipBox() //裁剪選擇塊的位置居中 function centerClipBox() { clipBox.style.left = `${(clipWrap.clientWidth - clipBox.clientWidth) / 2}px`; clipBox.style.top = `${(clipWrap.clientHeight - clipBox.clientHeight) / 2}px`; clipBox.style.display = 'block'; } clipBox.addEventListener('mousedown', (e) => { isTtrue = true; initX = e.clientX - clipWrap.offsetLeft - clipImg.offsetLeft - clipBox.offsetLeft; initY = e.clientY - clipWrap.offsetLeft - clipImg.offsetTop - clipBox.offsetTop; }) //滑鼠移動選擇裁剪區域,並新增節流最佳化 document.addEventListener('mousemove', throttle((e) => { if (isTtrue) { moveX = e.clientX - initX; let moveY = e.clientY - initY; let maxX = clipImg.offsetWidth - clipBox.offsetWidth + clipImg.offsetLeft; let maxY = clipImg.offsetHeight - clipBox.offsetHeight + clipImg.offsetTop; let minX = clipImg.offsetLeft; let minY = clipImg.offsetTop; moveX = Math.max(minX, Math.min(moveX, maxX)); moveY = Math.max(minY, Math.min(moveY, maxY)); clipBox.style.left = moveX + 'px'; clipBox.style.top = moveY + 'px'; //裁剪區域移動重新繪製圖片 drawImage() } }, 50)) document.addEventListener('mouseup', () => { isTtrue = false; }); //在canvas上繪製選擇的區域並轉為圖片 function drawImage() { const clipRect = clipBox.getBoundingClientRect(); const imgRect = clipImg.getBoundingClientRect(); const scaleX = clipImg.naturalWidth / imgRect.width; const scaleY = clipImg.naturalHeight / imgRect.height; const cropX = (clipRect.left - imgRect.left) * scaleX; const cropY = (clipRect.top - imgRect.top) * scaleY; const cropWidth = clipBox.clientWidth * scaleX; const cropHeight = clipBox.clientHeight * scaleY; // 調整畫布尺寸 canvas.width = cropWidth; canvas.height = cropHeight; // 清空畫布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 將畫布剪裁為圓形區域 ctx.beginPath(); ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); // 繪製圖片 ctx.drawImage( clipImg, cropX, cropY, cropWidth, cropHeight, 0, 0, canvas.width, canvas.height ); // 將畫布內容轉換為圖片 canvas.toBlob(function (blob) { const url = URL.createObjectURL(blob); clipAfterImg.src = url; imageBlob = blob; }, 'image/png'); } //圖片上傳 uploadBtn.addEventListener('click', () => { if (imageBlob) { const formData = new FormData(); formData.append('image', imageBlob, 'image.png'); fetch('/upload', { method: 'POST', body: formData, }) .then(response => response.json()) .then(data => { console.log('Success:', data); }) .catch((error) => { console.error('Error:', error); }); } else { console.error('No image available to upload.'); } }); // 節流 function throttle(fn, delay = 300) { let timer; return function (...args) { if (timer) return; timer = setTimeout(() => { fn(...args); timer = null; }, delay); } } </script> </body> </html>