实现视频实时马赛克

步骤 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);
};

总结

马赛克功能的实现核心是:

  1. 通过临时画布生成块状图案,确保视觉效果一致;
  2. 采用百分比坐标存储位置,适配不同尺寸的画布;
  3. 支持实时编辑 (移动、调整尺寸)和网络同步,满足协作需求;
  4. 截图时单独处理绘制逻辑,确保导出内容完整。
相关推荐
人生在勤,不索何获-白大侠4 分钟前
day25——HTML & CSS 前端开发
前端·css·html
星期天要睡觉4 分钟前
Linux零基础Shell教学全集(可用于日常查询语句,目录清晰,内容详细)(自学尚硅谷B站shell课程后的万字学习笔记,附课程链接)
linux·运维·shell
Running_C11 分钟前
Content-Type的几种类型
前端·面试
前端Hardy11 分钟前
10 分钟搞定婚礼小程序?我用 DeepSeek 把同学的作业卷成了范本!
前端·javascript·微信小程序
Tminihu13 分钟前
前端大文件上传的时候,采用切片上传的方式,如果断网了,应该如何处理
前端·javascript
颜酱15 分钟前
理解vue3中的compiler-core
前端·javascript·vue.js
果粒chenl26 分钟前
06-原型和原型链
前端·javascript·原型模式
Entropy-Lee27 分钟前
JavaScript语法、关键字和变量
开发语言·javascript·ecmascript
谢尔登28 分钟前
【JavaScript】手写 Object.prototype.toString()
前端·javascript·原型模式
蓝倾30 分钟前
小红书获取笔记详情API接口调用操作指南
前端·api·fastapi