canvas涂抹,擦除功能组件

198 阅读4分钟

可以透明涂抹,擦除,生成图片

vue写的一个组件,可以透明涂抹,擦除,生成图片,正好项目有这个功能就网上找方法写了一个,做个记录以后也不知道还用不用的的,顺便分享一下

<template>
  <div class="smear-container">
    <div class="top-btn">
      <div class="brush-size">
        <div class="brush-size-title">笔刷大小</div>
        <el-slider
          v-model.number="brushSize"
          :min="5"
          :max="100"
          :step="1"
          input-size="small"
          class="brush-slider"
        />
        <div class="brush-size-title">{{ brushSize }}</div>
      </div>
      <el-button
        size="small"
        :class="{ active: brushActive === 'smear' }"
        plain
        @click="setBrushActive('smear')"
        >涂抹</el-button
      >
      <el-button
        size="small"
        :class="{ active: brushActive === 'eraser' }"
        plain
        @click="setBrushActive('eraser')"
        >橡皮擦</el-button
      >
    </div>
    <div class="canvas-container">
      <canvas ref="brushCanvas" class="brush-canvas"></canvas>

      <canvas ref="topCanvasRef" class="top-canvas"></canvas>
      <canvas
        ref="brushCanvas2"
        @mouseenter="mouseenterDrawing"
        @mousedown="startDrawing"
        @mousemove="draw"
        @mouseup="stopDrawing"
        @mouseleave="leaveDrawing"
        class="brush-canvas"
      ></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 { uploadSingleFile } from "@/api/service/upload.js";

