1098 lines
27 KiB
Plaintext
1098 lines
27 KiB
Plaintext
<template>
|
||
<view class="t-cropper" v-show="props.imageUrl" >
|
||
<!-- #ifdef MP-WEIXIN || MP-ALIPAY-->
|
||
<canvas
|
||
type="2d"
|
||
canvas-id="canvas-cropper"
|
||
id="canvas-cropper"
|
||
class="canvas"
|
||
></canvas>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifdef APP-PLUS || H5 -->
|
||
<canvas
|
||
:canvas-id="canvasId"
|
||
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
||
></canvas>
|
||
<!-- #endif -->
|
||
<!-- 裁剪区域 -->
|
||
<view class="t-preview-container" :class="{ showPage: inInit }">
|
||
<view
|
||
class="preview-body"
|
||
@touchstart="(e) => touchStart(e, 'body')"
|
||
@touchmove="touchMove"
|
||
@touchend="touchEnd"
|
||
@touchcancel="touchCancel"
|
||
>
|
||
<!-- 完整背景图片 -->
|
||
<view
|
||
class="image-wrap"
|
||
:class="{ transit: transit }"
|
||
:style="comImageStyle('image-wrap')"
|
||
>
|
||
<image
|
||
class="image"
|
||
:class="{ transit: transit }"
|
||
:src="imageUrl"
|
||
@load="imageLoad"
|
||
:style="comImageStyle('image')"
|
||
/>
|
||
</view>
|
||
<view class="mask-model"></view>
|
||
<view
|
||
class="frame-box"
|
||
:class="{ transit: transit }"
|
||
:style="comImageStyle('frame-box')"
|
||
>
|
||
<view class="rect" :style="cirStyle">
|
||
<!-- 裁剪框图片 -->
|
||
<view
|
||
class="image-rect"
|
||
:class="{ transit: transit }"
|
||
:style="comImageStyle('image-rect')"
|
||
>
|
||
<image
|
||
class="rect-img"
|
||
:class="{ transit: transit }"
|
||
:src="imageUrl"
|
||
:style="comImageStyle('image')"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<!-- 矩阵框线条 -->
|
||
<view v-if="props.radius < 50" class="line-box">
|
||
<view class="line-one"></view>
|
||
<view class="line-two"></view>
|
||
<view class="line-three"></view>
|
||
<view class="line-four"></view>
|
||
</view>
|
||
<view
|
||
class="frame-left-top"
|
||
@touchstart.stop="(e) => touchStart(e, 'left-top')"
|
||
></view>
|
||
<view
|
||
class="frame-left-bottom"
|
||
@touchstart.stop="(e) => touchStart(e, 'left-bottom')"
|
||
></view>
|
||
<view
|
||
class="frame-right-top"
|
||
@touchstart.stop="(e) => touchStart(e, 'right-top')"
|
||
></view>
|
||
<view
|
||
class="frame-right-bottom"
|
||
@touchstart.stop="(e) => touchStart(e, 'right-bottom')"
|
||
></view>
|
||
</view>
|
||
</view>
|
||
<!-- 底部工具栏 -->
|
||
<view class="toolbar">
|
||
<view @tap.stop="onCancle" class="btn-cancel">取消</view>
|
||
<view
|
||
v-if="props.isRotateBtn && props.mode !== 'free'"
|
||
@tap.stop="onAngle"
|
||
class="btn-rotate"
|
||
>
|
||
<image src="../../static/svg/rotate.svg" data-type="inverse" />
|
||
</view>
|
||
<view @tap.stop="onConfirm" class="btn-confirm">确定</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref, watchEffect, computed, getCurrentInstance, toRefs } from "vue";
|
||
|
||
/**
|
||
* @property {String} mode 模式
|
||
* @value fixed 固定模式,裁剪出固定大小
|
||
* @value ratio 等比模式,宽高等比缩放
|
||
* @value free 自由模式,不限制宽高比
|
||
* @property {String} imageUrl 图片路径
|
||
* @property {Number} width 宽度
|
||
* @property {Number} height 高度
|
||
* @property {Number} maxWidth 最大宽度
|
||
* @property {Number} minHeight 最大高度
|
||
* @property {Number} scaleRatio 裁剪比列缩放
|
||
* @property {Number} minRatio 最小缩放
|
||
* @property {Number} maxRatio 最大缩放
|
||
* @property {Boolean} isRotateBtn 是否显示旋转
|
||
* @property {Number} radius 裁剪图片圆角半径,单位px
|
||
* @property {Number} delay 快速重复点击时间
|
||
*/
|
||
const props = defineProps({
|
||
mode: {
|
||
type: String,
|
||
default: "ratio",
|
||
},
|
||
imageUrl: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
width: {
|
||
type: Number,
|
||
default: 200,
|
||
},
|
||
height: {
|
||
type: Number,
|
||
default: 200,
|
||
},
|
||
maxWidth: {
|
||
type: Number,
|
||
default: 1024,
|
||
},
|
||
maxHeight: {
|
||
type: Number,
|
||
default: 1024,
|
||
},
|
||
scaleRatio: {
|
||
type: Number,
|
||
default: 0.7,
|
||
},
|
||
minRatio: {
|
||
type: Number,
|
||
default: 1,
|
||
},
|
||
maxRatio: {
|
||
type: Number,
|
||
default: 3,
|
||
},
|
||
isRotateBtn: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
radius: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
delay: {
|
||
type: Number,
|
||
default: 250,
|
||
},
|
||
});
|
||
|
||
// 获取组件实例
|
||
const instance = getCurrentInstance();
|
||
const inInit = ref(false);
|
||
const $emit = defineEmits(["confirm", "cancel"]);
|
||
const canvasId = ref(Math.random().toString(36).slice(-6));
|
||
|
||
//获取原始图片的大小
|
||
const origin = reactive({
|
||
imageUrl: "", //设置图片url
|
||
real: {
|
||
//原始图片宽高
|
||
width: 100,
|
||
height: 100,
|
||
},
|
||
body: {
|
||
//页面宽高
|
||
width: 100,
|
||
height: 100,
|
||
},
|
||
frame: {
|
||
//矩形框
|
||
left: 50,
|
||
top: 50,
|
||
width: 200,
|
||
height: 300,
|
||
},
|
||
bgImage: {
|
||
//背景框
|
||
left: 20,
|
||
top: 20,
|
||
width: 300,
|
||
height: 400,
|
||
},
|
||
rotate: 0,
|
||
transit: false,
|
||
canvasWidth: 100,
|
||
canvasHeight: 100,
|
||
frameSize: 150, //裁剪框最小尺寸
|
||
});
|
||
|
||
const { transit, rotate, imageUrl, canvasWidth, canvasHeight } = toRefs(origin);
|
||
|
||
//监听原始图片的变动
|
||
watchEffect(() => {
|
||
if (props.imageUrl) {
|
||
uni.showLoading({
|
||
title: "请稍候...",
|
||
mask: true,
|
||
});
|
||
origin.imageUrl = props.imageUrl; //设置图片url}
|
||
}
|
||
});
|
||
|
||
//检测图片加载完成
|
||
const imageLoad = (e) => {
|
||
const { width, height } = e.detail;
|
||
origin.real.width = width;
|
||
origin.real.height = height;
|
||
var query = uni.createSelectorQuery().in(instance);
|
||
query
|
||
.select(".preview-body")
|
||
.boundingClientRect((data) => {
|
||
origin.body.width = data.width;
|
||
origin.body.height = data.height;
|
||
inInit.value = true;
|
||
imageReset(); //重置图片
|
||
})
|
||
.exec();
|
||
};
|
||
|
||
//重置图片
|
||
const imageReset = () => {
|
||
origin.rotate = 0;
|
||
let frameRate = props.width / props.height; //裁剪比列
|
||
let frameHeight = origin.body.height * props.scaleRatio; //裁剪框图片高度 * 缩小0.7倍
|
||
let frameWidth = origin.body.width * props.scaleRatio; //裁剪框图片宽度 * 缩小0.7倍
|
||
|
||
// 裁剪框的位置:
|
||
// 缩放后的宽度/高度 > 组件裁剪的比例,就要对宽度重写
|
||
if (frameWidth / frameHeight > frameRate) {
|
||
frameWidth = frameHeight * frameRate;
|
||
} else {
|
||
frameHeight = frameWidth / frameRate;
|
||
}
|
||
|
||
// 裁剪框左边距=页面宽度-裁剪框宽度 / 2
|
||
let frameleft = (origin.body.width - frameWidth) / 2;
|
||
let frameTop = (origin.body.height - frameHeight) / 2;
|
||
origin.frame = {
|
||
left: frameleft,
|
||
top: frameTop,
|
||
width: frameWidth,
|
||
height: frameHeight,
|
||
};
|
||
|
||
// 背景图片位置:
|
||
let bgRate = origin.real.width / origin.real.height; //背景比列
|
||
let bgWidth = frameWidth;
|
||
let bgHeight = frameHeight;
|
||
|
||
// 裁剪框图片宽度/裁剪框图片高度>实际的图片比例
|
||
if (bgWidth / bgHeight > bgRate) {
|
||
bgHeight = bgWidth / bgRate;
|
||
} else {
|
||
bgWidth = bgHeight * bgRate;
|
||
}
|
||
|
||
// 背景左边距
|
||
let bgLeft = (frameWidth - bgWidth) / 2 + origin.frame.left;
|
||
// 背景右边距
|
||
let bgTop = (frameHeight - bgHeight) / 2 + origin.frame.top;
|
||
|
||
origin.bgImage = {
|
||
left: bgLeft,
|
||
top: bgTop,
|
||
width: bgWidth,
|
||
height: bgHeight,
|
||
};
|
||
uni.hideLoading();
|
||
};
|
||
|
||
//公共图片背景位置
|
||
const comImageStyle = computed(() => (source) => {
|
||
const { left, top, width, height } = origin.bgImage;
|
||
if (source == "image-wrap") {
|
||
//计算背景盒子样式
|
||
return `
|
||
top: ${top}px;
|
||
left: ${left}px;
|
||
width: ${width}px;
|
||
height: ${height}px;
|
||
`;
|
||
} else if (source == "image") {
|
||
// 计算背景图片样式
|
||
let left = 0;
|
||
let top = 0;
|
||
let width = origin.bgImage.width;
|
||
let height = origin.bgImage.height;
|
||
|
||
if (origin.rotate % 180 != 0) {
|
||
width = origin.bgImage.height;
|
||
height = origin.bgImage.width;
|
||
top = width / 2 - height / 2;
|
||
left = height / 2 - width / 2;
|
||
}
|
||
const style = {};
|
||
style.left = left + "px";
|
||
style.top = top + "px";
|
||
style.width = width + "px";
|
||
style.height = height + "px";
|
||
style.transform = `rotate(${origin.rotate}deg)`;
|
||
return style;
|
||
} else if (source == "image-rect") {
|
||
// 计算裁剪框上显示的图片位置
|
||
return `
|
||
top: ${top - origin.frame.top}px;
|
||
left: ${left - origin.frame.left}px;
|
||
width: ${width}px;
|
||
height: ${height}px;
|
||
`;
|
||
} else if (source == "frame-box") {
|
||
return `
|
||
left: ${origin.frame.left}px;
|
||
top: ${origin.frame.top}px;
|
||
width: ${origin.frame.width}px;
|
||
height: ${origin.frame.height}px;
|
||
`;
|
||
}
|
||
});
|
||
|
||
//计算圆角比列
|
||
const cirStyle = computed(() => {
|
||
const { width } = origin.frame;
|
||
let scale = props.width / width;
|
||
let radius = props.radius / scale;
|
||
return `
|
||
border-radius: ${radius}px
|
||
`;
|
||
});
|
||
|
||
// 取消按钮
|
||
const onCancle = () => {
|
||
$emit("cancel");
|
||
};
|
||
// 旋转按钮
|
||
const onAngle = () => {
|
||
origin.rotate -= 90; // 旋转的角度
|
||
let width = origin.bgImage.height; // 背景框宽度
|
||
let height = origin.bgImage.width; // 背景框高度
|
||
let left = origin.bgImage.left; // 背景框左边距
|
||
let top = origin.bgImage.top; // 背景框顶边距
|
||
let fWidth = origin.frame.width; // 裁剪框宽度
|
||
let fLeft = origin.frame.left; // 裁剪框左边距
|
||
let fTop = origin.frame.top; // 裁剪框顶边距
|
||
// 左边距 = 矩形框左边距 + (背景框顶部 - 裁剪框顶部)
|
||
left = fLeft + (top - fTop);
|
||
// 顶部边距 = 矩形框顶边距 -(背景框高度 - 矩形框宽度 -(矩形框左边距 - 背景框左边距))
|
||
top = fTop - (height - fWidth - (fLeft - origin.bgImage.left));
|
||
|
||
origin.bgImage = {
|
||
left: left,
|
||
top: top,
|
||
width: width,
|
||
height: height,
|
||
};
|
||
origin.transit = true;
|
||
setTimeout(() => {
|
||
origin.transit = false;
|
||
}, 300);
|
||
};
|
||
|
||
/**
|
||
* @name:防抖
|
||
* @param {function} fn
|
||
* @param {wait} wait
|
||
* @return {function}
|
||
*/
|
||
const debounce = (fn, wait = 1000) => {
|
||
let time = null;
|
||
return function (...args) {
|
||
if (time) clearTimeout(time);
|
||
time = setTimeout(() => {
|
||
fn.apply(this, args);
|
||
}, wait);
|
||
};
|
||
};
|
||
// 确定按钮
|
||
const onConfirm = debounce(async () => {
|
||
// #ifdef H5 || APP-PLUS
|
||
confirmH5();
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||
confirmWx();
|
||
// #endif
|
||
}, props.delay);
|
||
|
||
// h5裁剪确认
|
||
const confirmH5 = async () => {
|
||
let mx = computeMatrix(); //获取画布的信息
|
||
// 设备画布的宽高
|
||
origin.canvasWidth = mx.tw;
|
||
origin.canvasHeight = mx.th;
|
||
uni.showLoading({
|
||
title: "处理中",
|
||
});
|
||
|
||
// 设置宽高后等待更新完成在获取Canvas节点
|
||
await new Promise((resolve) => {
|
||
setTimeout(() => {
|
||
resolve();
|
||
}, 200);
|
||
});
|
||
|
||
// 创建一个画布
|
||
let ctx = uni.createCanvasContext(canvasId.value, instance);
|
||
// 矩形内清除指定的像素
|
||
await ctx.clearRect(0, 0, mx.tw, mx.th);
|
||
//剪切圆角
|
||
await drawClipImage(ctx, mx);
|
||
// 旋转
|
||
ctx.rotate((origin.rotate * Math.PI) / 180);
|
||
//绘制图片
|
||
ctx.drawImage(props.imageUrl, mx.sx, mx.sy, mx.sw, mx.sh, mx.dx, mx.dy, mx.dw, mx.dh);
|
||
ctx.restore();
|
||
|
||
ctx.draw(false, () => {
|
||
uni.canvasToTempFilePath(
|
||
{
|
||
canvasId: canvasId.value,
|
||
success: (res) => {
|
||
var path = res.tempFilePath;
|
||
// #ifdef H5
|
||
path = parseBlob(path);
|
||
$emit("confirm", {
|
||
errMsg: "parseBlob:ok",
|
||
tempFilePath: path,
|
||
});
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
$emit("confirm", res);
|
||
// #endif
|
||
},
|
||
fail: (err) => {
|
||
console.log(err);
|
||
},
|
||
complete: () => {
|
||
uni.hideLoading(); //关闭loading
|
||
},
|
||
},
|
||
instance
|
||
);
|
||
});
|
||
};
|
||
// 微信确认
|
||
const confirmWx = () => {
|
||
uni.showLoading({
|
||
title: "处理中",
|
||
});
|
||
let mx = computeMatrix(); //获取画布的信息
|
||
uni
|
||
.createSelectorQuery()
|
||
// #ifndef MP-ALIPAY
|
||
.in(instance)
|
||
// #endif
|
||
.select("#canvas-cropper")
|
||
.fields({
|
||
node: true,
|
||
size: true,
|
||
})
|
||
.exec(async (res) => {
|
||
const textCanvas = res[0].node;
|
||
const ctx = textCanvas.getContext("2d");
|
||
|
||
// 初始化画布大小
|
||
textCanvas.width = mx.tw;
|
||
textCanvas.height = mx.th;
|
||
// //绘制canvas
|
||
await drawClipImage(ctx, mx); //剪切圆角
|
||
await ctx.rotate((origin.rotate * Math.PI) / 180);
|
||
await createImage(origin.imageUrl, mx, ctx, textCanvas);
|
||
|
||
// #ifdef MP-WEIXIN
|
||
wx.canvasToTempFilePath({
|
||
canvas: textCanvas,
|
||
canvasId: "canvasID",
|
||
success: (res) => {
|
||
$emit("confirm", res);
|
||
},
|
||
fail: (err) => {
|
||
console.log(err);
|
||
},
|
||
});
|
||
// #endif
|
||
|
||
// #ifdef MP-ALIPAY
|
||
textCanvas.toTempFilePath({
|
||
success: (res) => {
|
||
$emit("confirm", res);
|
||
},
|
||
fail: (err) => {
|
||
console.log(err);
|
||
},
|
||
});
|
||
// #endif
|
||
|
||
uni.hideLoading(); //关闭loading
|
||
});
|
||
};
|
||
//剪切图片圆角
|
||
const drawClipImage = (ctx, mx) => {
|
||
let { radius } = props;
|
||
if (radius > 0) {
|
||
const w = Math.round(mx.tw); //画布宽度
|
||
const h = Math.round(mx.th); //画布高度
|
||
|
||
// 被缩放后的尺寸且裁剪的宽高相同
|
||
if (props.width === props.height && w == h && props.width < w) {
|
||
radius = (w / props.width) * radius;
|
||
}
|
||
// 被缩放后的尺寸且裁剪的宽高不相同
|
||
if (props.width != props.height && w != h && props.width < w) {
|
||
console.log("11111111");
|
||
if (w > h) {
|
||
radius = (w / h) * radius;
|
||
} else {
|
||
radius = (h / w) * radius;
|
||
}
|
||
}
|
||
// 如果裁剪是圆形
|
||
if (w === h && radius >= w / 2) {
|
||
// 圆形
|
||
ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
|
||
} else {
|
||
// 圆角矩形
|
||
if (w !== h) {
|
||
// 限制圆角半径不能超过短边的一半
|
||
radius = Math.min(w / 2, h / 2, radius);
|
||
}
|
||
// 其他形状
|
||
ctx.moveTo(radius, 0);
|
||
ctx.arcTo(w, 0, w, h, radius);
|
||
ctx.arcTo(w, h, 0, h, radius);
|
||
ctx.arcTo(0, h, 0, 0, radius);
|
||
ctx.arcTo(0, 0, w, 0, radius);
|
||
ctx.closePath();
|
||
}
|
||
ctx.clip(); //从原始画布剪切任意形状和尺寸的区域
|
||
ctx.restore(); //返回之前保存过的路径状态和属性
|
||
}
|
||
};
|
||
// 创建一个图片
|
||
const createImage = (img, mx, ctx, textCanvas) => {
|
||
return new Promise((resolve) => {
|
||
const headerImg = textCanvas.createImage();
|
||
headerImg.src = img;
|
||
headerImg.onload = () => {
|
||
let result = ctx.drawImage(
|
||
headerImg,
|
||
mx.sx,
|
||
mx.sy,
|
||
mx.sw,
|
||
mx.sh,
|
||
mx.dx,
|
||
mx.dy,
|
||
mx.dw,
|
||
mx.dh
|
||
);
|
||
resolve(result);
|
||
};
|
||
});
|
||
};
|
||
|
||
//计算矩阵
|
||
const computeMatrix = () => {
|
||
let width = props.width;
|
||
let height = props.height;
|
||
let mul = origin.bgImage.width / origin.real.width;
|
||
if (origin.rotate % 180 != 0) {
|
||
mul = origin.bgImage.height / origin.real.width;
|
||
}
|
||
if (props.mode != "fixed") {
|
||
width = origin.frame.width / mul;
|
||
height = origin.frame.height / mul;
|
||
}
|
||
var rate = width / height;
|
||
if (width > props.maxWidth) {
|
||
width = props.maxWidth;
|
||
height = width / rate;
|
||
}
|
||
if (height > props.maxHeight) {
|
||
height = props.maxHeight;
|
||
width = height * rate;
|
||
}
|
||
var sx = (origin.frame.left - origin.bgImage.left) / mul;
|
||
var sy = (origin.frame.top - origin.bgImage.top) / mul;
|
||
var sw = origin.frame.width / mul;
|
||
var sh = origin.frame.height / mul;
|
||
var ox = sx + sw / 2;
|
||
var oy = sy + sh / 2;
|
||
if (origin.rotate % 180 != 0) {
|
||
var temp = sw;
|
||
sw = sh;
|
||
sh = temp;
|
||
}
|
||
var angle = origin.rotate % 360;
|
||
if (angle < 0) {
|
||
angle += 360;
|
||
}
|
||
if (angle == 270) {
|
||
var x = origin.real.width - oy;
|
||
var y = ox;
|
||
ox = x;
|
||
oy = y;
|
||
}
|
||
if (angle == 180) {
|
||
var x = origin.real.width - ox;
|
||
var y = origin.real.height - oy;
|
||
ox = x;
|
||
oy = y;
|
||
}
|
||
if (angle == 90) {
|
||
var x = oy;
|
||
var y = origin.real.height - ox;
|
||
ox = x;
|
||
oy = y;
|
||
}
|
||
sx = ox - sw / 2;
|
||
sy = oy - sh / 2;
|
||
let dr = { x: 0, y: 0, w: width, h: height };
|
||
dr = parseRect(dr, -origin.rotate);
|
||
return {
|
||
tw: width,
|
||
th: height,
|
||
sx: sx,
|
||
sy: sy,
|
||
sw: sw,
|
||
sh: sh,
|
||
dx: dr.x,
|
||
dy: dr.y,
|
||
dw: dr.w,
|
||
dh: dr.h,
|
||
};
|
||
};
|
||
// 计算矩阵
|
||
const parseRect = (rect, angle) => {
|
||
let x1 = rect.x;
|
||
let y1 = rect.y;
|
||
let x2 = rect.x + rect.w;
|
||
let y2 = rect.y + rect.h;
|
||
let p1 = parsePoint({ x: x1, y: y1 }, angle);
|
||
let p2 = parsePoint({ x: x2, y: y2 }, angle);
|
||
let result = {};
|
||
result.x = Math.min(p1.x, p2.x);
|
||
result.y = Math.min(p1.y, p2.y);
|
||
result.w = Math.abs(p2.x - p1.x);
|
||
result.h = Math.abs(p2.y - p1.y);
|
||
return result;
|
||
};
|
||
// 计算x、y
|
||
const parsePoint = (point, angle) => {
|
||
var result = {};
|
||
result.x =
|
||
point.x * Math.cos((angle * Math.PI) / 180) -
|
||
point.y * Math.sin((angle * Math.PI) / 180);
|
||
result.y =
|
||
point.y * Math.cos((angle * Math.PI) / 180) +
|
||
point.x * Math.sin((angle * Math.PI) / 180);
|
||
return result;
|
||
};
|
||
// base64转blob
|
||
const parseBlob = (base64) => {
|
||
var arr = base64.split(",");
|
||
var mime = arr[0].match(/:(.*?);/)[1];
|
||
var bstr = atob(arr[1]);
|
||
var n = bstr.length;
|
||
var u8arr = new Uint8Array(n);
|
||
for (var i = 0; i < n; i++) {
|
||
u8arr[i] = bstr.charCodeAt(i);
|
||
}
|
||
var url = URL || webkitURL;
|
||
return url.createObjectURL(new Blob([u8arr], { type: mime }));
|
||
};
|
||
/**
|
||
* 绘制手指触摸
|
||
*/
|
||
const move = reactive({
|
||
touchType: "body", //移动的类型
|
||
start: {
|
||
//开始的位置
|
||
frame: {
|
||
left: 0,
|
||
top: 0,
|
||
width: 0,
|
||
height: 0,
|
||
},
|
||
bgImage: {
|
||
left: 0,
|
||
top: 0,
|
||
width: 0,
|
||
height: 0,
|
||
},
|
||
},
|
||
});
|
||
|
||
const touch = reactive({
|
||
touchStart: [], //记录开始触摸
|
||
});
|
||
|
||
// 手指触摸动作开始
|
||
const touchStart = (e, touchType = "") => {
|
||
// #ifdef APP-PLUS || H5
|
||
if (e.preventDefault) {
|
||
e.preventDefault();
|
||
}
|
||
if (e.stopPropagation) {
|
||
e.stopPropagation();
|
||
}
|
||
// #endif
|
||
|
||
switch (touchType) {
|
||
case "body":
|
||
move.touchType = "body";
|
||
break;
|
||
case "left-top":
|
||
move.touchType = "left-top";
|
||
break;
|
||
case "left-bottom":
|
||
move.touchType = "left-bottom";
|
||
break;
|
||
case "right-top":
|
||
move.touchType = "right-top";
|
||
break;
|
||
case "right-bottom":
|
||
move.touchType = "right-bottom";
|
||
break;
|
||
default:
|
||
move.touchType = "body";
|
||
break;
|
||
}
|
||
|
||
touch.touchStart = e.touches;
|
||
// 裁剪框的开始位置
|
||
move.start.frame.left = origin.frame.left;
|
||
move.start.frame.top = origin.frame.top;
|
||
move.start.frame.width = origin.frame.width;
|
||
move.start.frame.height = origin.frame.height;
|
||
|
||
// 背景图片的开始位置
|
||
move.start.bgImage.left = origin.bgImage.left;
|
||
move.start.bgImage.top = origin.bgImage.top;
|
||
move.start.bgImage.width = origin.bgImage.width;
|
||
move.start.bgImage.height = origin.bgImage.height;
|
||
return false;
|
||
};
|
||
|
||
// 手指触摸后移动
|
||
const touchMove = (e) => {
|
||
// #ifdef APP-PLUS || H5
|
||
if (e.preventDefault) {
|
||
e.preventDefault();
|
||
}
|
||
if (e.stopPropagation) {
|
||
e.stopPropagation();
|
||
}
|
||
// #endif
|
||
// 单手操作
|
||
if (touch.touchStart.length == 1) {
|
||
if (move.touchType == "body") {
|
||
// 移动图片
|
||
moveImage(touch.touchStart[0], e.touches[0]);
|
||
} else {
|
||
if (props.mode != "fixed") {
|
||
// 放大缩小图片
|
||
scaleFrame(touch.touchStart[0], e.touches[0]);
|
||
}
|
||
}
|
||
} else if (touch.touchStart.length == 2 && e.touches.length == 2) {
|
||
var ta = touch.touchStart[0];
|
||
var tb = touch.touchStart[1];
|
||
var tc = e.touches[0];
|
||
var td = e.touches[1];
|
||
if (ta.identifier != tc.identifier) {
|
||
var temp = tc;
|
||
tc = td;
|
||
td = temp;
|
||
}
|
||
scaleImage(ta, tb, tc, td);
|
||
}
|
||
};
|
||
|
||
// 移动图片
|
||
const moveImage = (ta, tb) => {
|
||
let mx = tb.clientX - ta.clientX;
|
||
let my = tb.clientY - ta.clientY;
|
||
// 开始移动图片背景 + 移动的左边距离
|
||
origin.bgImage.left = move.start.bgImage.left + mx;
|
||
origin.bgImage.top = move.start.bgImage.top + my;
|
||
|
||
let frameLeft = origin.frame.left;
|
||
let frameTop = origin.frame.top;
|
||
let frameWidth = origin.frame.width;
|
||
let frameHeight = origin.frame.height;
|
||
|
||
// 左边触边
|
||
if (origin.bgImage.left > frameLeft) {
|
||
origin.bgImage.left = frameLeft;
|
||
}
|
||
// 头部触边
|
||
if (origin.bgImage.top > frameTop) {
|
||
origin.bgImage.top = frameTop;
|
||
}
|
||
// 右边触边
|
||
if (origin.bgImage.left + origin.bgImage.width < frameLeft + frameWidth) {
|
||
origin.bgImage.left = frameLeft + frameWidth - origin.bgImage.width;
|
||
}
|
||
// 底部触边:背景距离头部+背景的高度 < 背景高度 + 矩形框顶部
|
||
if (origin.bgImage.top + origin.bgImage.height < frameTop + frameHeight) {
|
||
origin.bgImage.top = frameTop + frameHeight - origin.bgImage.height;
|
||
}
|
||
};
|
||
|
||
//缩放裁剪框
|
||
const scaleFrame = (ta, tb) => {
|
||
let mx = tb.clientX - ta.clientX;
|
||
let my = tb.clientY - ta.clientY;
|
||
let x1 = move.start.frame.left;
|
||
let y1 = move.start.frame.top;
|
||
let x2 = move.start.frame.left + move.start.frame.width;
|
||
let y2 = move.start.frame.top + move.start.frame.height;
|
||
let cx1 = false;
|
||
let cy1 = false;
|
||
let cx2 = false;
|
||
let cy2 = false;
|
||
let mix = origin.frameSize;
|
||
let rate = origin.frame.width / origin.frame.height;
|
||
|
||
const { width: fWidth, height: fHeight } = origin.frame;
|
||
const { width: bgWidth, height: bgHeight } = origin.bgImage;
|
||
const { width: realWidth, height: realHeight } = origin.real;
|
||
|
||
//左上角
|
||
if (move.touchType == "left-top") {
|
||
x1 += mx;
|
||
y1 += my;
|
||
cx1 = true;
|
||
cy1 = true;
|
||
// 左下角
|
||
} else if (move.touchType == "left-bottom") {
|
||
x1 += mx;
|
||
y2 += my;
|
||
cx1 = true;
|
||
cy2 = true;
|
||
// 右上角
|
||
} else if (move.touchType == "right-top") {
|
||
x2 += mx;
|
||
y1 += my;
|
||
cx2 = true;
|
||
cy1 = true;
|
||
// 右下角
|
||
} else if (move.touchType == "right-bottom") {
|
||
x2 += mx;
|
||
y2 += my;
|
||
cx2 = true;
|
||
cy2 = true;
|
||
}
|
||
|
||
if (x1 < origin.bgImage.left) {
|
||
x1 = origin.bgImage.left;
|
||
}
|
||
if (y1 < origin.bgImage.top) {
|
||
y1 = origin.bgImage.top;
|
||
}
|
||
if (x2 > origin.bgImage.left + origin.bgImage.width) {
|
||
x2 = origin.bgImage.left + origin.bgImage.width;
|
||
}
|
||
if (y2 > origin.bgImage.top + origin.bgImage.height) {
|
||
y2 = origin.bgImage.top + origin.bgImage.height;
|
||
}
|
||
if (cx1) {
|
||
if (x1 > x2 - mix) {
|
||
x1 = x2 - mix;
|
||
}
|
||
}
|
||
if (cy1) {
|
||
if (y1 > y2 - mix) {
|
||
y1 = y2 - mix;
|
||
}
|
||
}
|
||
if (cx2) {
|
||
if (x2 < x1 + mix) {
|
||
x2 = x1 + mix;
|
||
}
|
||
}
|
||
if (cy2) {
|
||
if (y2 < y1 + mix) {
|
||
y2 = y1 + mix;
|
||
}
|
||
}
|
||
if (cx1) {
|
||
if (props.mode != "free") {
|
||
var val = x2 - rate * (y2 - y1);
|
||
if (x1 < val) {
|
||
x1 = val;
|
||
}
|
||
}
|
||
}
|
||
if (cy1) {
|
||
if (props.mode != "free") {
|
||
var val = y2 - (x2 - x1) / rate;
|
||
if (y1 < val) {
|
||
y1 = val;
|
||
}
|
||
}
|
||
}
|
||
if (cx2) {
|
||
if (props.mode != "free") {
|
||
var val = rate * (y2 - y1) + x1;
|
||
if (x2 > val) {
|
||
x2 = val;
|
||
}
|
||
}
|
||
}
|
||
if (cy2) {
|
||
if (props.mode != "free") {
|
||
var val = (x2 - x1) / rate + y1;
|
||
if (y2 > val) {
|
||
y2 = val;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 裁剪框缩放限制
|
||
const owFrame = x2 - x1;
|
||
const ohFrame = y2 - y1;
|
||
|
||
if (owFrame < fWidth || ohFrame < fHeight) {
|
||
const currentRatio = props.scaleRatio * props.maxRatio;
|
||
const isScale =
|
||
bgWidth * currentRatio >= realWidth / props.scaleRatio &&
|
||
bgHeight * currentRatio >= realHeight / props.scaleRatio;
|
||
if (isScale) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
origin.frame.left = x1;
|
||
origin.frame.top = y1;
|
||
origin.frame.width = x2 - x1;
|
||
origin.frame.height = y2 - y1;
|
||
};
|
||
|
||
//双指缩放图片
|
||
const scaleImage = (ta, tb, tc, td) => {
|
||
let x1 = ta.clientX;
|
||
let y1 = ta.clientY;
|
||
let x2 = tb.clientX;
|
||
let y2 = tb.clientY;
|
||
let x3 = tc.clientX;
|
||
let y3 = tc.clientY;
|
||
let x4 = td.clientX;
|
||
let y4 = td.clientY;
|
||
let ol = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||
let el = Math.sqrt((x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4));
|
||
let ocx = (x1 + x2) / 2;
|
||
let ocy = (y1 + y2) / 2;
|
||
let ecx = (x3 + x4) / 2;
|
||
let ecy = (y3 + y4) / 2;
|
||
let ax = ecx - ocx;
|
||
let ay = ecy - ocy;
|
||
let scale = el / ol;
|
||
|
||
// 检测放大/缩小倍率
|
||
if (move.start.bgImage.width * scale < origin.frame.width) {
|
||
scale = origin.frame.width / move.start.bgImage.width;
|
||
}
|
||
|
||
if (move.start.bgImage.height * scale < origin.frame.height) {
|
||
scale = origin.frame.height / move.start.bgImage.height;
|
||
}
|
||
|
||
// 最大缩放
|
||
if (move.start.bgImage.width * scale > origin.frame.width * props.maxRatio) {
|
||
return;
|
||
}
|
||
|
||
origin.bgImage.left =
|
||
move.start.bgImage.left + ax - (ocx - move.start.bgImage.left) * (scale - 1);
|
||
|
||
origin.bgImage.top =
|
||
move.start.bgImage.top + ay - (ocy - move.start.bgImage.top) * (scale - 1);
|
||
|
||
origin.bgImage.width = move.start.bgImage.width * scale;
|
||
origin.bgImage.height = move.start.bgImage.height * scale;
|
||
|
||
if (origin.bgImage.left > origin.frame.left) {
|
||
origin.bgImage.left = origin.frame.left;
|
||
}
|
||
if (origin.bgImage.top > origin.frame.top) {
|
||
origin.bgImage.top = origin.frame.top;
|
||
}
|
||
if (
|
||
origin.bgImage.left + origin.bgImage.width <
|
||
origin.frame.left + origin.frame.width
|
||
) {
|
||
origin.bgImage.left = origin.frame.left + origin.frame.width - origin.bgImage.width;
|
||
}
|
||
if (
|
||
origin.bgImage.top + origin.bgImage.height <
|
||
origin.frame.top + origin.frame.height
|
||
) {
|
||
origin.bgImage.top = origin.frame.top + origin.frame.height - origin.bgImage.height;
|
||
}
|
||
};
|
||
|
||
// 手指触摸动作结束
|
||
const touchEnd = () => {
|
||
touch.touchStart = [];
|
||
resizeImage(); //调整图片的大小
|
||
};
|
||
|
||
// 手指触摸动作被打断,如来电提醒,弹窗
|
||
const touchCancel = () => {
|
||
touch.touchStart = [];
|
||
};
|
||
|
||
// 调整图片的大小
|
||
const resizeImage = () => {
|
||
var rate = origin.frame.width / origin.frame.height; //比列
|
||
var width = origin.body.width * props.scaleRatio; //裁剪框图片高度 * 缩小0.7倍
|
||
var height = origin.body.height * props.scaleRatio; //裁剪框图片宽度 * 缩小0.7倍
|
||
// 图片的位置:
|
||
// 缩放后的宽度/高度 > 组件裁剪的比例,就要对宽度重写
|
||
if (width / height > rate) {
|
||
width = height * rate;
|
||
} else {
|
||
height = width / rate;
|
||
}
|
||
var left = (origin.body.width - width) / 2;
|
||
var top = (origin.body.height - height) / 2;
|
||
var mul = width / origin.frame.width;
|
||
var ox = origin.frame.left - origin.bgImage.left;
|
||
var oy = origin.frame.top - origin.bgImage.top;
|
||
|
||
origin.frame = {
|
||
left: left,
|
||
top: top,
|
||
width: width,
|
||
height: height,
|
||
};
|
||
|
||
width = origin.bgImage.width * mul;
|
||
height = origin.bgImage.height * mul;
|
||
left = origin.frame.left - ox * mul;
|
||
top = origin.frame.top - oy * mul;
|
||
origin.bgImage = {
|
||
left: left,
|
||
top: top,
|
||
width: width,
|
||
height: height,
|
||
};
|
||
if (mul != 1) {
|
||
origin.transit = true;
|
||
setTimeout(() => {
|
||
origin.transit = false;
|
||
}, 300);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 绘制手指触摸结束
|
||
*/
|
||
</script>
|
||
|
||
<style>
|
||
page {
|
||
overflow: hidden;
|
||
overscroll-behavior: none;
|
||
}
|
||
</style>
|
||
<style lang="scss" scoped>
|
||
@import "./t-cropper";
|
||
</style>
|