canvas涂抹,擦除功能组件

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

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

ini 复制代码
<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"
        @mousedown="startDrawing"
        @mousemove="draw"
        @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";

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 initCanvas = () => {
  
  // 第一个canvas 只做背景
  const canvas = brushCanvas.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;

  // 获取画布上下文
  // 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);

  // 绘制原始图片
  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);
  }
};
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;
  topCtx.beginPath();
  topCtx.moveTo(x, y);
};
// 绘制函数 - 更新为创建透明区域
const draw = (event) => {
  if (!isDrawing.value) return;

  const canvas = topCanvasRef.value;
  const ctx = topCanvasContext.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;

  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.lineWidth = brushSize.value;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.lineTo(x, y);
    ctx.stroke();
  }

};

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

const leaveDrawing = () => {
  if (!isDrawing) return;
  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()
    const dataURL = canvas.toDataURL('image/png');
    emits("okPanel", dataURL);
  }
};

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
});
</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>

成品

相关推荐
胖虎2654 小时前
前端多文件上传核心功能实现:格式支持、批量上传与状态可视化
前端
胖虎2654 小时前
Vue2 项目常用配置合集:多语言、SVG 图标、代码格式化、权限指令 + 主题切换
前端
一键定乾坤4 小时前
npm 源修改
前端
parade岁月4 小时前
Vue 3 响应式陷阱:对象引用丢失导致的数据更新失效
前端
掘金安东尼4 小时前
GPT-6 会带来科学革命?奥特曼最新设想:AI CEO、便宜医疗与全新计算机
前端·vue.js·github
申阳4 小时前
Day 5:03. 基于Nuxt开发博客项目-页面结构组织
前端·后端·程序员
全马必破三4 小时前
React的设计理念与核心特性
前端·react.js·前端框架
ttod_qzstudio4 小时前
替代 TDesign Dialog:用 div 实现可拖拽、遮罩屏蔽的对话框
前端·tdesign
洞窝技术4 小时前
前端人必看的 node_modules 瘦身秘籍:从臃肿到轻盈,Umi 项目依赖优化实战
前端·vue.js·react.js