小疯子呵
小疯子呵
  • 发布:2025-11-02 23:33
  • 更新:2025-11-02 23:33
  • 阅读:449

用uni-app搞了个足球战术板,踩了不少canvas坑,分享一下经验

分类:uni-app

前言

最近开发了一款【苏超排名助手】,里面有一个功能页:足球战术板,主要就是让用户能在球场上画箭头、线条、曲线啥的,方便教练布置战术。本来以为挺简单的,结果各种坑,尤其是要兼容鸿蒙,差点没给我整崩溃。不过最后还是搞定了,记录一下,给后面的兄弟们少踩点坑。

需求是啥

说白了就是这几个功能:

  • 在球场上画箭头(传球路线)
  • 画直线和曲线(跑位路线)
  • 橡皮擦,画错了能擦掉
  • 能保存成图片分享
  • 还得支持鸿蒙(这个最坑)

Canvas初始化 - 第一个大坑

鸿蒙和其他平台的API不一样

uni-app有两种Canvas API:

  • 旧的:uni.createCanvasContext('canvasId')
  • 新的:Canvas 2D(type="2d")

鸿蒙现在只支持旧API,所以得这样写:

<!-- #ifdef APP-HARMONY -->  
<canvas  
  canvas-id="tacticsCanvas"  
  id="tacticsCanvas"  
  class="canvas-board"  
  @touchstart="handleCanvasTouchStart"  
  @touchmove="handleCanvasTouchMove"  
  @touchend="handleCanvasTouchEnd"  
  disable-scroll="true"  
></canvas>  
<!-- #endif -->  

<!-- #ifndef APP-HARMONY -->  
<canvas  
  type="2d"  
  id="tacticsCanvas"  
  canvas-id="tacticsCanvas"  
  class="canvas-board"  
  @touchstart="handleCanvasTouchStart"  
  @touchmove.prevent="handleCanvasTouchMove"  
  @touchend="handleCanvasTouchEnd"  
></canvas>  
<!-- #endif -->

注意几个细节:

  1. 鸿蒙不用写type="2d"
  2. 鸿蒙用disable-scroll="true",其他平台用@touchmove.prevent
  3. 两个平台都得有canvas-idid

初始化时机很关键

不能在onMounted里直接初始化,得等页面真正渲染完了再搞。我试了好几次,最后发现延迟个200ms比较靠谱:

onMounted(() => {  
  // #ifdef APP-HARMONY  
  setTimeout(() => {  
    const systemInfo = uni.getSystemInfoSync();  
    canvasWidth.value = systemInfo.windowWidth;  
    canvasHeight.value = systemInfo.windowWidth * 1.25;  

    ctx.value = uni.createCanvasContext("tacticsCanvas");  
    ctx.value.isCanvas2d = false; // 标记是旧API  

    // 再延迟获取真实边界  
    setTimeout(() => {  
      const query = uni.createSelectorQuery();  
      query.select(".canvas-board").boundingClientRect((rect) => {  
        if (rect) {  
          boardRect.value = rect;  
          canvasReady.value = true; // 就绪标记  
        }  
      }).exec();  
    }, 100);  
  }, 200);  
  // #endif  
});

为啥要搞个isCanvas2d标记?因为后面很多API调用方式不一样,得区分开。

触摸绘制 - 坐标转换是重点

坐标系问题

这个坑我踩了好久。触摸事件返回的坐标,在不同平台上格式不一样:

  • 鸿蒙:touch.x / touch.y(可能是相对坐标)
  • 其他平台:touch.clientX / touch.clientY(需要减去canvas的偏移)

我写了个通用函数处理:

const getCanvasCoords = (touch) => {  
  let x, y;  

  // #ifdef APP-HARMONY  
  if (touch.x !== undefined && touch.y !== undefined) {  
    const rawX = touch.x;  
    const rawY = touch.y;  

    // 如果在合理范围内,直接用  
    if (rawX >= 0 && rawX <= canvasWidth.value &&   
        rawY >= 0 && rawY <= canvasHeight.value) {  
      x = rawX;  
      y = rawY;  
    }  
    // 超出范围就减去偏移  
    else if (boardRect.value && rawX > canvasWidth.value) {  
      x = rawX - boardRect.value.left;  
      y = rawY - boardRect.value.top;  
    }  
    else {  
      x = rawX;  
      y = rawY;  
    }  
  }  
  // #endif  

  // #ifndef APP-HARMONY  
  if (touch.clientX !== undefined && touch.clientY !== undefined && boardRect.value) {  
    x = touch.clientX - boardRect.value.left;  
    y = touch.clientY - boardRect.value.top;  
  }  
  // #endif  

  // 限制在画布范围内  
  x = Math.max(0, Math.min(x, canvasWidth.value));  
  y = Math.max(0, Math.min(y, canvasHeight.value));  

  return { x, y };  
};

