实现视频实时马赛克
步骤 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; // 马赛克固定颜色#50B19Dwidth: 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-sliderv-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);
};
总结
马赛克功能的实现核心是:
- 通过临时画布生成块状图案,确保视觉效果一致;
- 采用百分比坐标存储位置,适配不同尺寸的画布;
- 支持实时编辑(移动、调整尺寸)和网络同步,满足协作需求;
- 截图时单独处理绘制逻辑,确保导出内容完整。