canvas 一个商品覆盖的组件

30 阅读8分钟

效果图

image.png

功能

可以拖动,缩放,旋转,用来做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>