绘制流程

整个绘制流程是这样的:

  1. touchstart:记录起点,初始化当前绘制对象

    const handleCanvasTouchStart = (e) => {  
    if (!canvasReady.value) {  
    uni.showToast({ title: "画布初始化中,请稍候", icon: "none" });  
    return;  
    }  
    
    const touch = e.touches[0];  
    const coords = getCanvasCoords(touch);  
    
    isDrawing.value = true;  
    currentDrawing.value = {  
    type: currentTool.value, // arrow/line/curve  
    startX: coords.x,  
    startY: coords.y,  
    endX: coords.x,  
    endY: coords.y,  
    points: [{ x: coords.x, y: coords.y }], // 曲线用  
    color: currentTool.value === 'arrow' ? '#ff6b35' : '#1a3b6e'  
    };  
    };
  2. touchmove:更新终点或添加曲线点

    const handleCanvasTouchMove = (e) => {  
    if (!isDrawing.value || !currentDrawing.value) return;  
    
    const touch = e.touches[0];  
    const coords = getCanvasCoords(touch);  
    
    if (currentTool.value === 'curve') {  
    // 曲线:不断添加点  
    currentDrawing.value.points.push({ x: coords.x, y: coords.y });  
    } else {  
    // 箭头/直线:更新终点  
    currentDrawing.value.endX = coords.x;  
    currentDrawing.value.endY = coords.y;  
    }  
    
    redrawCanvas(); // 实时重绘  
    };
  3. touchend:保存到数组

    const handleCanvasTouchEnd = () => {  
    if (isDrawing.value && currentDrawing.value) {  
    drawings.value.push({ ...currentDrawing.value });  
    isDrawing.value = false;  
    currentDrawing.value = null;  
    redrawCanvas();  
    }  
    };

绘制各种图形

兼容两种API的方法

旧API和新API的方法名不一样,得封装一下:

const drawShape = (shape, context = null) => {  
  const drawCtx = context || ctx.value;  
  if (!drawCtx) return;  

  // 兼容函数  
  const setStrokeStyle = (color) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.strokeStyle = color;  
    } else {  
      drawCtx.setStrokeStyle(color); // 旧API  
    }  
  };  

  const setLineWidth = (width) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.lineWidth = width;  
    } else {  
      drawCtx.setLineWidth(width);  
    }  
  };  

  const setLineCap = (cap) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.lineCap = cap;  
    } else {  
      drawCtx.setLineCap(cap);  
    }  
  };  

  // ... 其他方法类似  
};

绘制箭头

箭头是最常用的,分两部分:线段 + 箭头头部

if (shape.type === 'arrow') {  
  const endX = shape.endX || shape.startX;  
  const endY = shape.endY || shape.startY;  

  // 1. 画线  
  drawCtx.beginPath();  
  drawCtx.moveTo(shape.startX, shape.startY);  
  drawCtx.lineTo(endX, endY);  
  drawCtx.stroke();  

  // 2. 画箭头头部(两条边)  
  const angle = Math.atan2(endY - shape.startY, endX - shape.startX);  
  const arrowLength = 25;  

  setLineWidth(5);  
  drawCtx.beginPath();  
  drawCtx.moveTo(endX, endY);  
  // 左边  
  drawCtx.lineTo(  
    endX - arrowLength * Math.cos(angle - Math.PI / 6),  
    endY - arrowLength * Math.sin(angle - Math.PI / 6)  
  );  
  drawCtx.moveTo(endX, endY);  
  // 右边  
  drawCtx.lineTo(  
    endX - arrowLength * Math.cos(angle + Math.PI / 6),  
    endY - arrowLength * Math.sin(angle + Math.PI / 6)  
  );  
  drawCtx.stroke();  
}

箭头的原理:

  1. 先算出线的角度:Math.atan2(dy, dx)
  2. 箭头两边各偏离30度(Math.PI / 6)
  3. 用三角函数算出两条边的终点坐标

绘制曲线(虚线)

曲线用来表示跑位,所以用虚线:

