bitArt 系列 02:数据模型
做一个像素画编辑器最早会遇到的不是 Canvas 怎么画而是数据怎么存。如果只是画一个 16x16 的网格直接用二维数组也能跑。但真正的编辑器会很快长出更多需求多图层、锁定、透明度、撤销重做、动画帧、项目保存、图片导入、GIF 导出。这个时候数据模型如果一开始没想清楚后面的功能会越写越别扭。bitArt 的模型集中在D:\HM\bitArt\entry\src\main\ets\models\EditorModels.ets画布创建逻辑在EditorCanvasService.ets。这篇专门拆它的数据结构。1. 从一个像素开始为什么用 string 表示颜色bitArt 的像素矩阵底层使用string[]每个元素保存一个颜色值。常见值有#RRGGBB#RRGGBBAArgba(r,g,b,a)透明像素常量这种方式不一定是内存最省的但在 ArkTS 应用层有几个明显好处调试直观看到数组就知道颜色。和 Canvas 的fillStyle接口天然兼容。和导入导出、图层混合、JSON 保存的转换成本低。在 8x8 到 256x256 这种移动端轻量画布范围内性能压力可控。bitArt 对画布尺寸做了限制export class EditorCanvasService { static readonly MIN_CANVAS_SIZE: number 8; static readonly MAX_CANVAS_SIZE: number 256; static normalizeCanvasDimension(size: number): number { if (size EditorCanvasService.MIN_CANVAS_SIZE) { return EditorCanvasService.MIN_CANVAS_SIZE; } if (size EditorCanvasService.MAX_CANVAS_SIZE) { return EditorCanvasService.MAX_CANVAS_SIZE; } return Math.round(size); } }这个上限不是随便写的。像素编辑器一旦支持多图层、多帧和撤销栈实际内存不是width * height而是画布像素数 * 图层数 * 帧数 * 历史快照数量所以 bitArt 默认把自定义画布控制在 256 以内是一个比较务实的移动端选择。2. LayerItem 只保存元信息像素单独保存bitArt 的图层模型很轻export interface LayerItem { id: string; name: string; visible: boolean; locked: boolean; opacity: number; }注意这里没有pixels字段。图层的像素数据放在单独的string[][] layersPixels里。也就是说LayerItem[] 保存图层元信息 layersPixels[][] 保存每个图层的像素矩阵这种拆法看起来多了一层对应关系但很适合编辑器图层改名、显隐、锁定不需要复制像素数组。图层像素变化时可以只替换某一个图层的string[]。图层栈操作时layers和layersPixels可以同步 splice。做撤销快照时元信息和像素数据都能明确 clone。在LayerStackService.addLayer中可以看到这种结构const nextLayers LayerStackService.cloneLayers(layers); const nextLayersPixels LayerCanvasService.cloneLayersPixels(layersPixels); 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) );新增图层时元信息和像素矩阵一起插入同一个位置这样图层栈顺序就能保持一致。3. 新建画布默认图层加背景层新建空白画布时bitArt 默认创建两个图层layer-1可编辑透明图层。background锁定的白色背景层。核心逻辑在EditorCanvasService.createBlankCanvasstatic createBlankCanvas(width: number, height: number width): NewCanvasResult { const dimensions EditorCanvasService.normalizeCanvasDimensions(width, height); const layers: LayerItem[] [ { id: layer-1, name: 图层 1, visible: true, locked: false, opacity: 1 }, { id: background, name: 背景, visible: true, locked: true, opacity: 1 } ]; return { layers: layers, layersPixels: [ PixelCanvasService.createTransparentPixels(dimensions.width, dimensions.height), PixelCanvasService.createSolidPixels(dimensions.width, dimensions.height, #FFFFFF) ], activeLayer: layer-1, canvasWidth: dimensions.width, canvasHeight: dimensions.height, layerSerial: 2 }; }这个默认结构很接近常见图像编辑软件的心智模型用户画在上层背景层在底部兜底。背景层锁定可以避免用户无意中把底色擦空。4. Frame 是动画编辑的关键边界如果应用只做静态像素画项目里有一份图层数据就够了。但 bitArt 支持动画帧所以模型中有EditorFrameexport interface EditorFrame { id: string; name: string; durationMs: number; layers: LayerItem[]; layersPixels: string[][]; activeLayer: string; }这意味着每一帧都有自己的图层栈和像素矩阵而不是所有帧共用一套图层。这个设计很关键。动画编辑器里一帧的图层可见性、像素内容、当前激活图层都可能和另一帧不同。如果只保存“每帧一张合成图”后面想继续编辑图层就困难了如果所有帧共享图层切换帧时又容易互相污染。EditorFrameService.createFrame会深拷贝图层和像素static createFrame(id: string, name: string, durationMs: number, layers: LayerItem[], layersPixels: string[][], activeLayer: string): EditorFrame { return { id: id, name: name, durationMs: EditorFrameService.normalizeDuration(durationMs), layers: LayerStackService.cloneLayers(layers), layersPixels: LayerCanvasService.cloneLayersPixels(layersPixels), activeLayer: EditorFrameService.resolveActiveLayer(layers, activeLayer) }; }深拷贝的代价是内存更多但换来的是帧之间的数据隔离。对于编辑器来说这个取舍通常是值得的。5. Project 是可持久化的完整状态EditorPersistentProject是 bitArt 保存到本地的项目结构。它不仅保存画布像素还保存用户正在编辑时需要恢复的上下文export interface EditorPersistentProject { schemaVersion: number; projectId: string; projectName: string; updatedAt: number; canvasSize: number; canvasWidth?: number; canvasHeight?: number; activeTool: string; activeLayer: string; activeColor: string; activeFrame: string; frames: EditorFrame[]; gridVisible: boolean; autoSaveIntervalSeconds: number; exportImageFormat: string; layers: LayerItem[]; layersPixels: string[][]; undoStack: EditorSnapshot[]; redoStack: EditorSnapshot[]; }这里有几个细节值得注意。第一项目里同时有canvasSize、canvasWidth、canvasHeight。早期很多像素画工具只支持正方形画布但实际导入图片或自定义画布时独立宽高非常必要。第二项目保存了activeTool、activeColor、activeLayer、activeFrame。这让用户重新打开项目时可以回到接近离开时的状态。第三undoStack和redoStack也被保存了。很多轻量应用会在退出后丢弃撤销历史但 bitArt 把它纳入项目模型体验会更接近桌面编辑器。6. Snapshot 让撤销重做可恢复撤销快照模型是export interface EditorSnapshot { layers: LayerItem[]; layersPixels: string[][]; activeLayer: string; canvasSize: number; canvasWidth?: number; canvasHeight?: number; historyLabel?: string; }它没有保存全部项目字段而是只保存和一次编辑操作相关的画布状态。这个粒度比较合理撤销一次画笔操作时不应该把用户的导出格式、面板开合状态也一起回滚。在EditorHistoryService中记录历史时会 clone 快照并限制最大数量static record(undoStack: EditorSnapshot[], snapshot: EditorSnapshot, maxHistory: number EditorHistoryService.MAX_HISTORY): EditorSnapshot[] { const nextStack: EditorSnapshot[] undoStack.slice(); nextStack.push(EditorHistoryService.cloneSnapshot(snapshot)); if (nextStack.length EditorHistoryService.normalizeMaxHistory(maxHistory)) { nextStack.shift(); } return nextStack; }这就是编辑器应用里常见的平衡撤销要可靠但不能无限占内存。7. 用隔离性检查模型是否可靠数据模型是否合格可以通过几组隔离测试判断。复制一个帧后修改新帧的图层像素原帧不能跟着变化图层改名、显隐和锁定时像素数组内容不应被重建创建 32×64 画布后每层像素数量必须始终等于 2048撤销一次画笔操作时导出格式、自动保存间隔和面板开合不能一起回退。项目序列化后再加载还要检查活动图层和活动帧是否仍指向有效 ID。若导入数据缺少可选宽高应从兼容字段恢复若帧时长、画布尺寸或历史数量越界则由服务层归一化。模型可靠的标准不是字段齐全而是复制、切换、保存和恢复后仍保持数据所有权。8. 这个模型的扩展空间bitArt 当前的数据模型已经能支撑一个完整移动端像素画编辑器。后续如果继续扩展还可以往几个方向演进如果画布尺寸继续放大可以把string[]改为更紧凑的二进制表示。如果颜色数量固定可以用调色板索引代替颜色字符串。如果撤销栈过大可以从完整快照切换到命令增量。如果需要跨设备同步可以给帧、图层、操作记录增加更稳定的版本字段。但在移动端 MVP 到成熟工具之间bitArt 当前的设计是很实用的模型清晰序列化简单服务层容易操作UI 层也不用理解底层细节。下一篇继续看画布渲染bitArt 如何在 ArkUI Canvas 上处理像素网格、透明色和脏区域刷新以及怎样合并同一行的连续色块来减少fillRect次数。

