可以透明涂抹,擦除,生成图片
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>
成品