if (shape.type === 'curve' && shape.points) {  
  setLineDash([10, 5]); // 虚线:10px实线,5px空白  
  drawCtx.beginPath();  
  drawCtx.moveTo(shape.points[0].x, shape.points[0].y);  
  for (let i = 1; i < shape.points.length; i++) {  
    drawCtx.lineTo(shape.points[i].x, shape.points[i].y);  
  }  
  drawCtx.stroke();  
  setLineDash([]); // 恢复实线  
}

橡皮擦功能 - 点到线段距离算法

橡皮擦要判断点击的位置是不是靠近某条线,这个用到点到线段距离公式:

const pointToLineDistance = (px, py, x1, y1, x2, y2) => {  
  const A = px - x1;  
  const B = py - y1;  
  const C = x2 - x1;  
  const D = y2 - y1;  

  const dot = A * C + B * D;  
  const lenSq = C * C + D * D;  
  let param = -1;  

  if (lenSq !== 0) param = dot / lenSq;  

  let xx, yy;  

  if (param < 0) {  
    // 点在线段外侧,靠近起点  
    xx = x1;  
    yy = y1;  
  } else if (param > 1) {  
    // 点在线段外侧,靠近终点  
    xx = x2;  
    yy = y2;  
  } else {  
    // 点在线段范围内  
    xx = x1 + param * C;  
    yy = y1 + param * D;  
  }  

  const dx = px - xx;  
  const dy = py - yy;  
  return Math.sqrt(dx * dx + dy * dy);  
};

这个算法的核心思路:

  1. 把点投影到直线上
  2. 判断投影点是不是在线段范围内
  3. 算出点到投影点的距离

然后在touchstart判断:

if (currentTool.value === 'eraser') {  
  const clickX = coords.x;  
  const clickY = coords.y;  
  const eraseRadius = 30; // 橡皮擦范围  

  for (let i = drawings.value.length - 1; i >= 0; i--) {  
    const drawing = drawings.value[i];  
    let isNear = false;  

    if (drawing.type === 'line' || drawing.type === 'arrow') {  
      const dist = pointToLineDistance(  
        clickX, clickY,  
        drawing.startX, drawing.startY,  
        drawing.endX, drawing.endY  
      );  
      isNear = dist < eraseRadius;  
    } else if (drawing.type === 'curve') {  
      // 曲线:检查是否靠近任意点  
      for (let point of drawing.points) {  
        const dist = Math.sqrt(  
          Math.pow(clickX - point.x, 2) + Math.pow(clickY - point.y, 2)  
        );  
        if (dist < eraseRadius) {  
          isNear = true;  
          break;  
        }  
      }  
    }  

    if (isNear) {  
      drawings.value.splice(i, 1);  
      redrawCanvas();  
      break;  
    }  
  }  
}

重绘画布 - 性能优化

每次操作都要重绘整个画布,顺序很重要:

const redrawCanvas = () => {  
  if (!ctx.value) return;  

  // 1. 清空画布(但不渲染)  
  ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);  

  // 2. 绘制所有已保存的图形  
  drawings.value.forEach((drawing) => {  
    drawShape(drawing);  
  });  

  // 3. 绘制当前正在画的(实时反馈)  
  if (currentDrawing.value) {  
    drawShape(currentDrawing.value);  
  }  

  // 4. 旧API必须调用draw()才能渲染到屏幕  
  if (!ctx.value.isCanvas2d) {  
    ctx.value.draw(false); // false表示不清空之前的内容  
  }  
};

注意:

  • 新API(Canvas 2D):每次绘制都是实时的
  • 旧API:必须调用draw()才会显示
  • 鸿蒙用的是旧API,别忘了draw()
  • draw(false)的false很关键,true会清空画布

导出图片 - 生成分享海报

这个功能挺实用的,步骤是这样的:

  1. 先截取战术板:把球场、球员、战术线都画到临时canvas上
  2. 再画海报:在另一个canvas上画标题、战术板图片、底部信息
  3. 导出为图片:用uni.canvasToTempFilePath

绘制战术板完整内容