相关新闻

Pandas多维聚合实战:构建可钻取的数据立方体

Pandas多维聚合实战:构建可钻取的数据立方体

1. 项目概述:当数据不再是一张“平铺直叙”的表格你有没有遇到过这样的场景:销售部门要按季度、按区域、按产品大类看毛利,同时还要对比去年同期;财务团队需要把成本拆解到“部门-项目-费用类型-发生月份”四个维度,再…

2026/7/13 5:15:35阅读更多 →
Web安全实战:业务逻辑漏洞挖掘技巧与六大核心攻击面解析

Web安全实战:业务逻辑漏洞挖掘技巧与六大核心攻击面解析

1. 项目概述:为什么业务逻辑漏洞是Web安全的“隐形杀手”?刚入行做渗透测试那会儿,我和很多新手一样,满脑子都是SQL注入、XSS、文件上传这些“经典”漏洞。拿着扫描器一顿狂扫,看到个报错信息就兴奋得不行。直到有一次…

2026/7/13 5:15:34阅读更多 →
Ford-Fulkerson 算法实战:Python 实现最大流问题,5步求解23万吨/小时案例

Ford-Fulkerson 算法实战:Python 实现最大流问题,5步求解23万吨/小时案例

Ford-Fulkerson算法实战:Python实现最大流问题与23万吨/小时案例解析在系统架构设计师的软考备考过程中,图论中的最大流问题是一个既基础又关键的知识点。本文将从实战角度出发,通过Python代码完整实现Ford-Fulkerson算法,并逐步解…

