<template>
|
<view>
|
<view id="drop-box" class="drop-box" :style="[viewStyle]">
|
<view id="drop-center" class="drop-center" ref="drop-center" :style="[cutStyle]">
|
<view id="wrapper" ref="wrapper" class="image-wrapper" :style="[wrapperStyle]" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd">
|
<view style="display: flex;position: relative;" :style="[imageStyle]" @touchstart="imghandleTouchStart" @touchmove="imghandleTouchMove" @touchend="imghandleTouchEnd">
|
<image :src="imageUrl" :class="imageModel == 'widthFix' ? 'imageWidth' : 'imageHeight' " :mode="imageModel" crossOrigin="Anonymous"></image>
|
<view v-show="showRotateIcon" class="rotate-icon" @touchstart="handleRotateStart" @touchmove="handleRotateMove" @touchend="handleRotateEnd">🔁</view>
|
</view>
|
</view>
|
</view>
|
<view v-show="showMasker" class="drop-box-redio" :style="[circleStyle]"></view>
|
</view>
|
|
|
<text :prop="startStatus" :change:prop="canvas.down"></text>
|
<!-- <view style="margin-top: 100rpx;">
|
<button type="primary" @tap="chooseImage">选择图片</button>
|
<button style="margin-top: 20rpx;" type="primary" @tap="rotateCounterClockwise">旋转</button>
|
<button style="margin-top: 20rpx;" type="primary" @tap="mirrorImage">镜像</button>
|
<button style="margin-top: 20rpx;" type="primary" @click="canvas.down">保存图片到相册</button>
|
</view> -->
|
<!-- <image v-if="tempFilePath" :src="tempFilePath" mode="widthFix" style="width: 300px;height: 300px;"></image> -->
|
|
|
</view>
|
</template>
|
|
<script>
|
import { base64ToPath, pathToBase64 } from './image-tools.js';
|
export default {
|
props: {
|
// 图片路径
|
imageSrc: {
|
type: String,
|
default: '/static/image.jpg'
|
},
|
boxWidth: {
|
type: String,
|
default: '100vw'
|
},
|
boxHeight: {
|
type: String,
|
default: '100vw'
|
},
|
// 透明度
|
opacity: {
|
type: Number,
|
default: 0.4
|
},
|
// 圆的直径
|
circleDiameter: {
|
type: String,
|
default: '345rpx'
|
},
|
// 是否显示遮罩层
|
showMasker: {
|
type: Boolean,
|
default: true
|
},
|
// 是否显示右下角旋转图标
|
showRotateIcon: {
|
type: Boolean,
|
default: true
|
},
|
},
|
data() {
|
return {
|
canRotation: false,
|
onceChange: true,
|
wrapperStyle: {
|
transform: "scale(1) rotate(0deg)",
|
},
|
imageStyle: {
|
transform: "translate(0rpx, 0rpx) scale(1, 1)", // 初始变换样式
|
},
|
imageUrl: '',
|
imageModel: 'widthFix',
|
startDistance: 0, // 记录初始双指触摸时的距离
|
startAngle: 0, // 记录初始双指触摸时的角度
|
initialScale: 1, // 初始缩放比例
|
initialRotation: 0, // 初始旋转角度
|
initialTranslateX: 0, // 初始水平位移
|
initialTranslateY: 0, // 初始垂直位移
|
lastTouch: null, // 记录上一次单指触摸的位置
|
currentScale: 1, // 当前缩放比例
|
currentRotation: 0, // 当前旋转角度
|
currentTranslateX: 0, // 当前水平位移
|
currentTranslateY: 0, // 当前垂直位移
|
isRotating: false, // 标志是否正在进行旋转操作
|
isRotatingWithIcon: false, // 标志是否通过图标进行旋转操作
|
rotateIconStartPos: null, // 记录旋转图标初始触摸位置
|
rotateIconStartAngle: 0, // 记录旋转图标初始触摸角度
|
rotateIconStartDistance: 0, // 记录旋转图标初始触摸距离
|
isMirrored: false, // 标志图片是否被镜像
|
wrapperRect: null, // 存储wrapper元素的边界矩形
|
requestAnimationFrameId: null, // 存储requestAnimationFrame的ID
|
tempFilePath: '', // 用于存储生成的图片路径
|
canvasWidth: 300, // 初始宽度
|
canvasHeight: 300, // 初始高度
|
startStatus: 0,
|
};
|
},
|
onLoad() {
|
|
},
|
async mounted() {
|
this.init()
|
},
|
methods: {
|
init(){
|
this.$nextTick(()=>{
|
let isNet = this.isNetworkImage(this.imageSrc)
|
|
// 本地图片要处理成base64
|
if(isNet){
|
this.imageUrl = this.imageSrc
|
}else{
|
this.turnBase64Image(this.imageSrc)
|
}
|
this.getRect();
|
this.fitImageToCircle(this.imageSrc)
|
}, 0)
|
},
|
isNetworkImage(imageUrl) {
|
// 检查URL是否以http://或https://开头
|
return imageUrl.startsWith('http://') || imageUrl.startsWith('https://');
|
},
|
turnBase64Image(img) {
|
uni.getImageInfo({
|
src: img,
|
success: image => {
|
pathToBase64(image.path)
|
.then(base64 => {
|
this.imageUrl = base64;
|
})
|
.catch(error => {
|
console.log('转换失败:', error);
|
});
|
},
|
fail: err => {
|
console.log('将本地图片转为base 64报错:', err);
|
}
|
});
|
},
|
// 获取宽高
|
getRect() {
|
uni.createSelectorQuery().in(this).select("#wrapper").boundingClientRect((data) => {
|
if (data) {
|
this.wrapperRect = data; // 获取wrapper元素的边界矩形
|
}
|
}).exec();
|
},
|
// 判断图片是横向还是纵向
|
fitImageToCircle(filePath) {
|
uni.getImageInfo({
|
src: filePath,
|
success: (res) => {
|
const imgWidth = res.width;
|
const imgHeight = res.height;
|
const aspectRatio = imgWidth / imgHeight;
|
// 大于1说明是横向的图片,需要把高度撑到圆的直径
|
if (aspectRatio > 1) {
|
this.imageModel = 'heightFix'
|
} else {
|
this.imageModel = 'widthFix'
|
}
|
},
|
fail: (err) => {
|
uni.showToast({
|
title: "加载图片失败",
|
icon: "none",
|
});
|
console.error(err);
|
},
|
});
|
},
|
handleTouchStart(e) {
|
if (e.touches.length === 2) {
|
this.isRotating = true;
|
this.startDistance = this.getDistance(e.touches); // 记录初始双指触摸时的距离
|
this.startAngle = this.getAngle(e.touches); // 记录初始双指触摸时的角度
|
this.initialScale = this.currentScale; // 记录初始缩放比例
|
this.initialRotation = this.currentRotation; // 记录初始旋转角度
|
this.initialTranslateX = this.currentTranslateX; // 记录初始水平位移
|
this.initialTranslateY = this.currentTranslateY; // 记录初始垂直位移
|
}
|
},
|
imghandleTouchStart(e) {
|
if (e.touches.length === 1 && !this.isRotatingWithIcon) {
|
this.lastTouch = e.touches[0]; // 记录上一次单指触摸的位置
|
}
|
},
|
handleTouchMove(e) {
|
if (this.isRotating && e.touches.length === 2) {
|
const currentDistance = this.getDistance(e.touches); // 获取当前双指触摸时的距离
|
const currentAngle = this.getAngle(e.touches); // 获取当前双指触摸时的角度
|
let angleDelta = currentAngle - this.startAngle; // 计算角度差
|
if (angleDelta > 180) {
|
angleDelta -= 360;
|
} else if (angleDelta < -180) {
|
angleDelta += 360;
|
}
|
if(this.isMirrored == 1){ // 镜像操作,左右移动需要调整
|
angleDelta = -angleDelta;
|
}
|
this.currentScale = (currentDistance / this.startDistance) * this.initialScale; // 计算当前缩放比例
|
console.log(angleDelta)
|
// 大于2°才认为旋转了
|
if(Math.abs(angleDelta) > 5){
|
this.canRotation = true
|
if(this.onceChange){
|
this.onceChange = false
|
this.startAngle = this.startAngle + angleDelta; // 记录初始双指触摸时的角度
|
angleDelta = 0; // 计算角度差
|
}
|
}
|
|
|
if(this.canRotation){
|
this.currentRotation = angleDelta + this.initialRotation; // 计算当前旋转角度
|
}
|
|
this.updateTransform(); // 更新变换样式
|
}
|
},
|
imghandleTouchMove(e) {
|
if (e.touches.length === 1 && !this.isRotatingWithIcon &&this.lastTouch) {
|
let dx = e.touches[0].clientX - this.lastTouch.clientX; // 计算水平位移差
|
let dy = e.touches[0].clientY - this.lastTouch.clientY; // 计算垂直位移差
|
// 考虑旋转角度的影响
|
const rotationAngle = this.currentRotation * Math.PI / 180; // 转换为弧度制
|
// 计算旋转矩阵
|
const cos = Math.cos(rotationAngle);
|
const sin = Math.sin(rotationAngle);
|
const rotationMatrix = [
|
[cos, -sin],
|
[sin, cos]
|
];
|
// 计算逆旋转矩阵
|
const inverseRotationMatrix = [
|
[cos, sin],
|
[-sin, cos]
|
];
|
if(this.isMirrored == 1){ // 镜像操作,左右移动需要调整
|
dx = -dx;
|
}
|
// 使用逆旋转矩阵转换dx和dy
|
let transformedDx = dx * inverseRotationMatrix[0][0] + dy * inverseRotationMatrix[0][1];
|
let transformedDy = dx * inverseRotationMatrix[1][0] + dy * inverseRotationMatrix[1][1];
|
// 由于是父级放大,所以移动的时候需要算一下, 放大倍数的差别, 还有旋转的角度
|
this.currentTranslateX += transformedDx / this.currentScale;
|
this.currentTranslateY += transformedDy / this.currentScale; // 更新当前垂直位移
|
this.updateTransform(); // 更新变换样式
|
this.lastTouch = e.touches[0]; // 更新上一次单指触摸的位置
|
}
|
},
|
handleTouchEnd(e) {
|
this.onceChange = true
|
this.canRotation = false
|
if (e.touches.length === 1) {
|
if (this.isRotating) {
|
const remainingTouch = e.touches[0];
|
this.lastTouch = remainingTouch;
|
this.isRotating = false;
|
}
|
}
|
},
|
imghandleTouchEnd(e) {
|
if (e.touches.length === 0) {
|
this.isRotating = false;
|
this.isRotatingWithIcon = false;
|
this.rotateIconStartPos = null;
|
this.lastTouch = null;
|
this.initialScale = this.currentScale; // 记录当前缩放比例
|
this.initialRotation = this.currentRotation; // 记录当前旋转角度
|
this.initialTranslateX = this.currentTranslateX; // 记录当前水平位移
|
this.initialTranslateY = this.currentTranslateY; // 记录当前垂直位移
|
}
|
},
|
handleRotateStart(e) {
|
this.isRotatingWithIcon = true;
|
this.rotateIconStartPos = {
|
x: e.touches[0].clientX,
|
y: e.touches[0].clientY,
|
}; // 记录旋转图标初始触摸位置
|
this.rotateIconStartAngle = this.getAngleFromCenter(
|
this.rotateIconStartPos.x,
|
this.rotateIconStartPos.y
|
); // 记录旋转图标初始触摸角度
|
this.rotateIconStartDistance = this.getDistanceFromCenter(
|
this.rotateIconStartPos.x,
|
this.rotateIconStartPos.y
|
); // 记录旋转图标初始触摸距离
|
this.initialRotation = this.currentRotation; // 记录初始旋转角度
|
this.initialScale = this.currentScale; // 记录初始缩放比例
|
e.stopPropagation();
|
},
|
handleRotateMove(e) {
|
if (this.isRotatingWithIcon && e.touches.length === 1) {
|
const currentPos = {
|
x: e.touches[0].clientX,
|
y: e.touches[0].clientY
|
}; // 获取当前触摸位置
|
const currentAngle = this.getAngleFromCenter(
|
currentPos.x,
|
currentPos.y
|
); // 获取当前触摸角度
|
const currentDistance = this.getDistanceFromCenter(
|
currentPos.x,
|
currentPos.y
|
); // 获取当前触摸距离
|
|
let angleDelta = currentAngle - this.rotateIconStartAngle; // 计算角度差
|
if (angleDelta > 180) {
|
angleDelta -= 360;
|
} else if (angleDelta < -180) {
|
angleDelta += 360;
|
}
|
this.currentRotation = this.initialRotation + angleDelta; // 计算当前旋转角度
|
|
this.currentScale =
|
(currentDistance / this.rotateIconStartDistance) * this.initialScale; // 计算当前缩放比例
|
|
this.updateTransform(); // 更新变换样式
|
e.stopPropagation();
|
}
|
},
|
handleRotateEnd(e) {
|
this.isRotatingWithIcon = false;
|
this.rotateIconStartPos = null;
|
this.initialRotation = this.currentRotation; // 记录当前旋转角度
|
this.initialScale = this.currentScale; // 记录当前缩放比例
|
e.stopPropagation();
|
},
|
mirrorImage() {
|
this.isMirrored = !this.isMirrored; // 切换图片镜像状态
|
this.updateTransform(); // 更新变换样式
|
},
|
updateTransform() {
|
const mirrorScale = this.isMirrored ? -1 : 1; // 根据镜像状态确定缩放比例
|
const transformValue = `translate(${this.currentTranslateX}px, ${this.currentTranslateY}px) scale(${mirrorScale}, 1)`; // 构建变换样式字符串
|
this.imageStyle.transform = transformValue; // 更新图片的变换样式
|
this.wrapperStyle.transform = `scale(${this.currentScale}) rotate(${this.currentRotation}deg)`
|
},
|
getDistance(touches) {
|
const dx = touches[0].clientX - touches[1].clientX; // 计算水平距离差
|
const dy = touches[0].clientY - touches[1].clientY; // 计算垂直距离差
|
return Math.sqrt(dx * dx + dy * dy); // 返回两点之间的距离
|
},
|
getAngle(touches) {
|
const dx = touches[0].clientX - touches[1].clientX; // 计算水平距离差
|
const dy = touches[0].clientY - touches[1].clientY; // 计算垂直距离差
|
return Math.atan2(dy, dx) * (180 / Math.PI); // 返回两点之间的角度
|
},
|
getAngleFromCenter(x, y) {
|
const rect = this.wrapperRect; // 获取wrapper元素的边界矩形
|
const centerX = rect.left + rect.width / 2; // 计算中心点的水平坐标
|
const centerY = rect.top + rect.height / 2; // 计算中心点的垂直坐标
|
const dx = x - centerX; // 计算水平距离差
|
const dy = y - centerY; // 计算垂直距离差
|
return Math.atan2(dy, dx) * (180 / Math.PI); // 返回触摸点与中心点之间的角度
|
},
|
getDistanceFromCenter(x, y) {
|
const rect = this.wrapperRect; // 获取wrapper元素的边界矩形
|
const centerX = rect.left + rect.width / 2; // 计算中心点的水平坐标
|
const centerY = rect.top + rect.height / 2; // 计算中心点的垂直坐标
|
const dx = x - centerX; // 计算水平距离差
|
const dy = y - centerY; // 计算垂直距离差
|
return Math.sqrt(dx * dx + dy * dy); // 返回触摸点与中心点之间的距离
|
},
|
// 其他方法保持不变
|
rotateCounterClockwise() {
|
const currentRotation = this.currentRotation % 360;
|
let targetRotation = currentRotation - (currentRotation % 90) - 90;
|
if (targetRotation < -360) {
|
targetRotation += 360;
|
}
|
this.currentRotation = targetRotation;
|
this.updateTransform();
|
},
|
saveImg(url){
|
this.tempFilePath = url
|
base64ToPath(url).then(path => {
|
this.$emit('sendUrl', {
|
base64: url,
|
filePath: path
|
})
|
})
|
.catch(error => {
|
console.error('临时路径转换出错了:', error);
|
});
|
|
},
|
chooseImage(){
|
uni.chooseImage({
|
count: 1,
|
sourceType: ['album', 'camera'],
|
success: (res) => {
|
const filePath = res.tempFilePaths[0];
|
this.turnBase64Image(filePath, 'imageSrc')
|
},
|
fail: (err) => {
|
console.error('选择图片失败:', err);
|
}
|
});
|
},
|
saveImage(){
|
this.startStatus++
|
}
|
},
|
computed: {
|
circleStyle() {
|
return {
|
width: this.boxWidth,
|
height: this.boxHeight,
|
background: `radial-gradient(circle at center, transparent ${this.circleDiameter}, rgba(0, 0, 0, ${this.opacity}) ${this.circleDiameter})`
|
};
|
},
|
viewStyle() {
|
return {
|
width: this.boxWidth,
|
height: this.boxHeight,
|
};
|
},
|
cutStyle() {
|
return {
|
width: this.circleDiameter * 2,
|
height: this.circleDiameter * 2,
|
};
|
},
|
}
|
};
|
</script>
|
<!-- 新增 renderjs 模块 -->
|
<script module="canvas" lang="renderjs">
|
import html2canvas from './html2canvas.min.js';
|
export default {
|
methods: {
|
down(newValue, oldValue, ownerInstance, instance){
|
let id = ''
|
if(this.showMasker){
|
id = 'drop-center'
|
}else{
|
id = 'wrapper'
|
}
|
const wrapper = document.getElementById('drop-center');
|
if(wrapper == null){
|
return
|
}
|
html2canvas(wrapper, {
|
width: wrapper.clientWidth, //dom 原始宽度
|
height: wrapper.clientHeight,
|
backgroundColor: null,
|
scrollY: 0, // html2canvas默认绘制视图内的页面,需要把scrollY,scrollX设置为0
|
scrollX: 0,
|
useCORS: true, //支持跨域
|
scale: 2, // 设置生成图片的像素比例,默认是1,如果生成的图片模糊的话可以开启该配置项
|
ignoreElements: function(element) {
|
return element.classList.contains('rotate-icon');
|
}
|
}).then(canvas => {
|
var context = canvas.getContext('2d');
|
// 【重要】关闭抗锯齿
|
context.mozImageSmoothingEnabled = false;
|
context.webkitImageSmoothingEnabled = false;
|
context.msImageSmoothingEnabled = false;
|
context.imageSmoothingEnabled = false;
|
|
|
let tempFilePath = canvas.toDataURL('image/png')
|
tempFilePath = tempFilePath.replace(/[]/g, '');
|
ownerInstance.callMethod('saveImg', tempFilePath)
|
});
|
},
|
|
}
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.drop-box {
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-sizing: content-box;
|
z-index: 100;
|
overflow: hidden;
|
}
|
.drop-box-redio {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100vw;
|
height: 100vw;
|
/* 创建一个径向渐变,中心是透明的,周围是半透明的 */
|
// background: radial-gradient(circle at center, transparent 345rpx, rgba(0, 0, 0, 0.4) 345rpx);
|
/* 允许事件穿透,不阻挡拖动 */
|
pointer-events: none;
|
z-index: 101;
|
}
|
.drop-center {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 690rpx;
|
height: 690rpx;
|
border-radius: 50%;
|
overflow: hidden;
|
}
|
.image-wrapper {
|
position: absolute;
|
// top: 0;
|
// left: 0;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
// width: 100vw;
|
// height: 100vw;
|
// transform-origin: center center; /* 确保旋转和缩放以中心为基准 */
|
}
|
|
.imageWidth {
|
width: 690rpx;
|
height: auto;
|
}
|
.imageHeight {
|
width: auto;
|
height: 690rpx;
|
}
|
|
.mirror-icon {
|
position: absolute;
|
top: -15px;
|
left: -15px;
|
background: rgba(255, 255, 255, 0.8);
|
border-radius: 50%;
|
width: 30px;
|
height: 30px;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
cursor: pointer;
|
z-index: 10;
|
/* 确保图标在顶层 */
|
}
|
|
.rotate-icon {
|
position: absolute;
|
bottom: -30rpx;
|
right: -30rpx;
|
background: rgba(255, 255, 255, 0.8);
|
border-radius: 50%;
|
width: 60rpx;
|
height: 60rpx;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
cursor: pointer;
|
z-index: 10;
|
/* 确保图标在顶层 */
|
}
|
</style>
|