效果图
功能
可以拖动,缩放,旋转,用来做AI商品覆盖的
代码
写了我两天,勾股定理啥的都忘的差不多了,DeepSeek也不能生成我直接用的,不过没DeepSeek我是写不出来,这个主要就是画圆,算角度,半径啥的,没时间写具体的看代码吧。
// 上次的有bug,这次的改了,画了几个圆用于直观计算,不用可以去掉
<template>
<div class="smear-container">
<div class="canvas-container">
<canvas ref="brushCanvas" class="brush-canvas"></canvas>
<canvas
ref="topCanvasRef"
class="top-canvas"
@mousedown="startDrawing"
@mousemove="moveDraw"
@mouseup="stopDrawing"
@mouseleave="leaveDrawing"
></canvas>
</div>
<div class="action-buttons">
<el-button class="public-btn-style" @click="saveCanvas">保存</el-button>
<el-button class="public-btn-style" @click="close">关闭</el-button>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { canvasUtil } from "@/utils/canvas-util";
import { uploadFile } from "@/api/service/upload";
import rotateSvg from "./rotate.svg";
// import bgImg from "./bj.png"
// import spImg from "./sp.png"
const props = defineProps({
canvasImage: {
type: String,
default: "",
},
modelImage: {
type: String,
default: "",
},
imgW: {
// 产品初始大小 宽
type: Number || null,
default: null,
},
imgH: {
// 产品初始大小 高
type: Number || null,
default: null,
},
});
// 设置笔刷大小
const brushActive = ref("smear"); // smear - 涂抹 eraser - 橡皮擦
const setBrushActive = (active) => {
brushActive.value = active;
};
const brushSize = ref(30);
const isDrawing = ref(false);
const brushCanvas = ref(null);
const canvasContext = ref(null);
const topCanvasContext = ref(null);
const maskData = ref(null);
const originalImageData = ref(null);
const reset = () => {
brushActive.value = "smear";
maskData.value = null;
originalImageData.value = null;
};
const topCanvasRef = ref(null);
const backgroundImg = ref(null);
const foregroundImg = ref(null);
const drawImage = ref(null);
// 初始画图片
const loadImg = () => {
if (!(props.canvasImage && props.modelImage)) {
return ElMessage.warning("请先上传原始产品与替换产品图");
}
backgroundImg.value = new Image();
foregroundImg.value = new Image();
drawImage.value = new Image();
backgroundImg.value.onload =
foregroundImg.value.onload =
drawImage.value.onload =
() => {};
backgroundImg.value.src = props.canvasImage;
foregroundImg.value.src = props.modelImage;
drawImage.value.src = rotateSvg;
setTimeout(() => {
initCanvas();
}, 100);
};
// 图片旋转缩放参数
const imgState = ref({
x: 0, // 旋转中心点X
y: 0, // 旋转中心点Y
scale: 1, // 缩放比例
rotation: 0, // 旋转角度
isDragging: false, // 是否拖动
isRotating: false, // 是否旋转
isScale: false, // 是否缩放
dragStartX: 0, // 拖动距离x
dragStartY: 0, // 拖动距离Y
controlPoints: [
// 控制点
{ x: 0, y: 0, index: 0, handleRadius: 10 }, // 右下
{ x: 0, y: 0, index: 1, handleRadius: 10 }, // 左下
{ x: 0, y: 0, index: 2, handleRadius: 10 }, // 左上
{ x: 0, y: 0, index: 3, handleRadius: 10 }, // 右上
],
});
// 缩放旋转图片宽高
let imgW = 0;
let imgH = 0;
let catchImgW = 0;
let catchImgH = 0;
// 初始缩放比例
let imgScale = 1;
// 相对于图片的拖动距离,用来传给后端的
let dragX = 0;
let dragY = 0;
// 初始中心点,用来计算拖动距离
let initCenterX = 0;
let initCenterY = 0;
// 初始化画布
const initCanvas = () => {
console.log("初始化");
// 第一个canvas 只做背景
const canvas = brushCanvas.value;
// 第二个canvas 只做涂抹
const topCanvas = topCanvasRef.value;
const img = backgroundImg.value;
const img2 = foregroundImg.value;
// console.log("初始化画布", canvas, img);
if (!canvas || !img) return;
// 设置画布尺寸与图片相同
canvas.width = img.width;
canvas.height = img.height;
topCanvas.width = img.width;
topCanvas.height = img.height;
// console.log('img2', img, img2, img.width, img2.height);
// 获取画布上下文
// const ctx = canvas.getContext("2d");
canvasContext.value = canvas.getContext("2d", { willReadFrequently: true });
topCanvasContext.value = topCanvas.getContext("2d", {
willReadFrequently: true,
});
const ctx = canvasContext.value;
const topCtx = topCanvasContext.value;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
topCtx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制原始图片
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 覆盖产品高宽
imgScale = canvas.height / foregroundImg.value.width > 3 ? 1 : 0.35;
imgW = props.imgW ? props.imgW : foregroundImg.value.width * imgScale;
imgH = props.imgH ? props.imgH : (img2.height * imgW) / img2.width;
catchImgW = imgW;
catchImgH = imgH;
// 中心点
const x = canvas.width / 2;
const y = canvas.height / 2;
initCenterX = x;
initCenterY = y;
imgState.value = {
x: x, // 旋转中心点X
y: y, // 旋转中心点Y
scale: 1, // 缩放比例
rotation: 0, // 旋转角度
isDragging: false, // 是否拖动
isRotating: false, // 是否旋转
isScale: false, // 是否缩放
dragStartX: 0, // 拖动距离x
dragStartY: 0, // 拖动距离Y
controlPoints: [
// 控制点
{ x: 0, y: 0, index: 0, handleRadius: 10 }, // 右下
{ x: 0, y: 0, index: 1, handleRadius: 10 }, // 左下
{ x: 0, y: 0, index: 2, handleRadius: 10 }, // 左上
{ x: 0, y: 0, index: 3, handleRadius: 10 }, // 右上
],
// initialDistance: 0
};
// console.log(imgW, imgH);
// 覆盖产品起点
const startX = x - imgW / 2;
const startY = y - imgH / 2;
// 测试画圆
ctx.beginPath();
ctx.arc(x, y, 60 + imgW / 2, 0, Math.PI * 2);
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(x, y, imgW / 2, 0, Math.PI * 2);
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
ctx.stroke();
const dx = imgW / 2;
const dy = imgH / 2;
const distance = Math.sqrt(dx * dx + dy * dy);
ctx.beginPath();
ctx.arc(x, y, distance, 0, Math.PI * 2);
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
ctx.stroke();
// 测试画圆
topCtx.drawImage(img2, startX, startY, catchImgW, catchImgH);
drawBorderBox();
drawRotationHandle();
// 如果已有覆盖数据,恢复它
if (maskData.value) {
topCtx.putImageData(maskData.value, 0, 0);
}
};
// 绘制旋转控制圈
function drawRotationHandle() {
const handleRadius = 15; // 旋转控制圈的半径
const handleDistance = 60 + imgH / 2; // 旋转控制圈与图片之间的距离
const imgStateV = imgState.value;
const angleRadians = (imgStateV.rotation * Math.PI) / 180;
// 计算旋转控制圈的位置
const handleX = imgStateV.x + Math.cos(angleRadians) * handleDistance;
const handleY = imgStateV.y + Math.sin(angleRadians) * handleDistance;
const lineX = imgStateV.x + (Math.cos(angleRadians) * imgW) / 2;
const lineY = imgStateV.y + (Math.sin(angleRadians) * imgW) / 2;
const topctx = topCanvasContext.value;
// 绘制连接线
topctx.beginPath();
topctx.moveTo(lineX, lineY);
topctx.lineTo(handleX, handleY);
topctx.strokeStyle = "rgba(52, 152, 219, 1)";
topctx.lineWidth = 2;
topctx.stroke();
// 绘制旋转控制圈那个圈与图片
drawImageInCircle(handleX, handleY, handleRadius);
// 保存控制圈位置用于点击检测
imgStateV.rotationHandle = {
x: handleX,
y: handleY,
radius: handleRadius,
};
}
// 绘制旋转控制圈那个圈与图片
function drawImageInCircle(centerX, centerY, circleRadius) {
const imageScale = 0.7;
// 计算图片在圆形内的尺寸
const imgSize = circleRadius * 2 * imageScale;
const imgX = centerX - imgSize / 2;
const imgY = centerY - imgSize / 2;
const topctx = topCanvasContext.value;
// 使用裁剪路径确保图片只在圆形内显示
topctx.save();
topctx.beginPath();
topctx.arc(centerX, centerY, circleRadius, 0, Math.PI * 2);
topctx.fillStyle = "rgba(52, 152, 219, 1)";
topctx.fill();
topctx.strokeStyle = "white";
topctx.lineWidth = 2;
topctx.stroke();
topctx.clip();
// 绘制图片
// drawWithDrawImage(imgX, imgY, imgSize, imgSize);
// 绘制旋转控制圈中间的图片
topctx.drawImage(drawImage.value, imgX, imgY, imgSize, imgSize);
topctx.restore();
}
// 绘制商品边框
function drawBorderBox() {
const handleRadius = 10; // 边框上的点半径
const dx = imgW / 2;
const dy = imgH / 2;
// 计算半径
const distance = Math.sqrt(dx * dx + dy * dy);
const imgStateV = imgState.value;
// 算矩形的四个角角度
const radians1 = Math.atan2(-dx, dy) * (180 / Math.PI) + 90;
const radians2 = Math.atan2(dx, dy) * (180 / Math.PI) + 90;
const radians3 = Math.atan2(dx, -dy) * (180 / Math.PI) + 90;
const radians4 = Math.atan2(-dx, -dy) * (180 / Math.PI) + 90;
// console.log('radians', radians1, radians2, radians3, radians4)
const rotation = [
imgStateV.rotation + radians1,
imgStateV.rotation + radians2,
imgStateV.rotation + radians3,
imgStateV.rotation + radians4,
];
const topctx = topCanvasContext.value;
// const angleRadians = (imgStateV.rotation * Math.PI) / 180;
for (let i = 0; i < 4; i++) {
const angleRadians = (rotation[i] * Math.PI) / 180;
// 边框的4个点
const x1 = imgStateV.x + Math.cos(angleRadians) * distance;
const y1 = imgStateV.y + Math.sin(angleRadians) * distance;
imgStateV.controlPoints[i] = {
...imgStateV.controlPoints[i],
x: x1,
y: y1,
};
// 绘制4条线
// console.log(x1, y1);
if (i === 0) {
topctx.beginPath();
topctx.moveTo(x1, y1);
} else if (i === 3) {
topctx.lineTo(x1, y1);
topctx.strokeStyle = "rgba(52, 152, 219, 1)";
topctx.lineWidth = 2;
topctx.closePath();
topctx.stroke();
} else {
topctx.lineTo(x1, y1);
}
}
for (let i = 0; i < 4; i++) {
const angleRadians = (rotation[i] * Math.PI) / 180;
const handleX = imgStateV.x + Math.cos(angleRadians) * distance;
const handleY = imgStateV.y + Math.sin(angleRadians) * distance;
// 绘制4个点
topctx.beginPath();
topctx.arc(handleX, handleY, handleRadius, 0, Math.PI * 2);
topctx.fillStyle = "white";
topctx.fill();
}
}
// 检测是否点击在图片上
const isPointInImage = (x, y) => {
const imgStateV = imgState.value;
// 计算旋转后的图片边界
const halfWidth = (imgW * imgStateV.scale) / 2;
const halfHeight = (imgH * imgStateV.scale) / 2;
// 将点转换到图片的局部坐标系
const dx = x - imgStateV.x;
const dy = y - imgStateV.y;
// 应用反向旋转
const cos = Math.cos(-imgStateV.rotation);
const sin = Math.sin(-imgStateV.rotation);
const localX = dx * cos - dy * sin;
const localY = dx * sin + dy * cos;
// 检查点是否在图片边界内
return Math.abs(localX) <= halfWidth && Math.abs(localY) <= halfHeight;
};
// 检测是否点击在旋转控制圈上
function isPointInRotationHandle(x, y) {
if (!imgState.value.rotationHandle) return false;
const dx = x - imgState.value.rotationHandle.x;
const dy = y - imgState.value.rotationHandle.y;
// x*x + y*y <= r*r
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= imgState.value.rotationHandle.radius;
}
// 检测是否点击边框上的点,进行缩放
function isPointInScaleHandle(x, y) {
const obj = {
isIn: false,
index: -1,
};
for (let i = 0; i < 4; i++) {
const point = imgState.value.controlPoints[i];
const dx = point.x - x;
const dy = point.y - y;
// console.log(point.x, x, point.y, y, "点");
if (dx * dx + dy * dy <= point.handleRadius * point.handleRadius) {
obj.isIn = true;
obj.index = i;
break;
}
}
return obj;
}
// canvas css与本身的宽高不一致统一一下,方便计算
function windowToCanvas(x, y, rect) {
// 获取Canvas的边界矩形
const topCanvas = topCanvasRef.value;
// 计算缩放比例
const scaleX = topCanvas.width / rect.width;
const scaleY = topCanvas.height / rect.height;
// 转换为Canvas坐标
const canvasX = (x - rect.left) * scaleX;
const canvasY = (y - rect.top) * scaleY;
return { x: canvasX, y: canvasY };
}
let dragTarget = null;
let oppositeCorner = null;
let lastX = -1;
let lastY = -1;
let zsx = 0;
let zsy = 0;
// 开始绘制
const startDrawing = (e) => {
const rect = topCanvasRef.value.getBoundingClientRect();
const { x, y } = windowToCanvas(e.clientX, e.clientY, rect);
const imgStateV = imgState.value;
if (isPointInRotationHandle(x, y)) {
console.log("开始旋转");
// 开始旋转
imgStateV.isRotating = true;
const angleRadians = (imgStateV.rotation * Math.PI) / 180;
// 记录旋转起始角度
imgStateV.rotationStartAngle =
Math.atan2(y - imgStateV.y, x - imgStateV.x) - angleRadians;
return;
}
const { isIn, index } = isPointInScaleHandle(x, y);
if (isIn) {
// 确定对角点(固定点)
oppositeCorner = imgStateV.controlPoints[(index + 2) % 4];
dragTarget = {
x: x,
y: y,
index,
};
lastX = x;
lastY = y;
console.log("开始缩放");
imgStateV.isScale = true;
return;
}
if (isPointInImage(x, y)) {
console.log("开始移动");
imgStateV.isDragging = true;
imgStateV.dragStartX = x - imgStateV.x;
imgStateV.dragStartY = y - imgStateV.y;
return;
}
};
// 绘制函数 - 更新为创建透明区域
const moveDraw = (e) => {
if (
!(
imgState.value.isDragging ||
imgState.value.isRotating ||
imgState.value.isScale
)
)
return;
const canvas = topCanvasRef.value;
// const topctx = topCanvasContext.value;
// 获取鼠标相对于画布的位置
const rect = canvas.getBoundingClientRect();
const { x, y } = windowToCanvas(e.clientX, e.clientY, rect);
const imgStateV = imgState.value;
if (imgStateV.isDragging) {
const dx = imgState.value.x - initCenterX;
const dy = imgState.value.y - initCenterY;
// 移动不能超过50%
if (Math.abs(dx / canvas.width) > 0.5) {
imgStateV.x = dx < 0 ? 0 : canvas.width;
imgStateV.y = y - imgStateV.dragStartY;
topDraw();
// 这里不赋值stop那取不到
dragX = imgStateV.x - initCenterX;
dragY = imgStateV.y - initCenterY;
imgState.value.isDragging = false;
} else if (Math.abs(dy / canvas.height) > 0.5) {
imgStateV.y = dy < 0 ? 0 : canvas.height;
imgStateV.x = x - imgStateV.dragStartX;
topDraw();
// 这里不赋值stop那取不到
dragX = imgStateV.x - initCenterX;
dragY = imgStateV.y - initCenterY;
imgState.value.isDragging = false;
} else {
// 更新图片位置
imgStateV.x = x - imgStateV.dragStartX;
imgStateV.y = y - imgStateV.dragStartY;
topDraw();
}
} else if (imgStateV.isRotating) {
// 计算新的旋转角度
// console.log('旋转中')
const angle = Math.atan2(y - imgStateV.y, x - imgStateV.x);
imgStateV.rotation =
(angle - imgStateV.rotationStartAngle) * (180 / Math.PI);
topDraw();
} else if (imgStateV.isScale) {
// console.log("缩放中");
// 计算新图片尺寸
let dx = x - lastX;
let dy = y - lastY;
// 判断缩放方向
if (Math.abs(dx) > Math.abs(dy)) {
if (dragTarget.x > imgStateV.x) {
imgW = Math.max(50, imgW + dx * 2);
} else {
imgW = Math.max(50, imgW + dx * 2 * -1);
}
imgH = imgW * (catchImgH / catchImgW);
} else {
if (dragTarget.y > imgStateV.y) {
imgH = Math.max(50, imgH + dy * 2);
} else {
imgH = Math.max(50, imgH + dy * 2 * -1);
}
imgW = imgH * (catchImgW / catchImgH);
}
const r = Math.sqrt(imgW * imgW + imgH * imgH) / 2;
let radians;
// 计算对应点的旋转角度
switch (dragTarget.index) {
case 0:
radians = Math.atan2(-imgW / 2, imgH / 2) * (180 / Math.PI) + 90;
break;
case 1:
radians = Math.atan2(imgW / 2, imgH / 2) * (180 / Math.PI) + 90;
break;
case 2:
radians = Math.atan2(imgW / 2, -imgH / 2) * (180 / Math.PI) + 90;
break;
case 3:
radians = Math.atan2(-imgW / 2, -imgH / 2) * (180 / Math.PI) + 90;
break;
}
const angleRadians = ((imgStateV.rotation + radians) * Math.PI) / 180;
// 计算缩放后的中心点
zsx = oppositeCorner.x + Math.cos(angleRadians) * r;
zsy = oppositeCorner.y + Math.sin(angleRadians) * r;
imgStateV.x = zsx;
imgStateV.y = zsy;
// 缩放比例 更据最原始的图片计算
imgStateV.scale = imgW / catchImgW;
// 保存最后移动的坐标,用于缩放, 缩放为一个个像素加减
lastX = x;
lastY = y;
topDraw();
}
};
// 绘制函数
function topDraw() {
const canvas = topCanvasRef.value;
const ctx = topCanvasContext.value;
// 清除Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
const imgStateV = imgState.value;
// 绘制背景图片
// ctx.drawImage(backgroundImg, 0, 0, canvas.width, canvas.height);
// 保存当前状态
ctx.save();
// 移动到图片中心
ctx.translate(imgStateV.x, imgStateV.y);
// 应用旋转
ctx.rotate((imgStateV.rotation * Math.PI) / 180);
// console.log('重新绘制', imgStateV.scale);
// 应用缩放
ctx.scale(imgStateV.scale, imgStateV.scale);
// console.log('重新绘制');
// 绘制前景图片(以中心为基准)
const fImg = foregroundImg.value;
// console.log('fImg', catchImgW, catchImgH);
ctx.drawImage(fImg, -catchImgW / 2, -catchImgH / 2, catchImgW, catchImgH);
// 恢复状态
ctx.restore();
drawBorderBox();
// 绘制旋转控制圈
drawRotationHandle();
// 画个圆测试一下点
// ctx.beginPath();
// ctx.arc(zsx, zsy, 10, 0, Math.PI * 2);
// ctx.fillStyle = "red";
// ctx.fill();
}
// 停止绘制
const stopDrawing = (e) => {
if (imgState.value.isDragging) {
dragX = imgState.value.x - initCenterX;
dragY = imgState.value.y - initCenterY;
}
imgState.value.isDragging = false;
imgState.value.isRotating = false;
imgState.value.isScale = false;
};
const leaveDrawing = (e) => {
if (imgState.value.isDragging) {
dragX = imgState.value.x - initCenterX;
dragY = imgState.value.y - initCenterY;
}
imgState.value.isDragging = false;
imgState.value.isRotating = false;
imgState.value.isScale = false;
};
const saveCanvas = async () => {
const canvas = topCanvasRef.value;
// 图片宽高
const w = canvas.width;
const h = canvas.height;
// 相当图片移动距离 初始 50 %
const x = (dragX / w).toFixed(2) * 100;
const y = (dragY / h).toFixed(2) * 100;
console.log(w, h, dragX, dragY, x, y);
// 给后端的参数
const data = {
scale: imgScale * imgState.value.scale, // 缩放比例
rotate: imgState.value.rotate, // 旋转角度
dragStartX: x, // 拖动距离x 按百分比算
dragStartY: y, // 拖动距离y 按百分比算
};
// 后面优化用,现在先不弄,保存图片移动后的数据
// maskData.value = topCanvasContext.value.getImageData(0, 0, w, h)
emits("okPanel", data);
// console.log(data)
};
const getCanvas = () => {
// 创建一个新的画布来生成遮罩图像
const tempCanvas = document.createElement("canvas");
tempCanvas.width = brushCanvas.value.width;
tempCanvas.height = brushCanvas.value.height;
const tempCtx = tempCanvas.getContext("2d");
// 放置原始图像
tempCtx.drawImage(brushCanvas.value, 0, 0);
// 放置遮罩
tempCtx.globalAlpha = 0.7;
tempCtx.drawImage(topCanvasRef.value, 0, 0);
tempCtx.globalAlpha = 1.0;
return tempCanvas;
};
const getMaskFile = async () => {
const tempCanvas = getCanvas();
// canvas 转换为文件对象
const res = await canvasUtil.getFileObjectFromCanvas(tempCanvas);
const formData = new FormData();
formData.append("files", res);
// 上传获取文件url
const urlData = await uploadFile(formData);
const urls = urlData.data.replace(/[\[\]]/g, "").split(",");
return urls[0];
};
const emits = defineEmits(["close", "okPanel"]);
const close = () => {
emits("close");
};
defineExpose({
initCanvas,
reset,
maskData,
getMaskFile,
loadImg,
});
</script>
<style lang="scss" scoped>
.smear-container {
display: flex;
flex-direction: column;
height: 100%;
.canvas-container {
width: 100%;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.brush-canvas {
max-width: 100%;
max-height: 100%;
object-fit: contain;
cursor: crosshair;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.top-canvas {
max-width: 100%;
max-height: 100%;
object-fit: contain;
cursor: crosshair;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0.7;
}
}
}
.top-btn {
// text-align: right;
display: flex;
justify-content: flex-end;
align-items: center;
.brush-size {
width: 240px;
display: flex;
align-items: center;
padding-right: 16px;
gap: 10px;
}
.brush-size-title {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
flex: none;
// margin-right: 10px;
}
}
.top-btn :deep(.el-button) {
background-color: transparent;
border-color: $public-tab-color;
color: $public-tab-color;
&:hover {
// color: $public-btn-bg;
// border-color: $public-btn-bg;
background-color: rgba(106, 139, 254, 0.2);
}
&.active {
border-color: $public-btn-bg;
color: $public-btn-bg;
}
}
.action-buttons {
display: flex;
gap: 12px;
}
</style>