bitArt 的绘制体验,很大一部分取决于触控坐标转换是否稳定。像素画编辑器不是简单响应点击位置,而是要把屏幕坐标、安全区域、画布缩放、平移偏移、矩形画布比例,最后全部映射到一个明确的 row/col 像素格子。

图 1:bitArt 在 HarmonyOS 模拟器中的像素画编辑工作台。

bitArt HarmonyOS 像素画工作台

图 2:bitArt HarmonyOS 工程服务分层关系,触控坐标、画布视口和绘制服务保持解耦。

bitArt HarmonyOS 服务分层关系

坐标转换不能散落在工具里

bitArt 把视口换算收敛到 EditorViewportService,绘制工具只消费 row/col。这样铅笔、橡皮、取色、形状工具都不需要各自重复处理缩放和平移。

static getCanvasBaseScale(viewportWidth: number, viewportHeight: number, canvasWidth: number,
  canvasHeight: number): number {
  if (viewportWidth <= 0 || viewportHeight <= 0 || canvasWidth <= 0 || canvasHeight <= 0) {
    return 1;
  }
  return Math.min(viewportWidth / canvasWidth, viewportHeight / canvasHeight);
}

static positionToCell(position: number, canvasSide: number, canvasSize: number): number {
  if (canvasSide <= 0 || canvasSize <= 0) {
    return 0;
  }
  const rawCell = Math.floor(position / canvasSide * canvasSize);
  if (rawCell < 0) {
    return 0;
  }
  if (rawCell >= canvasSize) {
    return canvasSize - 1;
  }
  return rawCell;
}

这里最关键的是边界裁剪。触控点可能落在画布外侧,或者因为缩放产生小数坐标,如果不 clamp 到 0 到 canvasSize - 1,后面的像素数组就会出现越界风险。

Move 事件需要补点

移动端触控事件不是每个像素格都会触发一次。手指滑得快时,如果只处理当前 Move 点,画线会断。bitArt 在 PixelCanvasService.applyStroke 中做了类似 Bresenham 的插值,让 start 到 end 之间的格子都能被处理。

static applyStroke(pixels: string[], width: number, height: number, startRow: number, startCol: number,
  endRow: number, endCol: number, toolId: string, activeColor: string, toolSize: number = 1,
  toolOpacity: number = 100): PixelToolResult {
  let nextPixels = pixels.slice();
  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;
    }
  }

  return { pixels: nextPixels, pickedColor: '' };
}

页面层只做触控状态机

Index.ets 不直接改像素,而是维护 Down、Move、Up 的状态。Down 开始一段笔画,Move 判断是否进入新格子,Up/Cancel 结束笔画。

private handleCanvasCellTouch(type: TouchType, row: number, col: number, valid: boolean): void {
  if (type === TouchType.Up || type === TouchType.Cancel) {
    this.endStroke();
    return;
  }
  if (!valid) {
    return;
  }
  if (type === TouchType.Down) {
    this.beginStroke();
    this.applyCanvasTool(row, col);
    this.lastStrokeRow = row;
    this.lastStrokeCol = col;
    return;
  }
  if (type === TouchType.Move && this.strokeActive && this.activeTool !== 'fill' && this.activeTool !== 'picker') {
    if (row === this.lastStrokeRow && col === this.lastStrokeCol) {
      return;
    }
    this.applyCanvasStroke(this.lastStrokeRow, this.lastStrokeCol, row, col);
    this.lastStrokeRow = row;
    this.lastStrokeCol = col;
  }
}

工程上的收益

这种拆分让触控映射、绘制插值和图层合成分别可测,也更容易适配横竖屏、矩形画布和缩放拖动画布。对 HarmonyOS ArkUI Canvas 工具来说,稳定的坐标系统往往比画笔样式本身更重要。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