const captureBoard = () => {  
  return new Promise((resolve, reject) => {  
    const tempCtx = uni.createCanvasContext("tacticsCanvas");  
    tempCtx.isCanvas2d = false;  

    // 1. 画背景(球场)  
    tempCtx.setFillStyle("#2d8659");  
    tempCtx.fillRect(0, 0, boardWidth, boardHeight);  

    // 2. 画场地线(中线、禁区等)  
    tempCtx.setStrokeStyle("rgba(255, 255, 255, 0.6)");  
    tempCtx.setLineWidth(2);  
    tempCtx.beginPath();  
    tempCtx.moveTo(0, boardHeight * 0.5);  
    tempCtx.lineTo(boardWidth, boardHeight * 0.5);  
    tempCtx.stroke();  

    // 中圈  
    const centerCircleRadius = (100 / 750) * boardWidth;  
    tempCtx.beginPath();  
    tempCtx.arc(boardWidth * 0.5, boardHeight * 0.5, centerCircleRadius, 0, 2 * Math.PI);  
    tempCtx.stroke();  

    // ... 更多场地线  

    // 3. 画战术线路  
    drawings.value.forEach((drawing) => {  
      drawShape(drawing, tempCtx);  
    });  

    // 4. 画球员  
    playersOnField.value.forEach((player) => {  
      const markerRadius = (60 / 2) * (screenWidth / 750);  
      const centerX = player.x + markerRadius;  
      const centerY = player.y + markerRadius;  

      // 圆圈  
      tempCtx.setFillStyle(player.team === 'home' ? '#1a3b6e' : '#ff6b35');  
      tempCtx.beginPath();  
      tempCtx.arc(centerX, centerY, markerRadius, 0, 2 * Math.PI);  
      tempCtx.fill();  

      // 号码  
      tempCtx.setFillStyle('#ffffff');  
      tempCtx.setFontSize(markerRadius * 0.8);  
      tempCtx.setTextAlign('center');  
      tempCtx.setTextBaseline('middle');  
      tempCtx.fillText(player.number, centerX, centerY);  
    });  

    // 5. 导出  
    tempCtx.draw(false, () => {  
      setTimeout(() => {  
        uni.canvasToTempFilePath({  
          canvasId: "tacticsCanvas",  
          success: (res) => {  
            resolve(res.tempFilePath);  
          },  
          fail: reject  
        });  
      }, 500); // 等渲染完成  
    });  
  });  
};

绘制海报

