像素画编辑器的触控逻辑比普通按钮点击复杂很多。用户看到的是一块可以缩放、可以平移的画布程序真正要修改的是row和col对应的像素。只要坐标换算有一点偏差就会出现“手指点 A像素画到 B”的问题。画布缩放后、横竖屏切换后、矩形画布不是正方形时这类问题尤其明显。bitArt 的触控相关代码主要在两个文件components/editor/PixelCanvasView.etsservices/EditorViewportService.ets1. 触控层不要直接改像素PixelCanvasView并不直接调用画笔算法而是把触控事件转换成像素坐标后通过回调交给页面onCellTouch: (type: TouchType, row: number, col: number, valid: boolean) void (_type: TouchType, _row: number, _col: number, _valid: boolean) {};这样做有两个好处。第一Canvas 组件只负责坐标和手势。它不知道当前工具是铅笔、橡皮、填充还是取色。第二页面可以根据全局状态决定下一步。比如图层锁定时不绘制形状工具拖动时显示预览移动工具拖动时平移画布。这是复杂编辑器很重要的边界触控组件负责“你点了哪个像素”业务层负责“点这个像素意味着什么”。2. 先计算真实绘制区域Canvas 视口通常大于实际画布。画布需要居中显示还要支持缩放和平移。bitArt 用几个方法统一计算画布在屏幕里的位置private getDrawWidth(): number { return this.canvasBaseWidth * this.zoomScale; } private getDrawHeight(): number { return this.canvasBaseHeight * this.zoomScale; } private getDrawLeft(): number { return (this.viewportWidth - this.getDrawWidth()) / 2 this.panOffsetX; } private getDrawTop(): number { return (this.viewportHeight - this.getDrawHeight()) / 2 this.panOffsetY; }这里可以看到触控换算不是直接拿event.touches[0].x除以画布宽度而是先减去drawLeft和drawTop。否则画布居中、平移、缩放都会导致坐标错位。3. 从屏幕坐标转成像素行列核心方法是getTouchCellprivate getTouchCell(event: TouchEvent): CanvasCell { const drawWidth this.getDrawWidth(); const drawHeight this.getDrawHeight(); const x event.touches[0].x - this.getDrawLeft(); const y event.touches[0].y - this.getDrawTop(); if (x 0 || x drawWidth || y 0 || y drawHeight) { return { row: 0, col: 0, valid: false }; } return { row: EditorViewportService.positionToCell(y, drawHeight, this.canvasHeight), col: EditorViewportService.positionToCell(x, drawWidth, this.canvasWidth), valid: true }; }这段代码有一个细节行使用canvasHeight列使用canvasWidth。这说明 bitArt 支持矩形画布而不是假设画布一定是正方形。positionToCell做了边界保护static positionToCell(position: number, canvasSide: number, canvasSize: number): number { const rawCell Math.floor(position / canvasSide * canvasSize); if (rawCell 0) { return 0; } if (rawCell canvasSize) { return canvasSize - 1; } return rawCell; }用户刚好点到画布边缘时浮点计算可能让结果落到边界外。这里统一夹取到合法像素避免数组越界。4. 缩放和平移也需要边界bitArt 的缩放范围是 1 到 6export class EditorViewportService { static readonly MIN_ZOOM: number 1; static readonly MAX_ZOOM: number 6; static clampZoom(zoom: number): number { if (zoom EditorViewportService.MIN_ZOOM) { return EditorViewportService.MIN_ZOOM; } if (zoom EditorViewportService.MAX_ZOOM) { return EditorViewportService.MAX_ZOOM; } return zoom; } }平移也不是无限拖动而是根据视口和绘制尺寸计算最大偏移static getMaxPanOffset(viewportSide: number, drawSide: number): number { if (viewportSide 0 || drawSide 0) { return 0; } return Math.abs(drawSide - viewportSide) / 2; }这样可以避免画布被拖到完全看不见的位置。对移动端编辑器来说这种限制会明显提升可用性。5. 连续笔触不能只画触摸点很多画布 Demo 的问题是只处理当前触摸点。用户快速拖动时相邻触摸事件之间可能跨过多个像素如果只画事件点线条就会断。bitArt 在PixelCanvasService.applyStroke中使用了类似 Bresenham 的插值逻辑从起点走到终点let currentRow startRow; let currentCol startCol; const rowDelta Math.abs(endRow - startRow); const colDelta Math.abs(endCol - startCol); const rowStep startRow endRow ? 1 : -1; const colStep startCol endCol ? 1 : -1; let error rowDelta - colDelta; while (true) { const result PixelCanvasService.applyTool( nextPixels, width, height, currentRow, currentCol, toolId, activeColor, toolSize, toolOpacity ); nextPixels result.pixels; if (currentRow endRow currentCol endCol) { break; } const doubleError error * 2; if (doubleError -colDelta) { error - colDelta; currentRow rowStep; } if (doubleError rowDelta) { error rowDelta; currentCol colStep; } }这段逻辑保证从上一触控点到当前触控点之间的像素都会被扫到。铅笔和橡皮擦都可以复用这条路径。6. 笔刷大小影响脏区域连续笔触不仅要画中间像素还要正确更新脏区域。bitArt 会根据笔刷大小扩展边界private getStrokeDirtyBounds(startRow: number, startCol: number, endRow: number, endCol: number): CanvasDirtyBounds { const toolSize this.getActivePaintSize(); const beforeCenter Math.floor((toolSize - 1) / 2); const afterCenter toolSize - beforeCenter - 1; return this.clampDirtyBounds({ minRow: Math.min(startRow, endRow) - beforeCenter, minCol: Math.min(startCol, endCol) - beforeCenter, maxRow: Math.max(startRow, endRow) afterCenter, maxCol: Math.max(startCol, endCol) afterCenter }); }如果只把起点和终点当作脏区域粗笔刷边缘会漏刷新。这个细节在 Canvas 局部刷新里很关键。7. 多指手势要取消绘制状态PixelCanvasView同时支持双指缩放和平移。进入多指手势时它会取消当前绘制PinchGesture({ fingers: 2 }) .onActionStart(() { this.onCellTouch(TouchType.Cancel, 0, 0, false); this.pinchBaseScale this.zoomScale; }) .onActionUpdate((event: GestureEvent) { this.updateZoom(this.pinchBaseScale * event.scale); })这是一个很实用的交互防护。用户从单指画画切到双指缩放时必须结束当前笔触否则可能产生意外连线。8. 用边界场景检查坐标链路检查触控实现时可以先抓住几条关键规则触控组件只负责坐标转换不直接改业务状态。坐标转换必须考虑画布居中、缩放和平移。行列换算要分别使用画布宽高支持矩形画布。连续笔触要在两次触摸点之间插值。笔刷大小要参与脏区域计算。多指手势开始时要取消当前绘制状态。实际验证要覆盖正方形和矩形画布。可以在 64×64、32×96 画布的四角与中心分别点按再放大到 600%、平移到边界后重复测试随后用快速斜线跨过多个像素格确认插值后没有断点。最后在单指绘制过程中切换双指缩放确认当前笔触被取消不会在两次触点之间拉出意外长线。这些逻辑看起来偏底层但它们决定了编辑器的手感。坐标不准、线条断裂或缩放后落点漂移用户会立刻感觉“不跟手”。下一篇转到图层系统继续看触控得到的像素修改如何只落到活动图层并参与可见性、锁定和透明度合成。