const props = defineProps({
  canvasImage: {
    type: Object,
    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 brushCanvas2 = ref(null);
const brushCanvasTwoCtx = ref(null);
// 初始化画布
const initCanvas = () => {
  // 第一个canvas 只做背景
  const canvas = brushCanvas.value;
  const brushCanvasTwo = brushCanvas2.value;
  // 第二个canvas 只做涂抹
  const topCanvas = topCanvasRef.value;
  const img = props.canvasImage;
  // console.log("初始化画布", canvas, img);
  if (!canvas || !img) return;

  // 设置画布尺寸与图片相同
  canvas.width = img.width;
  canvas.height = img.height;
  topCanvas.width = img.width;
  topCanvas.height = img.height;
  brushCanvasTwo.width = img.width;
  brushCanvasTwo.height = img.height;

  // 获取画布上下文
  // const ctx = canvas.getContext("2d");
  canvasContext.value = canvas.getContext("2d", { willReadFrequently: true });
  topCanvasContext.value = topCanvas.getContext("2d", {
    willReadFrequently: true,
  });
  brushCanvasTwoCtx.value = brushCanvasTwo.getContext("2d");
  brushCanvasTwoCtx.value.clearRect(0, 0, canvas.width, canvas.height);

  const ctx = canvasContext.value;
  const topCtx = topCanvasContext.value;
  // 清空画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 绘制原始图片
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

  // 保存涂抹原始图像数据
  if (!originalImageData.value) {
    originalImageData.value = topCanvasContext.value.getImageData(
      0,
      0,
      canvas.width,
      canvas.height
    );
  }

  // 如果已有涂抹数据,恢复它
  if (maskData.value) {
    topCtx.putImageData(maskData.value, 0, 0);
  }
};
const clear2 = () => {
  const canvas = brushCanvas2.value;
  const ctx = brushCanvasTwoCtx.value;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
};
const createCircle = (x, y) => {
  // if (isDrawing.value) {
  const canvas = brushCanvas2.value;
  const ctx = brushCanvasTwoCtx.value;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  if (x >= 0 && y >= 0) {
    ctx.beginPath();
    ctx.arc(x, y, brushSize.value / 2, 0, Math.PI * 2);
    ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
    ctx.fill();
  }
  // }
};
const mouseenterDrawing = (event) => {
  // 获取鼠标相对于画布的位置
  const canvas = brushCanvas.value;
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;

  const x = (event.clientX - rect.left) * scaleX;
  const y = (event.clientY - rect.top) * scaleY;
  createCircle(x, y);
};
let startX = -1;
let startY = -1;
// 开始绘制
const startDrawing = (event) => {
  isDrawing.value = true;
  // draw(event);
  const topCtx = topCanvasContext.value;
  topCtx.lineCap = "round";
  topCtx.lineJoin = "round";
  const canvas = brushCanvas.value;
  // const ctx = canvasContext.value
  // 获取鼠标相对于画布的位置
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;

  const x = (event.clientX - rect.left) * scaleX;
  const y = (event.clientY - rect.top) * scaleY;
  startX = x;
  startY = y;
  createCircle(x, y);
  topCtx.beginPath();
  topCtx.moveTo(x, y);
};
// 绘制函数 - 更新为创建透明区域
const draw = (event) => {
  // 获取鼠标相对于画布的位置
  const canvas = topCanvasRef.value;
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;

  const x = (event.clientX - rect.left) * scaleX;
  const y = (event.clientY - rect.top) * scaleY;
  createCircle(x, y);
  if (!isDrawing.value) return;

  
  const ctx = topCanvasContext.value;

  

  if (brushActive.value === "eraser") {
    // 擦除模式 - 恢复原始图像
    ctx.globalCompositeOperation = "source-over";

    // 创建临时画布用于绘制擦除路径
    const tempCanvas = document.createElement("canvas");
    tempCanvas.width = canvas.width;
    tempCanvas.height = canvas.height;
    const tempCtx = tempCanvas.getContext("2d");

    // 在临时画布上绘制擦除路径
    tempCtx.globalCompositeOperation = "source-over";
    tempCtx.fillStyle = "white";
    tempCtx.strokeStyle = "white";
    tempCtx.lineWidth = brushSize.value;
    tempCtx.lineCap = "round";
    tempCtx.lineJoin = "round";

    tempCtx.beginPath();
    //   // 连续绘制线条
    tempCtx.moveTo(startX, startY);
    tempCtx.lineTo(x, y);
    tempCtx.stroke();

    // 获取擦除路径的图像数据作为蒙版
    const pathImageData = tempCtx.getImageData(
      0,
      0,
      tempCanvas.width,
      tempCanvas.height
    );

    // 在主画布上应用原始图像到擦除区域
    const currentImageData = ctx.getImageData(
      0,
      0,
      canvas.width,
      canvas.height
    );
    const originalData = originalImageData.value.data;
    const currentData = currentImageData.data;
    const pathData = pathImageData.data;

    for (let i = 0; i < pathData.length; i += 4) {
      if (pathData[i] > 0) {
        // 如果路径蒙版有值
        // 恢复原始图像的像素
        currentData[i] = originalData[i]; // R
        currentData[i + 1] = originalData[i + 1]; // G
        currentData[i + 2] = originalData[i + 2]; // B
        currentData[i + 3] = originalData[i + 3]; // A
      }
    }

    ctx.putImageData(currentImageData, 0, 0);
  } else {
    // ctx.globalCompositeOperation = "source-in";
    // 涂抹模式 - 使区域透明
    ctx.lineWidth = brushSize.value;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.lineTo(x, y);
    ctx.stroke();
  }
};

// 停止绘制
const stopDrawing = () => {
  if (!isDrawing) return;
  clear2();
  isDrawing.value = false;
};

const leaveDrawing = () => {
  if (!isDrawing) return;
  clear2()
  isDrawing.value = false;
};

const saveCanvas = async () => {
  if (topCanvasContext.value) {
    // 保存涂抹数据
    const topCanvas = topCanvasRef.value;
    maskData.value = topCanvasContext.value.getImageData(
      0,
      0,
      topCanvas.width,
      topCanvas.height
    );
    const canvas = getCanvas(0.7);
    const dataURL = canvas.toDataURL("image/png");
    emits("okPanel", dataURL);
  }
};

const getCanvas = (opacity) => {
  // 创建一个新的画布来生成遮罩图像
  const tempCanvas = document.createElement("canvas");

  tempCanvas.width = brushCanvas.value.width;
  tempCanvas.height = brushCanvas.value.height;
  const tempCtx = tempCanvas.getContext("2d");
  // tempCtx.globalCompositeOperation = 'destination-out'
  tempCtx.drawImage(brushCanvas.value, 0, 0);
  // 放置原始图像
  if (opacity) {
    tempCtx.globalAlpha = opacity;
    tempCtx.drawImage(topCanvasRef.value, 0, 0);
  } else {
    tempCtx.globalCompositeOperation = "destination-out";
    // 放置遮罩
    // tempCtx.globalAlpha = opacity ? opacity : 1
    tempCtx.drawImage(topCanvasRef.value, 0, 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 uploadSingleFile(res);

  // const urls = urlData.data.replace(/[\[\]]/g, "").split(",");
  return urlData.data;
};
const emits = defineEmits(["close", "okPanel"]);

const close = () => {
  emits("close");
};

defineExpose({
  initCanvas,
  reset,
  maskData,
  getMaskFile,
});
</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;
    margin: 10px 0;
    // cursor: none;
    .brush-canvas {
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
      cursor: none;
      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>

成品

image.png