Pixel Board on HarmonyOS: 32x32 Canvas, Bresenham and undo
Today I opened the Pixel Board case in the Huawei Cloud Harmony Practice Center (the case page is titled 像素画板, and its code lives in the GitCode repo harmony-practice-center/HarmonyOS-AtomSer-pixel-paint) and spent time reading how it is built. It is a small app with a big lesson: a 32 by 32 pixel canvas is enough to teach the core ideas of ArkUI Canvas drawing, state management and local persistence. This post records what I took away from it.
1. Why 32 by 32 is the right size
A 32 by 32 grid is 1,024 cells. That is small enough to keep the whole picture in a plain two-dimensional array in memory, and large enough to draw a recognisable character, icon or emoji. It also fits a phone screen: each cell is still big enough to tap, and the whole board can be drawn in one Canvas pass without any scrolling. The case stores the picture as data (a grid of colours), not as a bitmap, so saving, loading and undo all become array operations instead of image operations.
2. Three tools, three classic algorithms
The case offers a brush, a straight-line tool and a fill tool, and each one maps to a textbook algorithm.
The brush is the simplest: convert the touch point to a grid cell and set that cell to the current colour. The conversion is the part beginners get wrong. You must use the Canvas component's own size, not the screen size: column = floor((touchX - rectLeft) / rectWidth * 32), row = floor((touchY - rectTop) / rectHeight * 32), then clamp both to 0..31 so a finger that slips off the edge cannot write outside the array.
The line tool uses Bresenham's algorithm. Instead of interpolating floating-point points along the line and rounding them, Bresenham steps cell by cell with integer error accumulation, so the line is continuous with no gaps and no floating-point drift. If you draw a line by sampling points at fixed intervals, diagonal lines come out dotted; Bresenham is the fix.
The fill tool uses scanline flood fill. Starting from the tapped cell, it expands across connected cells of the same colour, row by row. The key detail is a visited set: without it, the fill revisits cells it has already painted and can loop or do far more work than necessary. The case also guards the obvious edge case - tapping a cell whose colour already equals the fill colour should do nothing, otherwise the fill would walk the whole board for no change.
3. Eraser and eyedropper are the same idea in reverse
The eraser is not a separate drawing engine. It writes the empty state into a cell, exactly as the brush writes a colour. The eyedropper reads a cell's colour and makes it the current brush colour. Once you see the grid as data, these tools are one-line operations, and that is the point of the design: the tools differ only in what value they write or read.
4. Undo and redo with two stacks
Undo is implemented with two stacks, one for undo and one for redo. Before every stroke the case pushes a snapshot of the grid onto the undo stack. Undo pops a snapshot and pushes the current grid onto the redo stack; redo does the reverse. The rule that is easy to forget: any new stroke must clear the redo stack, otherwise redo would resurrect a stroke from a branch of history that no longer exists. A snapshot per stroke (rather than per cell) keeps the logic simple and the memory cost trivial at 1,024 cells.
5. Saving: preferences plus a gallery with a cap
Works are saved through the preferences API, and the case keeps a gallery of up to 50 saved works. Loading a saved work replaces the current canvas, so the case asks for confirmation first - overwriting an unsaved drawing by accident is the classic data-loss bug in drawing apps. Deleting a gallery entry removes it from preferences. The 50-item cap matters: preferences are not a database, and an uncapped gallery would grow without bound.
6. Export: from grid to a real image
Export renders the grid into a PixelMap and saves it as PNG or JPEG, and the result can be shared through the system share sheet. The practical lesson is the separation: the grid is the source of truth, the PixelMap is only a render target at export time. That separation is also why the app can offer both a small pixelated preview and a clean exported file.
7. Four pitfalls I noted while reading the case
First, flood fill without a visited set is the most likely performance trap in a drawing app. Second, forgetting to clear the redo stack after a new stroke creates impossible history. Third, saving without a gallery cap slowly bloats local storage. Fourth, redrawing the whole Canvas on every touch-move event wastes frames; redrawing only the changed cells, or throttling redraws, keeps the brush feeling responsive.
8. What I verified today
I browsed this case in the practice center for more than 30 seconds and starred its GitCode repo from the case page - the star count went from 99 to 100. The case was published in the practice center on 2026-11-13. If you want a first HarmonyOS Canvas project, this is a good one to read end to end before you write your own.
A good next step after understanding it is to extend it: add a second layer, animate by saving successive grids as frames, or add a symmetry mode that mirrors each stroke. Each of those is a small change on top of the same grid, and that is exactly why the grid-first design is worth copying.
- 点赞
- 收藏
- 关注作者
评论(0)