const drawSharePoster = (boardImagePath) => {  
  return new Promise((resolve, reject) => {  
    const pWidth = 375;  
    const pHeight = 550;  
    const pCtx = uni.createCanvasContext("sharePosterCanvas");  

    // 1. 背景渐变  
    const gradient = pCtx.createLinearGradient(0, 0, 0, pHeight);  
    gradient.addColorStop(0, "#1a3b6e");  
    gradient.addColorStop(1, "#2c5282");  
    pCtx.setFillStyle(gradient);  
    pCtx.fillRect(0, 0, pWidth, pHeight);  

    // 2. 标题  
    pCtx.setFillStyle("#ffffff");  
    pCtx.setFontSize(24);  
    pCtx.setTextAlign("center");  
    pCtx.fillText("⚽ 足球战术板", pWidth / 2, 40);  

    // 3. 副标题  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.8)");  
    pCtx.setFontSize(14);  
    pCtx.fillText("Football Tactics Board", pWidth / 2, 65);  

    // 4. 战术板图片(重点!)  
    const margin = 20;  
    const maxBoardWidth = pWidth - margin * 2;  
    const boardAspect = boardRect.value.width / boardRect.value.height;  
    let drawWidth = maxBoardWidth;  
    let drawHeight = drawWidth / boardAspect;  

    const boardX = (pWidth - drawWidth) / 2;  
    const boardY = 90;  

    // 白色卡片背景  
    pCtx.setFillStyle("#ffffff");  
    pCtx.fillRect(boardX - 8, boardY - 8, drawWidth + 16, drawHeight + 16);  

    // 画战术板图片  
    pCtx.drawImage(boardImagePath, boardX, boardY, drawWidth, drawHeight);  

    // 5. 底部信息  
    const footerY = pHeight - 50;  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.9)");  
    pCtx.setFontSize(16);  
    pCtx.fillText("苏超排名助手", pWidth / 2, footerY);  

    const now = new Date();  
    const timeStr = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}`;  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.5)");  
    pCtx.setFontSize(10);  
    pCtx.fillText(`生成时间: ${timeStr}`, pWidth / 2, footerY + 25);  

    // 6. 导出高清图(2倍分辨率)  
    pCtx.draw(false, () => {  
      setTimeout(() => {  
        uni.canvasToTempFilePath({  
          canvasId: "sharePosterCanvas",  
          width: pWidth,  
          height: pHeight,  
          destWidth: pWidth * 2,  // 2倍分辨率  
          destHeight: pHeight * 2,  
          fileType: "png",  
          quality: 1,  
          success: (res) => resolve(res.tempFilePath),  
          fail: reject  
        });  
      }, 800);  
    });  
  });  
};

注意destWidthdestHeight,设置成2倍能提高导出图片的清晰度。

踩过的坑总结

1. 初始化时机问题

症状:获取不到canvas尺寸,或者坐标转换不准

原因:页面还没渲染完就初始化了

解决:延迟200ms,再用createSelectorQuery获取真实位置

setTimeout(() => {  
  const query = uni.createSelectorQuery();  
  query.select(".canvas-board").boundingClientRect((rect) => {  
    boardRect.value = rect;  
    canvasReady.value = true;  
  }).exec();  
}, 200);

2. 坐标转换不准确

症状:画的位置跟手指点的位置对不上

原因:没考虑canvas的偏移量(padding、margin等)

解决:必须用boundingClientRect获取真实位置,然后减去偏移

x = touch.clientX - boardRect.value.left;  
y = touch.clientY - boardRect.value.top;

3. 鸿蒙draw()必须调用

症状:画了但是屏幕上看不到

原因:旧API必须调用draw()才会渲染

解决:每次绘制完都调用

if (!ctx.value.isCanvas2d) {  
  ctx.value.draw(false);  
}

4. 导出图片时机问题

症状:导出的图片是空白的或者不完整

原因draw()是异步的,还没渲染完就导出了

解决:在draw()的回调里,再延迟500ms

ctx.draw(false, () => {  
  setTimeout(() => {  
    uni.canvasToTempFilePath({...});  
  }, 500);  
});

5. 橡皮擦范围不好控制

症状:点了删不掉,或者不小心删了别的线

原因:距离阈值设置不合理

解决:30-40px比较合适,太小不好点,太大容易误删

const eraseRadius = 30; // 橡皮擦范围  
if (dist < eraseRadius) {  
  // 删除  
}

6. touchmove和页面滚动冲突

症状:画线的时候页面跟着滚动

原因:touchmove事件冒泡到了页面

解决:加.stop.prevent修饰符

<canvas @touchmove.stop.prevent="handleCanvasTouchMove"></canvas>

但是鸿蒙不支持.prevent,要用disable-scroll="true"

<!-- #ifdef APP-HARMONY -->  
<canvas disable-scroll="true"></canvas>  
<!-- #endif -->

7. 重绘性能问题

症状:线条多了之后很卡

原因:每次touchmove都要重绘所有线条

解决

  • 限制绘制对象数量(最多50个)
  • touchmove节流(每10ms更新一次)
  • 复杂背景用离屏canvas缓存
let lastTime = 0;  
const handleCanvasTouchMove = (e) => {  
  const now = Date.now();  
  if (now - lastTime < 10) return; // 节流  
  lastTime = now;  
  // ...  
};

8. 图片清晰度问题

症状:导出的图片很模糊

原因:没设置destWidthdestHeight

解决:设置成实际尺寸的2倍

uni.canvasToTempFilePath({  
  canvasId: "myCanvas",  
  destWidth: width * 2,  
  destHeight: height * 2,  
  fileType: "png",  
  quality: 1  
});

性能优化建议

1. 节流touchmove

绘制曲线时,不用每次touchmove都添加点:

let lastTime = 0;  
const handleCanvasTouchMove = (e) => {  
  const now = Date.now();  
  if (now - lastTime < 16) return; // 约60fps  
  lastTime = now;  

  const coords = getCanvasCoords(e.touches[0]);  
  currentDrawing.value.points.push(coords);  
  redrawCanvas();  
};

2. 限制绘制数量

太多绘制对象会导致重绘变慢:

const MAX_DRAWINGS = 50;  

const handleCanvasTouchEnd = () => {  
  if (isDrawing.value && currentDrawing.value) {  
    drawings.value.push({ ...currentDrawing.value });  

    // 限制数量  
    if (drawings.value.length > MAX_DRAWINGS) {  
      drawings.value.shift(); // 删除最早的  
    }  

    redrawCanvas();  
  }  
};

3. 离屏canvas缓存背景

球场背景每次都画很费性能,可以缓存起来:

let backgroundCache = null;  

const cacheBackground = () => {  
  const offscreenCanvas = uni.createOffscreenCanvas({  
    type: '2d',  
    width: canvasWidth.value,  
    height: canvasHeight.value  
  });  

  const offCtx = offscreenCanvas.getContext('2d');  

  // 画球场背景  
  offCtx.fillStyle = '#2d8659';  
  offCtx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);  
  // ... 画其他场地线  

  backgroundCache = offscreenCanvas;  
};  

const redrawCanvas = () => {  
  if (!ctx.value) return;  

  ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);  

  // 用缓存的背景  
  if (backgroundCache) {  
    ctx.value.drawImage(backgroundCache, 0, 0);  
  }  

  // 画战术线  
  drawings.value.forEach(drawing => drawShape(drawing));  

  if (!ctx.value.isCanvas2d) {  
    ctx.value.draw(false);  
  }  
};

4. 按需重绘

如果只是改变某个元素,可以不重绘整个画布(但Canvas API不支持局部重绘,这个比较难实现)。

替代方案:把不同层分开

  • 背景层:静态的球场(很少变化)
  • 战术层:箭头、线条(经常变化)
  • 球员层:用普通DOM而不是Canvas
<!-- 背景canvas -->  
<canvas id="bgCanvas" class="bg-layer"></canvas>  

<!-- 战术canvas -->  
<canvas id="tacticsCanvas" class="tactics-layer"></canvas>  

<!-- 球员用普通DOM -->  
<view class="players-layer">  
  <view v-for="player in players" :key="player.id"   
        :style="{left: player.x, top: player.y}">  
    {{ player.number }}  
  </view>  
</view>

这样球员拖动就不需要重绘canvas了。

实用技巧

1. 调试坐标

在开发时可以实时显示坐标,方便调试:

const handleCanvasTouchMove = (e) => {  
  const coords = getCanvasCoords(e.touches[0]);  
  console.log(`x: ${coords.x}, y: ${coords.y}`);  
  // 或者显示在页面上  
  debugInfo.value = `x: ${coords.x}, y: ${coords.y}`;  
};

2. 撤销功能

保存历史记录数组:

const history = ref([]);  
const historyIndex = ref(-1);  

const saveHistory = () => {  
  // 删除当前索引之后的历史  
  history.value = history.value.slice(0, historyIndex.value + 1);  
  // 添加新状态  
  history.value.push(JSON.parse(JSON.stringify(drawings.value)));  
  historyIndex.value++;  

  // 限制历史记录数量  
  if (history.value.length > 20) {  
    history.value.shift();  
    historyIndex.value--;  
  }  
};  

const undo = () => {  
  if (historyIndex.value > 0) {  
    historyIndex.value--;  
    drawings.value = JSON.parse(JSON.stringify(history.value[historyIndex.value]));  
    redrawCanvas();  
  }  
};  

const redo = () => {  
  if (historyIndex.value < history.value.length - 1) {  
    historyIndex.value++;  
    drawings.value = JSON.parse(JSON.stringify(history.value[historyIndex.value]));  
    redrawCanvas();  
  }  
};

3. 颜色选择器

让用户自定义线条颜色:

const colors = ['#ff6b35', '#1a3b6e', '#4caf50', '#f44336', '#9e9e9e'];  
const currentColor = ref('#ff6b35');  

const handleCanvasTouchStart = (e) => {  
  // ...  
  currentDrawing.value = {  
    type: currentTool.value,  
    startX: coords.x,  
    startY: coords.y,  
    color: currentColor.value // 使用选中的颜色  
  };  
};

4. 线条粗细调整

const lineWidths = [2, 4, 6, 8];  
const currentWidth = ref(4);  

const drawShape = (shape) => {  
  // ...  
  const lineWidth = shape.width || 4;  
  setLineWidth(lineWidth);  
  // ...  
};

最后

整个功能做下来,最大的感受就是:Canvas在uni-app编译到鸿蒙还是有不少坑的。不过搞定之后还挺有成就感的。

开发建议

  1. 先在H5上调试,等逻辑都对了再适配鸿蒙
  2. 鸿蒙的真机调试很慢,多用console.loguni.showToast
  3. 坐标问题一定要用实际设备测试,模拟器不准
  4. 多保存代码,Canvas相关的代码很容易改崩

适用场景

  • 战术板(足球、篮球等)
  • 签名功能
  • 涂鸦板
  • 路径规划
  • 图片标注

代码在附件,有问题可以留言交流。希望能帮到要做类似功能的兄弟们!

5 关注 分享
威龙 唐家三少 蜂医 WstWrld DCloud_UNI_CHB

要回复文章请先登录注册