2026/7/13 5:10:34阅读更多 →
VC++医院门诊收费系统优化:异步改造与性能提升实战

VC++医院门诊收费系统优化:异步改造与性能提升实战

1. 项目概述:为什么VC依然是医院核心业务系统的“定海神针”最近和几个在医院信息科工作的朋友聊天,他们都在为系统升级换代头疼。新来的年轻工程师总想用Java、.NET Core甚至Flutter来重写那些“老掉牙”的门诊收费系统,觉得VC(V…

2026/7/13 6:20:40阅读更多 →
数据科学面试避坑指南:9个致命错误与业务导向型解法

数据科学面试避坑指南:9个致命错误与业务导向型解法

1. 这不是“背题清单”,而是一份数据科学面试避坑实录我带过37个转行进数据科学的学员,也作为技术面试官参与过62场数据岗终面——从一线互联网大厂到金融风控团队,再到医疗AI初创公司。每次面试结束复盘,最常听到的反馈不是“算法…

2026/7/13 6:20:40阅读更多 →
C++ std::list 深度解析:双向链表的性能优势与实战应用

C++ std::list 深度解析:双向链表的性能优势与实战应用

1. 项目概述:为什么C程序员绕不开std::list?如果你刚开始学习C的STL(标准模板库),在熟悉了vector和array之后,下一个大概率会遇到的容器就是std::list。很多教程会告诉你,它是一个“双向链表”&…

2026/7/13 6:20:40阅读更多 →
FreeRTOS 信号量 vs 队列:5个维度性能对比与选型指南

FreeRTOS 信号量 vs 队列:5个维度性能对比与选型指南

FreeRTOS 信号量与队列深度对比:5大核心维度解析与实战选型策略1. 同步机制的本质差异与适用场景在嵌入式实时系统中,任务间的通信与同步是系统设计的核心挑战。FreeRTOS提供了两种基础机制:信号量(Semaphore)和队列&a…

2026/7/13 6:20:40阅读更多 →
Linux 提权实战:Zico2 靶场中 2 种 Sudo 权限滥用方法(tar/zip)原理与复现

Linux 提权实战:Zico2 靶场中 2 种 Sudo 权限滥用方法(tar/zip)原理与复现

Linux 提权实战:Zico2 靶场中 2 种 Sudo 权限滥用方法(tar/zip)原理与复现在渗透测试和安全研究中,Linux 系统的权限提升(Privilege Escalation)是一个至关重要的环节。Zico2 靶场作为 VulnHub 上的经典练习…

2026/7/13 6:20:40阅读更多 →
AI FDE 能力模型详解:从价值判断到资产复用的七项能力

AI FDE 能力模型详解:从价值判断到资产复用的七项能力

结论前置:之前我把 FDE 的能力拆成了五项。最近做了几个项目后,我发现还有两个维度一直没讲清楚,却会直接影响项目能否进入真实业务流程。今天把模型升级到七项,并补上一道经常被忽略的前提:一把手认知。 为什么要升级…

2026/7/13 6:15:39阅读更多 →
VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异当你在VSCode中启动一个新的TypeScript项目时,第一个技术决策往往从安装方式开始。这个看似简单的选择——全局安装还是项目本地安装——实际上会深刻影响你的开发流程、团队协作和…

2026/7/13 4:47:19阅读更多 →
智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏…

2026/7/13 0:50:34阅读更多 →
Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否在GOG或Epic Games Store购买了心仪的游戏…

2026/7/13 4:52:09阅读更多 →
卡梅德生物技术快报|蛋白质分离纯化:肠激酶可溶性原核表达 + 两步层析全参数|标准化蛋白质分离纯化 SOP

卡梅德生物技术快报|蛋白质分离纯化:肠激酶可溶性原核表达 + 两步层析全参数|标准化蛋白质分离纯化 SOP

研究痛点提出(提出问题)重组肠激酶是融合标签切除核心工具酶,当前原核表达体系存在三大标准化难题,直接阻碍可复现的蛋白质分离纯化流程搭建:Trx、GST、单 SUMO 标签融合产物绝大多数为包涵体,沉淀占比超 9…

2026/7/13 0:04:58阅读更多 →
语音转文字工具AsrTools:让音频整理变得简单高效

语音转文字工具AsrTools:让音频整理变得简单高效

语音转文字工具AsrTools:让音频整理变得简单高效 【免费下载链接】AsrTools ✨ AsrTools: Smart Voice-to-Text Tool | Efficient Batch Processing | User-Friendly Interface | No GPU Required | Supports SRT/TXT Output | Turn your audio into accurate text …

2026/7/13 0:04:58阅读更多 →
Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改 【免费下载链接】palworld-save-tools Tools for converting Palworld .sav files to JSON and back 项目地址: https://gitcode.com/gh_mirrors/pa/palworld-save-tools 你是否曾经想要调整Palwor…

2026/7/13 0:04:58阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/13 4:21:17阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/13 4:08:53阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/12 21:43:43阅读更多 →