js+canvas图片裁剪
canvas 裁剪图片功能实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0" /> <title>裁剪图片</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } .app { display: grid; place-content: center; row-gap: 20px; } .avatar-layout { display: flex; place-content: center; column-gap: 20px; margin-top: 20px; } .preview { overflow: hidden; display: grid; place-content: center; width: 240px; height: 240px; border-radius: 6px; user-select: none; } .origin-preview { outline: 1px solid #999; .img-wrap { position: relative; display: flex; } .mask { --mask-x: 0px; --mask-y: 0px; --mask-width: 50px; --mask-height: 50px; --mask-show: none; display: var(--mask-show); position: absolute; inset: 0; translate: var(--mask-x) var(--mask-y); width: var(--mask-width); height: var(--mask-height); background-color: rgba(0, 0, 0, 0.5); /* border: 1px solid pink; */ z-index: 2; will-change: translate; .drag-point { --size: 20px; position: absolute; width: var(--size); height: var(--size); border: inherit; background-color: inherit; z-index: 3; } .point-lt { top: 0; left: 0; translate: calc(var(--size) * -1) calc(var(--size) * -1); cursor: nw-resize; } .point-lb { bottom: 0; left: 0; translate: calc(var(--size) * -1) calc(var(--size)); cursor: sw-resize; } .point-rt { top: 0; right: 0; translate: calc(var(--size)) calc(var(--size) * -1); cursor: ne-resize; } .point-rb { bottom: 0; right: 0; translate: var(--size) var(--size); cursor: se-resize; } } } .clip-preview { row-gap: 20px; text-align: center; outline: 1px solid #999; .clip-wrap { /* overflow: hidden; */ display: grid; place-content: center; width: 120px; height: 120px; /* border-radius: 50%; */ background-color: #dedede; outline: inherit; .clip { width: inherit; height: inherit; } } } img { width: 100%; height: 100%; object-fit: contain; } .btn { margin-inline: auto; } </style> </head> <body> <div class="app"> <div class="avatar-layout"> <div class="preview origin-preview"> <div class="img-wrap"> <img class="origin" alt="" draggable="false" /> <input type="file" id="file" hidden /> <div class="mask"> <span data-dir="lefttop" class="drag-point point-lt"></span> <span data-dir="leftbottom" class="drag-point point-lb"></span> <span data-dir="righttop" class="drag-point point-rt"></span> <span data-dir="rightbottom" class="drag-point point-rb"></span> </div> </div> </div> <hr /> <div class="preview clip-preview"> <span>预览头像</span> <div class="clip-wrap"> <canvas class="clip"></canvas> </div> </div> </div> <div class="btn"> <button class="btn-upload">重新选择</button> <button class="btn-crop-preview">预览裁剪图片</button> </div> </div> <script> /* canvas 裁剪图片功能实现 功能: 1. [x] 选择图片文件,预览原图 2. [x] 裁剪图片 3. [x] 显示裁剪后的图片 4. [x] 调整裁剪区域大小 5. [x] 获取裁剪后的图片 6. [] 图片裁剪功能优化 */ const originImg = document.querySelector('.origin'); /** @type {HTMLCanvasElement} */ const clipCanvas = document.querySelector('.clip'); const mask = document.querySelector('.mask'); const dragPoint = Array.from(mask.querySelectorAll('.drag-point')); const clipInfo = { width: mask.offsetWidth, height: mask.offsetHeight, x: originImg.offsetLeft, y: originImg.offsetTop, container: null, // 图片缩放比例 scaleX: originImg.naturalWidth / originImg.offsetWidth, scaleY: originImg.naturalHeight / originImg.offsetHeight, // 限制最小的 mask 大小 minMaskSize: 30, }; // mask.style.setProperty('--mask-x', clipInfo.x + 'px'); // mask.style.setProperty('--mask-y', clipInfo.y + 'px'); /** @type {CanvasRenderingContext2D} */ const clipCtx = clipCanvas.getContext('2d'); let pointX = 0, pointY = 0; init(); function init() { /** @type {HTMLInputElement} */ const file = document.getElementById('file'); const updloadBtn = document.querySelector('.btn-upload'); const imgWrap = document.querySelector('.img-wrap'); const cropPreviewBtn = document.querySelector('.btn-crop-preview'); // 裁切canvas的宽高 clipCanvas.width = clipCanvas.offsetWidth * devicePixelRatio; clipCanvas.height = clipCanvas.offsetHeight * devicePixelRatio; clipInfo.container = imgWrap; // 预览裁剪图片 cropPreviewBtn.addEventListener('click', () => { clipCanvas.toBlob((blob) => { const file = new File([blob], 'clip.png', { type: 'image/png' }); const url = URL.createObjectURL(file); open(url); }); }); // 监听文件选择 file.addEventListener('change', handleFile); // 点击按钮触发文件选择 updloadBtn.addEventListener('click', () => file.click()); // 监听鼠标按下 mask.addEventListener('pointerdown', (e) => { // const isPoint = dragPoint.includes(e.target); // if (isPoint) return; const { container: { offsetLeft, offsetTop }, } = clipInfo; pointX = e.offsetX + offsetLeft; pointY = e.offsetY + offsetTop; mask.style.cursor = 'grabbing'; // 监听鼠标移动 window.addEventListener('pointermove', handleMaskMove); // 监听鼠标抬起 window.addEventListener('pointerup', handleMaskUp); }); dragPoint.forEach((point) => { point.addEventListener('pointerdown', (e) => { e.stopPropagation(); const sx = e.clientX; const sy = e.clientY; const { minMaskSize, container } = clipInfo; const { left: appLeft, top: appTop, width: appWidth, height: appHeight, } = container.getBoundingClientRect(); let { left, top, width, height, right, bottom } = mask.getBoundingClientRect(); const { dir } = e.target.dataset; window.addEventListener('pointermove', handleDragPointMove); window.addEventListener('pointerup', handleDragPointUp); function handleDragPointMove(e) { const { clientX, clientY } = e; const dx = clientX - sx, dy = clientY - sy; const handlers = { lefttop: (box) => updateSizeAndPosition(box, -dx, -dy, true, true), leftbottom: (box) => updateSizeAndPosition(box, -dx, dy, true, false), righttop: (box) => updateSizeAndPosition(box, dx, -dy, false, true), rightbottom: (box) => updateSizeAndPosition(box, dx, dy, false, false), }; handlers[dir]?.(mask); function updateSizeAndPosition(box, dx, dy, x, y) { let _width = Math.round(width + dx), _height = Math.round(height + dy), _x = Math.round(left + -1 * dx - appLeft), _y = Math.round(top + -1 * dy - appTop); // 限制范围 _x = Math.max(0, _x); _x = Math.min(right - minMaskSize - appLeft, _x); _y = Math.max(0, _y); _y = Math.min(bottom - minMaskSize - appTop, _y); _width = Math.max(minMaskSize, _width); _width = Math.min(appWidth - (left - appLeft), _width); _height = Math.max(minMaskSize, _height); _height = Math.min(appHeight - (top - appTop), _height); box.style.setProperty('--mask-width', `${_width}px`); box.style.setProperty('--mask-height', `${_height}px`); if (x) box.style.setProperty('--mask-x', `${_x}px`); if (y) box.style.setProperty('--mask-y', `${_y}px`); clipInfo.width = _width; clipInfo.height = _height; clipInfo.x = _x; clipInfo.y = _y; drawClipImg(); } } function handleDragPointUp(e) { dragPoint.forEach((point) => { window.removeEventListener('pointermove', handleDragPointMove); window.removeEventListener('pointerup', handleDragPointUp); }); } }); }); } function handleMaskMove(e) { // 遮罩 mask 在容器内的位置 let x = Math.round(e.clientX - pointX); let y = Math.round(e.clientY - pointY); // (边界处理) 限制 mask 移动范围 x = Math.max(x, 0, originImg.offsetLeft); x = Math.min(x, originImg.offsetLeft + originImg.offsetWidth - clipInfo.width); y = Math.max(y, 0, originImg.offsetTop); y = Math.min(y, originImg.offsetTop + originImg.offsetHeight - clipInfo.height); clipInfo.x = x; clipInfo.y = y; mask.style.setProperty('--mask-x', clipInfo.x + 'px'); mask.style.setProperty('--mask-y', clipInfo.y + 'px'); drawClipImg(); } function handleMaskUp() { this.removeEventListener('pointermove', handleMaskMove); this.removeEventListener('pointerup', handleMaskUp); pointX = pointY = 0; mask.style.cursor = 'grab'; } function handleFile(e) { /** @type {File} */ const file = e.target.files[0]; if (!file) return; // 读取文件内容 const reader = new FileReader(); reader.onload = function () { originImg.onload = () => { // 图片加载完成后,设置图片大小 if (originImg.width < originImg.height) { const previewWrap = document.querySelector('.origin-preview'); clipInfo.container.style.height = previewWrap.offsetHeight + 'px'; clipInfo.container.style.width = originImg.width * previewWrap.offsetHeight / originImg.height + 'px'; } // 图片加载完成后,添加蒙层 addMask(); // 绘制裁剪图片 drawClipImg(); }; // 赋值原图图片 base64地址 originImg.src = reader.result; }; reader.readAsDataURL(file); } // 绘制裁剪图片 function drawClipImg() { const { x, y, width, height, scaleX, scaleY } = clipInfo; clipCtx.clearRect(0, 0, clipCanvas.width, clipCanvas.height); clipCtx.drawImage( originImg, /* 裁剪区域 */ (x - originImg.offsetLeft) * scaleX, (y - originImg.offsetTop) * scaleY, clipInfo.width * scaleX, clipInfo.height * scaleY, /* 绘制区域 */ 0, 0, clipCanvas.width, clipCanvas.height ); } /** * 添加蒙层 */ function addMask() { mask.style.setProperty('--mask-show', 'block'); clipInfo.width = mask.offsetWidth; clipInfo.height = mask.offsetHeight; clipInfo.x = originImg.offsetLeft; clipInfo.y = originImg.offsetTop; clipInfo.scaleX = originImg.naturalWidth / originImg.offsetWidth; clipInfo.scaleY = originImg.naturalHeight / originImg.offsetHeight; mask.style.setProperty('--mask-x', clipInfo.x + 'px'); mask.style.setProperty('--mask-y', clipInfo.y + 'px'); } </script> </body> </html>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)