步骤 1:定义马赛克相关类型与变量
在代码中声明马赛克工具的类型、状态变量(如尺寸、移动状态),并在绘图动作接口中支持马赛克类型。
对应代码:
// 定义绘图工具类型,包含马赛克
type DrawingTool = 'pen' | 'rectangle' | 'circle' | 'arrow' | 'eraser' | 'text' | 'line' | 'mosaic';
// 马赛克相关变量
const mosaicSize = ref(100); // 马赛克尺寸(默认100px)
let isMovingMosaic = ref(false); // 是否正在移动马赛克
let currentMosaicIndex = ref(-1); // 当前选中的马赛克索引
// 绘图动作接口(支持马赛克工具)
interface DrawingAction {
tool: DrawingTool;
points: Point[]; // 存储中心点坐标(百分比)
color: string; // 马赛克固定颜色#50B19D
width: number; // 存储马赛克尺寸
text?: string;
}
步骤 2:添加马赛克工具按钮与尺寸控制器
在工具栏中添加马赛克按钮,点击后激活工具,并显示尺寸调整滑块。
对应代码:
<!-- 模板中的马赛克工具按钮 -->
<XmBtn icon-text="马赛克" @click="selectTool('mosaic')" :class="{ active: activeTool === 'mosaic' }">
<template #icon>
<span class="iconfont icon-mosaic"></span>
</template>
</XmBtn>
<!-- 马赛克尺寸调整器(仅在选中马赛克工具时显示) -->
<div v-if="activeTool === 'mosaic'" class="mosaic-size-control">
<el-slider
v-model="mosaicSize"
:min="30"
:max="500"
:step="10"
:show-input="true"
style="width: 140px"
tooltip="always">
</el-slider>
</div>
步骤 3:实现马赛克绘制逻辑(创建与预览)
处理鼠标事件,在画布上点击并拖动时创建马赛克,实时预览其位置和尺寸。
对应代码:
// 开始绘图(马赛克工具逻辑)
const startDrawing = (e: MouseEvent) => {
if (!props.isInitiator || !canvasContext || !canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
// 计算点击位置相对于画布的百分比坐标
const xPercent = (e.clientX - rect.left) / rect.width;
const yPercent = (e.clientY - rect.top) / rect.height;
// 马赛克工具逻辑
if (activeTool.value === 'mosaic') {
// 检查是否点击了已有的马赛克(用于移动)
const clickedIndex = findClickedAction(xPercent, yPercent);
if (clickedIndex !== -1 && drawingHistory.value[clickedIndex].tool === 'mosaic') {
isMovingMosaic.value = true;
currentMosaicIndex.value = clickedIndex;
startPoint = { x: xPercent, y: yPercent };
return;
}
// 创建新的马赛克
isDrawing.value = true;
startPoint = { x: xPercent, y: yPercent };
currentAction = {
tool: 'mosaic',
points: [{ x: xPercent, y: yPercent }], // 中心点坐标
color: '#50B19D', // 固定马赛克颜色
width: mosaicSize.value // 用width存储尺寸
};
return;
}
};
// 绘图过程(实时更新马赛克位置)
const draw = (e: MouseEvent) => {
if (activeTool.value !== 'mosaic' || !isDrawing.value || !currentAction || !canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
const xPercent = (e.clientX - rect.left) / rect.width;
const yPercent = (e.clientY - rect.top) / rect.height;
// 更新当前马赛克的中心点坐标和尺寸
currentAction.points = [{ x: xPercent, y: yPercent }];
currentAction.width = mosaicSize.value;
redrawCanvas(); // 实时重绘预览
};
// 停止绘图(保存马赛克到历史记录)
const stopDrawing = () => {
if (activeTool.value === 'mosaic' && isDrawing.value && currentAction) {
isDrawing.value = false;
drawingHistory.value.push(currentAction); // 保存到历史记录
sendDrawingAction({ type: 'draw', data: currentAction }); // 同步到其他用户
currentAction = null;
startPoint = null;
return;
}
};
步骤 4:实现马赛克的绘制渲染(画布显示)
通过临时画布生成马赛克图案(块状效果),并绘制到主画布,同时添加边框和角落标记增强可视性。
对应代码:
// 绘制单个标注动作(包含马赛克)
const drawAction = (action: DrawingAction) => {
if (!canvasContext || !canvasRef.value) return;
const { tool, points, color, width } = action;
const canvas = canvasRef.value;
// 转换百分比坐标为画布实际像素坐标
const actualPoints = points.map(p => ({
x: p.x * canvas.width,
y: p.y * canvas.height
}));
// 根据工具类型绘制,此处处理马赛克
switch (tool) {
case 'mosaic':
if (actualPoints.length) {
drawMosaic(actualPoints[0], width); // 调用马赛克绘制方法
}
break;
// 其他工具绘制逻辑...
}
};
// 绘制马赛克(核心渲染逻辑)
const drawMosaic = (position: Point, size: number) => {
if (!canvasContext || !canvasRef.value) return;
const canvas = canvasRef.value;
// 基于参考尺寸计算实际显示大小(适配画布缩放)
const mosaicSize = size * (canvas.width / props.referenceWidth);
const cellSize = 10; // 马赛克块大小(固定10px,保持块状感)
// 创建临时画布生成马赛克图案
const tempCanvas = document.createElement('canvas');
tempCanvas.width = mosaicSize;
tempCanvas.height = mosaicSize;
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) return;
// 绘制马赛克块(主色#50B19D,带浅色边框)
tempCtx.fillStyle = '#50B19D'; // 固定主色
tempCtx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; // 块间边框
tempCtx.lineWidth = 1;
// 绘制网格状马赛克块
for (let y = 0; y < mosaicSize; y += cellSize) {
for (let x = 0; x < mosaicSize; x += cellSize) {
tempCtx.fillRect(x, y, cellSize, cellSize); // 填充块
tempCtx.strokeRect(x, y, cellSize, cellSize); // 块边框
}
}
// 将临时画布绘制到主画布(居中显示)
canvasContext.drawImage(
tempCanvas,
position.x - mosaicSize / 2, // 左上角x(居中)
position.y - mosaicSize / 2, // 左上角y(居中)
mosaicSize,
mosaicSize
);
// 绘制外边框(突出马赛克范围)
canvasContext.strokeStyle = 'rgba(80, 177, 157, 0.8)'; // 同色系半透明边框
canvasContext.lineWidth = 2;
canvasContext.strokeRect(position.x - mosaicSize / 2, position.y - mosaicSize / 2, mosaicSize, mosaicSize);
// 绘制角落标记(增强边界识别)
const cornerSize = 8;
canvasContext.fillStyle = '#50B19D';
canvasContext.fillRect(position.x - mosaicSize / 2, position.y - mosaicSize / 2, cornerSize, cornerSize); // 左上角
canvasContext.fillRect(position.x + mosaicSize / 2 - cornerSize, position.y - mosaicSize / 2, cornerSize, cornerSize); // 右上角
canvasContext.fillRect(position.x - mosaicSize / 2, position.y + mosaicSize / 2 - cornerSize, cornerSize, cornerSize); // 左下角
canvasContext.fillRect(position.x + mosaicSize / 2 - cornerSize, position.y + mosaicSize / 2 - cornerSize, cornerSize, cornerSize); // 右下角
};
步骤 5:实现马赛克的编辑功能(移动与删除)
支持点击选中马赛克并拖动移动,以及通过橡皮擦工具删除。
对应代码:
// 移动马赛克(在draw方法中处理)
const draw = (e: MouseEvent) => {
// ...其他逻辑
// 移动马赛克逻辑
if (isMovingMosaic.value && currentMosaicIndex.value !== -1 && startPoint) {
const rect = canvasRef.value!.getBoundingClientRect();
const xPercent = (e.clientX - rect.left) / rect.width;
const yPercent = (e.clientY - rect.top) / rect.height;
// 计算移动偏移量
const dx = xPercent - startPoint.x;
const dy = yPercent - startPoint.y;
// 更新马赛克位置
const mosaic = drawingHistory.value[currentMosaicIndex.value];
mosaic.points[0].x += dx;
mosaic.points[0].y += dy;
// 限制在画布范围内(0-1之间)
mosaic.points[0].x = Math.max(0, Math.min(1, mosaic.points[0].x));
mosaic.points[0].y = Math.max(0, Math.min(1, mosaic.points[0].y));
// 更新起始点用于下一次计算
startPoint = { x: xPercent, y: yPercent };
redrawCanvas(); // 重绘
return;
}
};
// 停止移动马赛克(在stopDrawing中处理)
const stopDrawing = () => {
if (isMovingMosaic.value) {
isMovingMosaic.value = false;
if (currentMosaicIndex.value !== -1) {
// 同步移动后的马赛克数据
sendDrawingAction({
type: 'update',
index: currentMosaicIndex.value,
data: drawingHistory.value[currentMosaicIndex.value]
});
currentMosaicIndex.value = -1;
}
startPoint = null;
return;
}
};
// 橡皮擦删除马赛克(在startDrawing中处理)
const startDrawing = (e: MouseEvent) => {
// ...其他逻辑
// 橡皮擦逻辑(删除点击的标注,包括马赛克)
if (activeTool.value === 'eraser') {
const clickedIndex = findClickedAction(xPercent, yPercent);
if (clickedIndex !== -1) {
drawingHistory.value.splice(clickedIndex, 1); // 从历史中删除
sendDrawingAction({ type: 'remove', index: clickedIndex }); // 同步删除
redrawCanvas();
}
return;
}
};
步骤 6:实现马赛克的网络同步
通过 WebSocket 将马赛克的创建、移动、删除动作同步到其他用户。
对应代码:
// 发送绘图动作到服务器
const sendDrawingAction = (message: SocketMessage) => {
if (props.socket && props.isInitiator) {
props.socket.sendJson({
incidentType: 'annotation',
annotationType: message.type, // 'draw'/'update'/'remove'
data: message.data, // 马赛克数据
index: message.index, // 索引(用于删除/更新)
userId: props.userId,
creater: props.creater
});
}
};
// 处理接收的同步数据
const handleDrawingData = (data: any) => {
if (data.annotationType === 'draw' && data.data.tool === 'mosaic') {
drawingHistory.value.push(data.data); // 添加新马赛克
redrawCanvas();
} else if (data.annotationType === 'update' && data.data.tool === 'mosaic') {
drawingHistory.value[data.index] = data.data; // 更新移动后的马赛克
redrawCanvas();
} else if (data.annotationType === 'remove') {
drawingHistory.value.splice(data.index, 1); // 删除马赛克
redrawCanvas();
}
};
步骤 7:截图时保留马赛克效果
截图功能中单独处理马赛克绘制,确保导出的图片包含马赛克。
对应代码:
// 截图时绘制马赛克
const drawActionToCanvas = (action: DrawingAction, ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => {
switch (action.tool) {
case 'mosaic':
if (action.points.length) {
drawMosaicToCanvas(action.points[0], action.width, ctx, canvas);
}
break;
// 其他工具...
}
};
// 截图中的马赛克绘制方法
const drawMosaicToCanvas = (position: Point, size: number, ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => {
// 逻辑与drawMosaic类似,但基于原始参考尺寸绘制
const tempCanvas = document.createElement('canvas');
tempCanvas.width = size;
tempCanvas.height = size;
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) return;
// 绘制马赛克块(同主画布逻辑)
tempCtx.fillStyle = '#50B19D';
tempCtx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
const cellSize = 10;
for (let y = 0; y < size; y += cellSize) {
for (let x = 0; x < size; x += cellSize) {
tempCtx.fillRect(x, y, cellSize, cellSize);
tempCtx.strokeRect(x, y, cellSize, cellSize);
}
}
// 绘制外边框和角落标记
tempCtx.strokeStyle = 'rgba(80, 177, 157, 0.8)';
tempCtx.lineWidth = 2;
tempCtx.strokeRect(0, 0, size, size);
const cornerSize = 8;
tempCtx.fillRect(0, 0, cornerSize, cornerSize);
// ...其他三个角落
// 绘制到截图画布
ctx.drawImage(tempCanvas, position.x - size / 2, position.y - size / 2, size, size);
};
总结
马赛克功能的实现核心是:
- 通过临时画布生成块状图案,确保视觉效果一致;
- 采用百分比坐标存储位置,适配不同尺寸的画布;
- 支持实时编辑 (移动、调整尺寸)和网络同步,满足协作需求;
- 截图时单独处理绘制逻辑,确保导出内容完整。