原生鸿蒙像素画板实战 05:多图层系统

📅 2026/7/15 21:51:35
原生鸿蒙像素画板实战 05:多图层系统
图层是像素画编辑器从“涂鸦 Demo”走向“创作工具”的关键分水岭。没有图层时所有像素都在同一张画布上画错了只能撤销或擦掉。有了图层以后用户可以把线稿、阴影、背景、装饰拆开管理。对程序来说这意味着所有画笔操作都要从“改一张像素矩阵”变成“改当前图层再合成最终画面”。bitArt 的图层逻辑主要在两个服务里LayerStackService.ets负责图层栈操作。LayerCanvasService.ets负责图层像素操作和合成。1. 图层元信息和像素矩阵分离bitArt 的图层元信息很轻export interface LayerItem { id: string; name: string; visible: boolean; locked: boolean; opacity: number; }真正的像素放在layersPixels: string[][]。也就是layers[0] 对应 layersPixels[0] layers[1] 对应 layersPixels[1] background 对应最后一层背景像素这种结构让图层属性变化更便宜。比如改名、隐藏、锁定只需要复制LayerItem[]不用复制像素矩阵。2. 图层栈操作保持不可变更新LayerStackService的方法基本都返回新的LayerStackResultexport interface LayerStackResult { layers: LayerItem[]; layersPixels: string[][]; activeLayer: string; }新增图层时先 clone 原数组再插入新的元信息和透明像素矩阵const nextLayers LayerStackService.cloneLayers(layers); const nextLayersPixels LayerCanvasService.cloneLayersPixels(layersPixels); const activeIndex LayerCanvasService.getLayerIndex(nextLayers, activeLayer); const newLayerId layer- serial.toString(); nextLayers.splice(insertIndex, 0, { id: newLayerId, name: 图层 serial.toString(), visible: true, locked: false, opacity: 1 }); nextLayersPixels.splice(insertIndex, 0, PixelCanvasService.createTransparentPixels(width, height));这种写法很适合 ArkUI 状态刷新。返回新数组后页面层可以统一替换状态并刷新而不是在原数组上做难以追踪的原地修改。3. 锁定图层时工具操作直接拦截图层锁定不是 UI 装饰而是要在服务层生效。LayerCanvasService.applyTool会先判断当前图层是否锁定const layerIndex LayerCanvasService.getLayerIndex(layers, activeLayerId); if (layers[layerIndex].locked) { return { layersPixels: layersPixels, pickedColor: }; }这样即使页面误触发了绘制调用服务层也不会修改锁定图层。复杂应用里这种防线很有必要UI 状态可能出错但领域规则应该尽量兜底。4. 可见性和透明度参与合成最终显示到 Canvas 的不是某一层而是所有可见图层的合成结果。bitArt 用createCompositePixels遍历每个像素static createCompositePixels(layersPixels: string[][], layers: LayerItem[], width: number, height: number): string[] { const compositePixels: string[] new Arraystring(width * height); for (let row 0; row height; row) { for (let col 0; col width; col) { compositePixels[row * width col] LayerCanvasService.getCompositePixel(layersPixels, layers, width, row, col); } } return compositePixels; }单个像素的合成逻辑从底层往上叠for (let index layersPixels.length - 1; index 0; index--) { if (!layers[index].visible) { continue; } const color PixelCanvasService.getPixel(layersPixels[index], width, row, col); if (color ! PixelCanvasService.TRANSPARENT_COLOR) { const source LayerCanvasService.parseColor(color); source.alpha source.alpha * layers[index].opacity; blended LayerCanvasService.blendSourceOver(source, blended); } }这里处理了两个关键规则隐藏图层不参与合成。图层透明度会乘到像素自身 alpha 上。5. source-over 混合公式图层混合使用常见的 source-over 规则private static blendSourceOver(source: ColorRgba, destination: ColorRgba): ColorRgba { if (source.alpha 0) { return destination; } if (destination.alpha 0) { return source; } const alpha source.alpha destination.alpha * (1 - source.alpha); return { red: Math.round((source.red * source.alpha destination.red * destination.alpha * (1 - source.alpha)) / alpha), green: Math.round((source.green * source.alpha destination.green * destination.alpha * (1 - source.alpha)) / alpha), blue: Math.round((source.blue * source.alpha destination.blue * destination.alpha * (1 - source.alpha)) / alpha), alpha: alpha }; }这使得半透明颜色、图层透明度、橡皮擦降低 alpha 等能力可以共用同一套合成逻辑。6. 局部合成减少重算画布连续绘制时bitArt 不一定每次都重算整张合成图。updateCompositePixels支持只更新脏区域static updateCompositePixels(compositePixels: string[], layersPixels: string[][], layers: LayerItem[], width: number, height: number, minRow: number, minCol: number, maxRow: number, maxCol: number): string[] { const nextCompositePixels compositePixels.slice(); for (let row startRow; row endRow; row) { for (let col startCol; col endCol; col) { nextCompositePixels[row * width col] LayerCanvasService.getCompositePixel(layersPixels, layers, width, row, col); } } return nextCompositePixels; }这和 Canvas 脏区域刷新是配套的先局部合成再局部渲染。否则只优化 Canvas 绘制没有优化图层合成性能收益会打折。7. 合并图层要把透明度烘焙进像素图层向下合并时bitArt 不是简单丢掉一层而是先把上下两层像素混合成新的底层像素nextLayersPixels[targetIndex] LayerCanvasService.mergeLayerPixels( nextLayersPixels[activeIndex], nextLayers[activeIndex].opacity, nextLayersPixels[targetIndex], nextLayers[targetIndex].opacity, width, height ); nextLayers[targetIndex].opacity 1; nextLayers.splice(activeIndex, 1); nextLayersPixels.splice(activeIndex, 1);合并后目标层透明度被设为 1因为透明度已经进入像素颜色结果里。这个细节如果处理不好合并后视觉效果会变暗或变淡。8. 用叠色和锁定场景验证图层规则bitArt 的图层系统没有追求过度复杂但关键规则都需要能被实际验证图层元信息和像素矩阵分离。图层栈操作返回新状态适合 ArkUI 刷新。锁定图层在服务层拦截。可见性和透明度参与最终合成。局部合成配合脏区域渲染。合并图层时把 alpha 正确烘焙进像素。可以准备上下两层半透明色块分别切换显隐并调整透明度确认合成颜色符合 source-over锁定活动层后尝试画笔、填充和清空像素都不应改变合并图层前后截图对比视觉结果应一致同时目标层透明度应回到 1。再在连续绘制时记录脏区域确认局部像素变化没有触发整张图反复合成。对 HarmonyOS 应用来说这套结构也能迁移到贴纸编辑、图片标注、手写批注和地图绘制。只要有多层内容叠加就可以把“层管理”和“像素合成”拆成两个服务维护。下一篇会继续处理图层状态的历史记录说明撤销与重做为什么采用快照以及快照边界应该保存到哪里。