在浏览器里裁剪图片:纯原生、零依赖的 JavaScript 小妙招

先用一个极简 demo 走一遍流程。

你将做出什么从用户设备本地上传图片在 <canvas> 中实时展示与裁剪拖拽框调整裁剪区域即时预览裁剪结果一键生成并下载裁剪后的图片,无需服务器Demo(原生 HTML/CSS/JS)
复制
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <style> .container { max-width: 600px; margin: 20px auto; text-align: center; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; } .image-wrapper { position: relative; display: inline-block; } .crop-frame { display: none; position: absolute; border: 3px dashed #28a745; /* 视觉反馈更明显 */ box-sizing: border-box; cursor: move; } .resize-handle { position: absolute; width: 12px; height: 12px; background: #28a745; cursor: nwse-resize; } .preview-area { margin-top: 20px; } button { padding: 10px20px; background: #28a745; color: #fff; border: none; cursor: pointer; border-radius: 6px; } button:disabled { opacity: .6; cursor: not-allowed; } </style> </head> <body> <div class="container"> <input type="file" id="imageInput" accept="image/*" /> <div class="image-wrapper"> <canvas id="mainCanvas"></canvas> <div class="crop-frame" id="cropFrame"></div> </div> <button id="cropButton" disabled>Crop & Download</button> <div class="preview-area"> <canvas id="previewCanvas"></canvas> </div> </div> <script> const imageInput = document.getElementById("imageInput"); const mainCanvas = document.getElementById("mainCanvas"); const ctx = mainCanvas.getContext("2d"); const cropFrame = document.getElementById("cropFrame"); const previewCanvas = document.getElementById("previewCanvas"); const previewCtx = previewCanvas.getContext("2d"); const cropButton = document.getElementById("cropButton"); let image = new Image(); let cropData = { x: 100, y: 100, width: 150, height: 150 }; let isDragging = false; // 初始尺寸设为 0(数值),避免拉伸 mainCanvas.width = 0; mainCanvas.height = 0; imageInput.addEventListener("change", (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; image.src = URL.createObjectURL(file); image.onload = () => { // 固定画布演示尺寸(也可按比例自适应) mainCanvas.width = 600; mainCanvas.height = 450; // 绘制整图到画布 ctx.clearRect(0, 0, mainCanvas.width, mainCanvas.height); ctx.drawImage(image, 0, 0, mainCanvas.width, mainCanvas.height); // 启用裁剪 updateCropFrame(); cropButton.disabled = false; }; }); function updateCropFrame() { cropFrame.style.display = "block"; cropFrame.style.left = `${cropData.x}px`; cropFrame.style.top = `${cropData.y}px`; cropFrame.style.width = `${cropData.width}px`; cropFrame.style.height = `${cropData.height}px`; cropFrame.innerHTML = <div class="resize-handle" style="bottom: -6px; right: -6px;"></div>; } cropFrame.addEventListener("mousedown", (e) => { isDragging = true; if (e.target.classList.contains("resize-handle")) { cropFrame.dataset.mode = "resize"; } else { cropFrame.dataset.mode = "move"; } }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const rect = mainCanvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; if (cropFrame.dataset.mode === "move") { cropData.x = Math.max( 0, Math.min( mouseX - cropData.width / 2, mainCanvas.width - cropData.width ) ); cropData.y = Math.max( 0, Math.min( mouseY - cropData.height / 2, mainCanvas.height - cropData.height ) ); } elseif (cropFrame.dataset.mode === "resize") { cropData.width = Math.max(50, mouseX - cropData.x); cropData.height = Math.max(50, mouseY - cropData.y); // 限制在画布范围内 cropData.width = Math.min(cropData.width, mainCanvas.width - cropData.x); cropData.height = Math.min(cropData.height, mainCanvas.height - cropData.y); } updateCropFrame(); }); document.addEventListener("mouseup", () => { isDragging = false; cropFrame.dataset.mode = ""; }); cropButton.addEventListener("click", () => { if (!image.width) return; previewCanvas.width = cropData.width; previewCanvas.height = cropData.height; // 将画布坐标映射回原图坐标 const scaleX = image.width / mainCanvas.width; const scaleY = image.height / mainCanvas.height; previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); previewCtx.drawImage( image, cropData.x * scaleX, cropData.y * scaleY, cropData.width * scaleX, cropData.height * scaleY, 0, 0, cropData.width, cropData.height ); const link = document.createElement("a"); link.download = "cropped-image.png"; link.href = previewCanvas.toDataURL("image/png"); link.click(); }); </script> </body> </html>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.117.118.119.120.121.122.123.124.125.126.127.128.129.130.131.132.133.134.135.136.137.138.139.140.141.142.143.144.145.146.147.148.149.150.151.152.153.154.155.156.157.158.159.160.161.162.163.164.165.166.167.168.169.170.171.172.173.174.175.176.177.178.179.180.181.182.183.184.
原理一眼看懂用一个 <canvas> 承担图片渲染,轻量且原生用户选择本地图片,加载到固定尺寸的画布里;拖动裁剪框或其缩放手柄,实时改变裁剪区域。绿框与手柄提供直观的视觉反馈;布局简单,移动端/桌面端都能顺利操作。点击按钮后,按比例把画布坐标映射到原图像素,把裁剪结果绘到预览画布,并生成 Data URL 触发下载——全程不触服务器重点回顾纯 HTML/CSS/JS 即可完成:上传 → 裁剪 → 预览 → 下载canvas.drawImage() + 比例换算是关键交互只需拖动移动单点缩放两种模式,逻辑清晰、易扩展

把它塞进你的下一个小项目里试试吧。

THE END