446 lines
15 KiB
JavaScript
446 lines
15 KiB
JavaScript
/**
|
||
* 图片裁剪核心库 - View显示版本
|
||
* 使用view和image显示界面,只在导出时使用canvas
|
||
* 支持H5、App、微信小程序、支付宝小程序、抖音小程序
|
||
*/
|
||
|
||
export default class ImageCropper {
|
||
constructor(context,options) {
|
||
|
||
this.context = context;
|
||
this.options = options || {};
|
||
|
||
this.canvasId = options.canvasId;
|
||
this.fileType = options.fileType || 'jpg';
|
||
this.quality = options.quality || 0.8;
|
||
|
||
const { windowWidth, windowHeight } = uni.$u.window();
|
||
|
||
// 画布尺寸(显示区域)
|
||
this.canvasWidth = options.canvasWidth || windowWidth;
|
||
this.canvasHeight = options.canvasHeight || windowHeight;
|
||
|
||
// 输出图片配置
|
||
this.width = options.width || 400;
|
||
this.height = options.height || 400;
|
||
|
||
// 裁剪框配置
|
||
this.rectWidth = options.rectWidth || 400;
|
||
this.rectHeight = options.rectHeight || 400;
|
||
|
||
// 图片数据
|
||
this.imgData = {
|
||
x: 0,
|
||
y: 0,
|
||
width: 0,
|
||
height: 0,
|
||
scale: 1,
|
||
angle: 0,
|
||
originalWidth: 0,
|
||
originalHeight: 0,
|
||
src: ''
|
||
};
|
||
|
||
// 触摸数据
|
||
this.touch = {
|
||
startX: 0,
|
||
startY: 0,
|
||
isTouch: false,
|
||
isMove: false
|
||
};
|
||
|
||
// 图片加载状态
|
||
this.imageLoaded = false;
|
||
|
||
// 回调函数
|
||
this.onUpdate = options.onUpdate || (() => {});
|
||
this.triggerUpdate();
|
||
}
|
||
|
||
// 设置图片源
|
||
setImage(src) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!src) {
|
||
reject(new Error('图片路径不能为空'));
|
||
return;
|
||
}
|
||
|
||
uni.getImageInfo({
|
||
src: src,
|
||
success: (res) => {
|
||
this.imgData.src = src;
|
||
this.imgData.originalWidth = res.width;
|
||
this.imgData.originalHeight = res.height;
|
||
this.imageLoaded = true;
|
||
this.resetImageData();
|
||
this.triggerUpdate();
|
||
resolve(res);
|
||
},
|
||
fail: () => reject(new Error('图片信息获取失败'))
|
||
});
|
||
});
|
||
}
|
||
|
||
// 重置图片数据
|
||
resetImageData() {
|
||
if (!this.imageLoaded) return;
|
||
|
||
const imgWidth = this.imgData.originalWidth;
|
||
const imgHeight = this.imgData.originalHeight;
|
||
|
||
// 计算让图片宽度或高度等于裁剪框尺寸的缩放比例
|
||
const scaleX = this.rectWidth / imgWidth;
|
||
const scaleY = this.rectHeight / imgHeight;
|
||
// 使用较大的缩放比例,确保图片至少有一边等于裁剪框尺寸
|
||
const scale = Math.max(scaleX, scaleY);
|
||
|
||
// 计算裁剪框在画布中的居中位置
|
||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||
|
||
// 保持当前的旋转角度
|
||
const currentAngle = this.imgData.angle || 0;
|
||
|
||
this.imgData = {
|
||
...this.imgData,
|
||
x: rectX + (this.rectWidth - imgWidth * scale) / 2,
|
||
y: rectY + (this.rectHeight - imgHeight * scale) / 2,
|
||
width: imgWidth * scale,
|
||
height: imgHeight * scale,
|
||
scale: scale,
|
||
angle: currentAngle
|
||
};
|
||
|
||
// 如果图片有旋转,需要重新计算边界限制
|
||
if (currentAngle !== 0) {
|
||
this.applyBoundaryConstraints();
|
||
}
|
||
}
|
||
|
||
// 应用边界约束
|
||
applyBoundaryConstraints() {
|
||
// 计算裁剪框在画布中的居中位置
|
||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||
|
||
// 获取旋转后的边界框
|
||
const rotatedBounds = this.getRotatedBounds();
|
||
|
||
// 计算图片中心点相对于裁剪框的偏移范围
|
||
const maxOffsetX = Math.max(0, (rotatedBounds.width - this.rectWidth) / 2);
|
||
const maxOffsetY = Math.max(0, (rotatedBounds.height - this.rectHeight) / 2);
|
||
|
||
// 裁剪框中心点
|
||
const rectCenterX = rectX + this.rectWidth / 2;
|
||
const rectCenterY = rectY + this.rectHeight / 2;
|
||
|
||
// 当前图片中心点
|
||
const imgCenterX = this.imgData.x + this.imgData.width / 2;
|
||
const imgCenterY = this.imgData.y + this.imgData.height / 2;
|
||
|
||
// 限制图片中心点的移动范围
|
||
const limitedCenterX = Math.max(rectCenterX - maxOffsetX, Math.min(rectCenterX + maxOffsetX, imgCenterX));
|
||
const limitedCenterY = Math.max(rectCenterY - maxOffsetY, Math.min(rectCenterY + maxOffsetY, imgCenterY));
|
||
|
||
// 重新计算图片位置
|
||
this.imgData.x = limitedCenterX - this.imgData.width / 2;
|
||
this.imgData.y = limitedCenterY - this.imgData.height / 2;
|
||
}
|
||
|
||
// 计算旋转后图片的边界框
|
||
getRotatedBounds() {
|
||
const { width, height, angle } = this.imgData;
|
||
|
||
if (angle === 0 || angle % 360 === 0) {
|
||
return { width, height };
|
||
}
|
||
|
||
// 将角度转换为弧度
|
||
const rad = (angle * Math.PI) / 180;
|
||
|
||
// 计算旋转后的边界框
|
||
const cos = Math.abs(Math.cos(rad));
|
||
const sin = Math.abs(Math.sin(rad));
|
||
|
||
const rotatedWidth = width * cos + height * sin;
|
||
const rotatedHeight = width * sin + height * cos;
|
||
|
||
return {
|
||
width: rotatedWidth,
|
||
height: rotatedHeight
|
||
};
|
||
}
|
||
|
||
// 触摸开始
|
||
touchStart(e) {
|
||
if (!this.imageLoaded) return;
|
||
|
||
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
||
const y = e.touches ? e.touches[0].clientY : e.clientY;
|
||
|
||
this.touch.startX = x;
|
||
this.touch.startY = y;
|
||
this.touch.isTouch = true;
|
||
this.touch.isMove = false;
|
||
}
|
||
|
||
// 触摸移动
|
||
touchMove(e) {
|
||
if (!this.touch.isTouch || !this.imageLoaded) return;
|
||
|
||
e.preventDefault && e.preventDefault();
|
||
|
||
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
||
const y = e.touches ? e.touches[0].clientY : e.clientY;
|
||
|
||
// 单点拖拽
|
||
const deltaX = x - this.touch.startX;
|
||
const deltaY = y - this.touch.startY;
|
||
|
||
// 计算新的图片位置
|
||
let newX = this.imgData.x + deltaX;
|
||
let newY = this.imgData.y + deltaY;
|
||
|
||
// 计算裁剪框在画布中的居中位置
|
||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||
|
||
// 获取旋转后的边界框
|
||
const rotatedBounds = this.getRotatedBounds();
|
||
|
||
// 计算图片中心点相对于裁剪框的偏移范围
|
||
const maxOffsetX = Math.max(0, (rotatedBounds.width - this.rectWidth) / 2);
|
||
const maxOffsetY = Math.max(0, (rotatedBounds.height - this.rectHeight) / 2);
|
||
|
||
// 裁剪框中心点
|
||
const rectCenterX = rectX + this.rectWidth / 2;
|
||
const rectCenterY = rectY + this.rectHeight / 2;
|
||
|
||
// 限制图片中心点的移动范围
|
||
const imgCenterX = newX + this.imgData.width / 2;
|
||
const imgCenterY = newY + this.imgData.height / 2;
|
||
|
||
const limitedCenterX = Math.max(rectCenterX - maxOffsetX, Math.min(rectCenterX + maxOffsetX, imgCenterX));
|
||
const limitedCenterY = Math.max(rectCenterY - maxOffsetY, Math.min(rectCenterY + maxOffsetY, imgCenterY));
|
||
|
||
// 重新计算图片位置
|
||
newX = limitedCenterX - this.imgData.width / 2;
|
||
newY = limitedCenterY - this.imgData.height / 2;
|
||
|
||
this.imgData.x = newX;
|
||
this.imgData.y = newY;
|
||
|
||
this.touch.startX = x;
|
||
this.touch.startY = y;
|
||
this.touch.isMove = true;
|
||
|
||
this.triggerUpdate();
|
||
}
|
||
|
||
// 触摸结束
|
||
touchEnd(e) {
|
||
this.touch.isTouch = false;
|
||
this.touch.isMove = false;
|
||
}
|
||
|
||
// 缩放图片
|
||
scaleImage(ratio) {
|
||
if (!this.imageLoaded) return;
|
||
|
||
const newScale = this.imgData.scale * ratio;
|
||
|
||
// 计算新的尺寸
|
||
const newWidth = this.imgData.originalWidth * newScale;
|
||
const newHeight = this.imgData.originalHeight * newScale;
|
||
|
||
// 限制最小缩放,确保图片至少有一边等于或大于裁剪框
|
||
const minScaleX = this.rectWidth / this.imgData.originalWidth;
|
||
const minScaleY = this.rectHeight / this.imgData.originalHeight;
|
||
const minScale = Math.max(minScaleX, minScaleY);
|
||
|
||
// 限制缩放范围
|
||
if (newScale < minScale || newScale > 3) return;
|
||
|
||
// 保存当前中心点
|
||
const centerX = this.imgData.x + this.imgData.width / 2;
|
||
const centerY = this.imgData.y + this.imgData.height / 2;
|
||
|
||
// 更新图片数据
|
||
this.imgData.scale = newScale;
|
||
this.imgData.width = newWidth;
|
||
this.imgData.height = newHeight;
|
||
|
||
// 重新计算位置,保持中心点不变
|
||
this.imgData.x = centerX - this.imgData.width / 2;
|
||
this.imgData.y = centerY - this.imgData.height / 2;
|
||
|
||
// 应用边界约束
|
||
this.applyBoundaryConstraints();
|
||
this.triggerUpdate();
|
||
}
|
||
|
||
// 旋转图片
|
||
rotate(angle = 90) {
|
||
if (!this.imageLoaded) return;
|
||
|
||
this.imgData.angle += angle;
|
||
this.imgData.angle = this.imgData.angle % 360;
|
||
|
||
// 旋转后应用边界约束
|
||
this.applyBoundaryConstraints();
|
||
this.triggerUpdate();
|
||
}
|
||
|
||
// 重置图片
|
||
reset() {
|
||
this.resetImageData();
|
||
this.triggerUpdate();
|
||
}
|
||
|
||
loadImage(canvas,src) {
|
||
return new Promise( (resolve, reject) => {
|
||
if (this.options.type == '2d') {
|
||
var img = canvas.createImage();
|
||
img.onload = () => {
|
||
resolve(img);
|
||
};
|
||
img.onerror = (e) => {
|
||
reject(e);
|
||
};
|
||
img.src = src;
|
||
} else {
|
||
resolve(src);
|
||
}
|
||
})
|
||
}
|
||
|
||
canvasToTempFilePath(resolve, reject, canvas) {
|
||
let params = {
|
||
canvas: canvas,
|
||
canvasId: this.canvasId,
|
||
fileType: this.fileType,
|
||
quality: this.quality,
|
||
width: this.width,
|
||
height: this.height,
|
||
success: (res) => {
|
||
resolve(res.tempFilePath)
|
||
},
|
||
fail: (err) => {
|
||
console.error('导出图片失败:', err);
|
||
reject(err);
|
||
}
|
||
};
|
||
// #ifdef MP-ALIPAY
|
||
uni.canvasToTempFilePath(params);
|
||
// #endif
|
||
// #ifndef MP-ALIPAY
|
||
uni.canvasToTempFilePath(params, this.context);
|
||
// #endif
|
||
}
|
||
|
||
// 导出裁剪图片
|
||
getCropperImage() {
|
||
|
||
return new Promise(async (resolve, reject) => {
|
||
if (!this.imageLoaded) {
|
||
reject(new Error('图片未加载'));
|
||
return;
|
||
}
|
||
|
||
// 计算裁剪框在画布中的居中位置
|
||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||
|
||
// 计算图片在裁剪框中的相对位置
|
||
const imgX = this.imgData.x - rectX;
|
||
const imgY = this.imgData.y - rectY;
|
||
|
||
// 计算缩放比例 - 导出图片尺寸 vs 裁剪框尺寸
|
||
const scaleX = this.width / this.rectWidth;
|
||
const scaleY = this.height / this.rectHeight;
|
||
|
||
let canvas = null;
|
||
// #ifdef MP-ALIPAY
|
||
let ctx = uni.createCanvasContext(this.canvasId);
|
||
// #endif
|
||
// #ifndef MP-ALIPAY
|
||
let ctx = uni.createCanvasContext(this.canvasId, this.context);
|
||
// #endif
|
||
|
||
if(this.options.type == '2d'){
|
||
canvas = await new Promise(resolve => {
|
||
uni.createSelectorQuery()
|
||
.in(this.context)
|
||
.select(`#${this.canvasId}`)
|
||
.fields({
|
||
node: true,
|
||
size: true
|
||
})
|
||
.exec(res => {
|
||
resolve(res[0].node);
|
||
});
|
||
});
|
||
|
||
canvas.width = this.width;
|
||
canvas.height = this.height;
|
||
|
||
ctx = canvas.getContext('2d');
|
||
ctx.fillStyle = '#FFFFFF';
|
||
ctx.fillRect(0, 0, this.width, this.height);
|
||
|
||
}else{
|
||
// 清空画布
|
||
ctx.clearRect(0, 0, this.width, this.height);
|
||
}
|
||
|
||
const image = await this.loadImage(canvas,this.imgData.src);
|
||
|
||
// 计算缩放后的图片位置和尺寸
|
||
const scaledImgX = imgX * scaleX;
|
||
const scaledImgY = imgY * scaleY;
|
||
const scaledImgWidth = this.imgData.width * scaleX;
|
||
const scaledImgHeight = this.imgData.height * scaleY;
|
||
|
||
// 绘制图片
|
||
if (this.imgData.angle == 0) {
|
||
ctx.drawImage(image, scaledImgX, scaledImgY, scaledImgWidth, scaledImgHeight);
|
||
} else {
|
||
ctx.save();
|
||
ctx.translate(scaledImgX + scaledImgWidth / 2, scaledImgY + scaledImgHeight / 2);
|
||
ctx.rotate((this.imgData.angle * Math.PI) / 180);
|
||
ctx.drawImage(image, -scaledImgWidth / 2, -scaledImgHeight / 2, scaledImgWidth, scaledImgHeight);
|
||
ctx.restore();
|
||
}
|
||
|
||
if(this.options.type == '2d') {
|
||
this.canvasToTempFilePath(resolve, reject, canvas);
|
||
}else{
|
||
ctx.draw(false, ()=>{
|
||
this.canvasToTempFilePath(resolve, reject);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// 触发更新回调
|
||
triggerUpdate() {
|
||
this.onUpdate({
|
||
imageLoaded: this.imageLoaded,
|
||
imageSrc: this.imgData.src,
|
||
imageData: { ...this.imgData },
|
||
canvasWidth: this.canvasWidth,
|
||
canvasHeight: this.canvasHeight,
|
||
rectWidth: this.rectWidth,
|
||
rectHeight: this.rectHeight
|
||
});
|
||
}
|
||
|
||
// 销毁实例
|
||
destroy() {
|
||
this.imageLoaded = false;
|
||
this.imgData = {};
|
||
this.touch = {};
|
||
this.options = null;
|
||
this.onUpdate = null;
|
||
}
|
||
} |