开发工作中,常常会遇到需要对图片进行二次处理的一些功能,常见的就有上传头像时,需要对选择的图片进行二次裁剪,包括截图时,也可以对截取的图片继续进行一些操作。常用到的工具网站切图编辑器大致就长这样。
但全用画布去绘画的话,难免工作量会很大。于是我引入cropper.js插件,配合canvas们就可以轻松实现图中的所有功能。以下是代码。
页面引入
<link rel="stylesheet" href="https://unpkg.com/cropperjs/dist/cropper.css">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="https://unpkg.com/cropperjs/dist/cropper.js"></script>
html
<div class="container">
<canvas id="panl-canvas" width="150" height="300" style="display: none;"></canvas>
<div class="upload-box">
<!-- 回显上传图片 -->
<div class="upload-img-box">
<img src="" alt="">
</div>
<div class="upload-btn">
<button type="button" class="btn-box">choose image</button>
<input id="select-box" type="file" style="display: none;" />
</div>
</div>
<!-- 裁剪图片弹窗 -->
<div class="module-cropper" style="display: none;">
<div class="module-cropper-content">
<div class="module-cropper-bg">
<!-- 包装图像或画布元素 -->
<div class="cropper-img-box">
<img id="cropperImg" src="" />
</div>
</div>
</div>
</div>
</div>
<div class="module-cropper-btn">
<button id="rotate90" onclick="rotateCropper(90)">90度旋转</button>
<button id="rotateHor" onclick="flipCropperH()">水平翻转</button>
<button id="rotateVer" onclick="flipCropperV()">垂直翻转</button>
<button id="rotateFree" onclick="$('#panl-canvas').toggle()">自由旋转</button>
<button id="cropFree" class="crop-btn" data-value="">自由裁剪</button>
<button id="crop1_1" class="crop-btn">1:1</button>
<button id="crop2_3" class="crop-btn">2:3</button>
<button id="crop3_2" class="crop-btn">3:2</button>
<button id="crop3_4" class="crop-btn">3:4</button>
<button id="crop4_3" class="crop-btn">4:3</button>
<button id="crop9_16" class="crop-btn">9:16</button>
<button id="crop16_9" class="crop-btn">16:9</button>
<button id="exportButton" onclick="cropperSucess()">导出</button>
</div>
css
<style>
* {
margin: 0;
padding: 0;
}
.container {
width: 500px;
height: 500px;
margin: 50px auto;
position: relative;
}
#panl-canvas {
position: absolute;
left: -200px;
top: 0;
}
.upload-box {
width: 100%;
overflow: hidden;
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
}
.upload-img-box {
width: 80%;
margin: 20px auto;
padding: 20px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 5px;
}
.upload-img-box img {
width: 100%;
}
.upload-btn {
width: 80%;
margin: 0 auto;
overflow: hidden;
text-align: center;
}
.btn-box {
width: 120px;
background: #2DCEC2;
line-height: 35px;
text-align: center;
color: #fff;
border-radius: 5px;
border: none;
outline-style: none;
}
.module-cropper {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background: rgba(0, 0, 0, .8);
}
#cropperImg {
max-width: 100%;
}
.module-cropper-content {
width: 100%;
height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.module-cropper-bg {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.cropper-img-box {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
}
.module-cropper-btn {
width: 100%;
display: flex;
align-items: center;
margin-top: 100px;
}
.module-cropper-btn button {
width: 120px;
height: 50px;
background: #2DCEC2;
line-height: 50px;
text-align: center;
color: #fff;
border-radius: 5px;
border: none;
outline-style: none;
margin-left: 20px;
}
</style>
js
<script>
const image = document.getElementById('cropperImg');// 包装图像或画布元素
let options = {
aspectRatio: NaN, // 裁剪框的宽高比,默认NAN,可以随意改变裁剪框的宽高比
viewMode: 0, // 0,1,2,3
dragMode: 'move', // 'crop': 可以产生一个新的裁剪框 'move': 只可以移动 'none': 什么也不处理
// preview:".small", // 添加额外的元素(容器)以供预览
responsive: true, //在调整窗口大小的时候重新渲染cropper,默认为true
restore: true, // 调整窗口大小后恢复裁剪的区域。
checkCrossOrigin: true, //检查当前图像是否为跨域图像,默认为true
modal: true, // 显示图片上方的黑色模态并在裁剪框下面,默认为true
guides: true, // 显示在裁剪框里面的虚线,默认为true
center: true, // 裁剪框在图片正中心,默认为true
highlight: true, // 在裁剪框上方显示白色的区域,默认为true
background: true, // 显示容器的网格背景(即马赛克背景),默认为true,若为false,这不显示
autoCrop: true, // 当初始化时,显示裁剪框,改成false裁剪框消失需要你重绘裁剪区域,默认为true
autoCropArea: 1, // 定义自动裁剪面积大小(百分比)和图片进行对比,默认为0.8
movable: true, // 是否允许可以移动后面的图片,默认为true(但是如果dragMode为crop,由于和重绘裁剪框冲突,所以移动图片会失效)
rotatable: true, // 是否允许旋转图像,默认为true
scalable: true, // 是否允许缩放图像,默认为true
zoomable: true, // 是否允许放大图像,默认为true
zoomOnTouch: true, // 是否可以通过拖动触摸来放大图像,默认为true
wheelZoomRatio: 0.1, // 用鼠标移动图像时,定义缩放比例,默认0.1
cropBoxMovable: true, // 是否通过拖拽来移动剪裁框,默认为true
cropBoxResizable: true, // 是否通过拖动来调整剪裁框的大小,默认为true
toggleDragModeOnDblclick: true, // 当点击两次时可以在“crop”和“move”之间切换拖拽模式,默认为true
crop: function (event) {
// let zoom = calculateZoom(-(rotation / Math.PI) * 180)
// console.log(zoom, 'zoomzoomzoomzoom');
// cropper.zoomTo(zoom)
}
}
let cropper = new Cropper(image, options); // 初始化cropper对象
//选择图片
$(".btn-box").click(function () {
$('#select-box').click()
})
// input事件
$('#select-box').on('change', function (e) {
let file = e.target.files[0];
let reader = new FileReader();
reader.onload = function (evt) {
let replaceSrc = evt.target.result;
// 更换cropper的图片
cropper.replace(replaceSrc, false);
}
reader.readAsDataURL(file);
$(".module-cropper").show();
})
// 旋转图片
function rotateCropper(deg) {
cropper.rotate(deg);
}
// 水平翻转
function flipCropperH() {
cropper.scaleX(-cropper.getData().scaleX);
console.log(cropper.getCanvasData(), 'cropper.getCanvasData()cropper.getCanvasData()');
}
// 垂直翻转
function flipCropperV() {
cropper.scaleY(-cropper.getData().scaleY);
}
// 图片选择完成
function cropperSucess() {
let baseSrc = cropper.getCroppedCanvas().toDataURL('image/jpeg', 0.7);
$(".module-cropper").hide();
$(".upload-img-box").find("img").attr("src", baseSrc)
// 创建一个隐藏的链接元素
const link = document.createElement('a');
link.href = baseSrc;
link.download = 'cropped_image.jpg';
link.click();
URL.revokeObjectURL(link.href);
}
//比例裁剪
$('.crop-btn').on('click', function () {
const str = $(this).attr('id');
const match = str.match(/\d+/g);
if (match && match.length >= 2) {
const firstNumber = match[0]; // 第一个数字
const secondNumber = match[1]; // 第二个数字
console.log(firstNumber, secondNumber);
const aspectRatio = firstNumber / secondNumber;
cropper.setAspectRatio(aspectRatio);
} else {
cropper.setAspectRatio(NaN);
}
})
//自由旋转
const canvas = document.getElementById("panl-canvas")
let height = canvas.height
let width = canvas.width
let graduateWidth = 10 // 刻度的长度
let ctx = canvas.getContext('2d')
let reqFrame = requestAnimationFrame || setTimeout
let allRotate = 0 // 旋转弧度
const circleRadius = 300 // 大圆半径
const circleBorderDistance = 70 // 距离边上距离
const circlePoint = {
x: width - circleRadius - circleBorderDistance,
y: height / 2
}
function calcPoint(r, a) {
x1 = 0 + r * Math.cos(a) // 0表示圆心位置
y1 = 0 + r * Math.sin(a)
return [x1, y1]
}
function drawTick(ctx) {
let maxDeg = 2 * Math.PI
let tick = maxDeg / 18
ctx.beginPath()
ctx.lineCap = "round"
for (let item = 0; item < 18; item++) {
if (allRotate > 0 && [6, 7, 8].indexOf(item) !== -1) {
continue
} else if (allRotate < 0 && [10, 11, 12].indexOf(item) !== -1) {
continue
}
ctx.save()
ctx.translate(circlePoint.x, circlePoint.y);
ctx.rotate(allRotate)
let deg = tick * item
let innerline = circleRadius - graduateWidth
ctx.strokeStyle = '#aaa'
let start = calcPoint(innerline, deg) // 刻度开始点
let end = calcPoint(circleRadius, deg) // 刻度结束点
ctx.moveTo(...start)
ctx.lineTo(...end)
ctx.restore()
}
ctx.stroke()
}
function drawText(ctx) {
let maxDeg = 2 * Math.PI
let tick = maxDeg / 18
let outerline = height * 0.8 / 2
const textPoint = { // 默认0的位置
x: circleRadius - 30,
y: 4
}
ctx.beginPath()
ctx.fillStyle = '#000';
for (let item = 0; item < 18; item++) {
ctx.save()
ctx.translate(circlePoint.x, circlePoint.y);
ctx.rotate(tick * item + allRotate)
ctx.strokeStyle = '#000'
let num = item * 20 > 180 ? item * 20 - 360 : item * 20
if (allRotate >= 0) {
num = num === 180 ? -180 : num
num < 60 && (ctx.fillText(num, textPoint.x, textPoint.y))
} else if (allRotate < 0) {
num > -60 && (ctx.fillText(num, textPoint.x, textPoint.y))
}
ctx.restore()
}
ctx.stroke()
}
function drawCircles(ctx) {
ctx.beginPath();
ctx.arc(circlePoint.x, circlePoint.y, circleRadius, 0, 2 * Math.PI); // 边框圆
ctx.stroke();
ctx.beginPath();
ctx.arc(circlePoint.x + circleRadius, circlePoint.y, 5, 0, 2 * Math.PI); // 当前值的圆
ctx.fill();
}
function initPanl(ctx, width, height) {
ctx.clearRect(0, 0, width, height)
drawTick(ctx) // 刻度
drawText(ctx) // 数字
drawCircles(ctx) // 两个圆
}
let lastRotateValue = 0; // 记录上一次的角度值
canvas.addEventListener('mousedown', event => {
let startX = event.clientX;
let startY = event.clientY;
const windowMouseMoveFun = e => {
const distanceY = (e.clientY - startY) / 5; // 除5控制表盘滑动快慢
const rotateValue = (allRotate / Math.PI) * 180;
allRotate += distanceY * 2 * Math.PI / 360;
allRotate = Math.min(Math.PI, Math.max(-Math.PI, allRotate)); // 限制角度范围在 -π 到 π 之间
reqFrame(initPanl.bind(null, ctx, width, height));
startY = e.clientY;
// 计算角度差值
const currentRotateValue = -(allRotate / Math.PI) * 180;
const angleDifference = currentRotateValue - lastRotateValue;
//
if (!image) return;
rotation = allRotate; // 设置角度
rotateCropper(angleDifference); // 应用角度变化
let zoom = calculateZoom(-(rotation / Math.PI) * 180)
// 1. 获取实际图像尺寸
const imageData = cropper.getImageData();
const actualWidth = imageData.naturalWidth;
const actualHeight = imageData.naturalHeight;
// 2. 获取显示尺寸(例如容器的宽度和高度)
const containerWidth = cropper.getContainerData().width;
const containerHeight = cropper.getContainerData().height;
// 3. 根据实际尺寸和显示尺寸计算缩放比例
const zoomRatioW = containerWidth / actualWidth;
const zoomRatioH = containerHeight / actualHeight;
const FinalZoom = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH
// 4. 更新 Cropper 的缩放
cropper.zoomTo(zoom * FinalZoom)
lastRotateValue = currentRotateValue; // 更新上一次的角度值
}
const windowMouseUpFun = e => {
window.removeEventListener('mousemove', windowMouseMoveFun);
window.removeEventListener('mouseup', windowMouseUpFun);
}
window.addEventListener('mousemove', windowMouseMoveFun);
window.addEventListener('mouseup', windowMouseUpFun);
});
initPanl(ctx, width, height)
function calculateZoom(angle) {
// 计算绝对值
const absAngle = Math.abs(angle);
// 将角度限制在0到90度之间
const clampedAngle = Math.max(0, Math.min(absAngle, 180));
const mappedAngle = clampedAngle <= 90 ? clampedAngle : 90 - (clampedAngle - 90);
return mappedAngle / 90 + 1;
}
</script>
附上cropper.js官方地址首页 | Cropper.js
(第一次发表文章,欢迎吐槽,多有不足,一起进步)