← back to zenimate

zenimate — intro guide

A fuller tour of zenimate than the README: how to build it, how to drive the GUI, the file formats it reads and writes, and how the code is layered. For a one-paragraph overview and the quick build, see the README.

Building

make            # show all targets
make build      # the GUI into dist/
make build-gui  # raylib frontend, cgo-free (purego) — no system GL headers needed

The GUI links raylib. The default build-gui target uses raylib-go's purego path, which is cgo-free and builds anywhere. For the normal desktop runtime path you can instead build with cgo:

make build-gui-cgo

which needs the system development libraries raylib expects. On Debian/Ubuntu:

sudo apt-get install libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev \
                     libxinerama-dev libxi-dev libxxf86vm-dev libwayland-dev \
                     libxkbcommon-dev

Requirements: Go 1.25 or later. The GUI at runtime needs an OpenGL-capable display; building it does not.

Controls

Left-drag paints, right-drag erases. Panning uses space as a modifier (hold space and left-drag) or a middle-mouse-button drag; either pans the viewport. The mouse wheel zooms in and out toward the cursor. Both zoom and pan carry glide-after-release inertia. Enter toggles play/stop; [ ] and 1..9 change frame. Esc does not close the window.

The frame strip runs along the top, with a scrubber slider above it (drag to move between frames) and - / + buttons to add or remove frames (1–16). Right-click a frame button for its context menu (Insert empty frame, Duplicate frame, Copy frame, Paste frame, Insert and paste, Delete frame); items disable themselves at the frame-count bounds and when the clipboard is empty. Press and hold, then drag a frame button past a small movement threshold to reorder it: a pulsating badge follows the pointer and an insertion line shows where it will land; release inside the strip to drop, or press Escape to cancel. The bottom buttons include 32x24 (the full 32×24-cell screen) and 2x2 size presets, per-cell width/height steppers (±1 cell each way), reset, play/stop, and separate copy and paste frame buttons. Sizing is non-destructive — existing pixels and attributes are preserved — and the viewport animates to fit the whole sprite when the size changes. Sprites can be any size up to 32×24 character cells (256×192 px).

Three view modes sit in a row under the frame strip:

Four transform buttons act on the selected frame: H FLIP and V FLIP mirror pixels and per-cell colour together; ROT 90 rotates clockwise in place, or — holding Ctrl or Option — also swaps a non-square frame's dimensions so nothing is clipped; INVERT flips every pixel (colour is untouched).

In the bitmap views, two onion-skin toggles overlay neighbouring frames as translucent silhouettes: the previous frame in red, the next in green (wrapping at the ends). Each toggles independently; onion skins are not shown in Spectrum Colour mode.

The frame strip and the view-mode/onion toolbars sit to the right of the title block, across the top, using the available width. Clicking the title collapses it to a small button (freeing horizontal space for the toolbars); clicking the button restores it. Keeping the toolbars on the top band leaves the viewport more vertical room.

The window is resizable: the editor cell size adapts so the whole grid stays visible at any window or sprite size, with dark-grey guide lines on the ZX 8×8 character-cell boundaries.

The preview is a fixed-size box (top-right) that shows the sprite at a fixed integer zoom (×1–×4, where ×N draws each sprite pixel as an N×N square), centred on the cursor's pixel while you hover the paint area, otherwise on the last pixel you edited. Right-click the preview to cycle the zoom. Press and hold (left button) to unfurl a full-sprite popup that grows out of the preview box — anchored to its top-right corner — and shrinks back on release.

Undo and redo cover painting, transforms, resizing, and frame changes (add, remove, insert, duplicate, move, clear, reset, paste) up to 100 levels back — a whole paint stroke counts as one action, however many pixels it touches.

File operations sit on the bottom button row and on shortcuts: Ctrl+O opens (a sprite, animation, screen, image, or a bundle to browse into); Ctrl+S saves back to the current source; Ctrl+Shift+S is Save As; Ctrl+E exports the current frame to a Spectrum format; Ctrl+Shift+E adds the sprite to a bundle; Ctrl+F toggles the save-extension form between long (.zani/.zbun) and 8.3 (.zan/.zbu); Ctrl+Z undoes, Ctrl+Shift+Z redoes; Ctrl+C/Ctrl+V copy/paste the selected frame. On macOS, Cmd works wherever Ctrl is shown for all of the above. Files can also be dragged onto the window: a sprite, animation, screen or image loads directly, and a dropped bundle opens the browser so you can pick an animation. The title block shows the current source (name.zani, name - bundle.zbun, or name (unsaved)) so it is always clear what Save will write to.

The HELP button (or F1) opens a scrollable reader listing all the shortcuts and explaining the file formats; scroll with the wheel, arrows, PgUp/PgDn or Home/End, and close it with Esc or the close box.

Files and formats

zenimate reads and writes these file types:

Which extension form is written — long (.zani/.zbun) or 8.3 (.zan/.zbu) — is a setting, toggled with Ctrl+F. Loading always accepts every form.

zenimate can also import a raw screen (.scr), pull a loading screen out of a tape (.tap/.tzx) or snapshot (.sna/.z80), and reduce a raster image (JPEG/PNG/GIF) to a Spectrum screen with a chosen fit strategy. Any of these can be opened from the dialog or dropped onto the window.

Save vs Save As. Ctrl+S saves back to wherever the sprite came from: a file is overwritten in place; a sprite opened from a bundle asks (once) whether to update it inside that bundle or split it off as a separate .zani; a brand new sprite prompts for a destination. Ctrl+Shift+S is Save As and always prompts. The title block shows the current source so it is always clear what Save will write to.

Design

pkg/bdf          standalone BDF font reader + rasteriser (no project deps)
pkg/zxpalette    ZX Spectrum palette + attribute encoding (cloned from zenzx)
pkg/zenui        renderer-agnostic UI toolkit (stdlib only): the file dialog,
                 the frame context menu, and every chooser modal share one
                 Renderer/Input contract and one Status lifecycle
pkg/version      build version, synced from VERSION
internal/fonts   embedded Sinclair + Cozette faces (decoded via pkg/bdf)
internal/model   the sprite document: variable frames, dims, per-frame
                 per-cell attributes, edits, observer hook
internal/ui      the frontend-independent controller
cmd/zenimate-gui raylib frontend (text via pkg/bdf only)

pkg/bdf is the reusable BDF font system: it was lifted from the subterm terminal renderer and stripped of its buffer/registry coupling so it stands alone. The GUI proves the "reuse the BDF system" intent end to end — it asks pkg/bdf to rasterise each glyph of the Sinclair face, uploads those pixmaps as raylib textures, and blits strings cell by cell.

Continuous integration and releases

GitHub Actions builds, vets, and tests on every push and pull request. Because the GUI uses raylib-go's cgo-free purego path, every binary cross-compiles from a single Linux runner with no per-platform toolchains.

Pushing a v* tag (for example v0.6.1) triggers the release workflow, which cross-compiles the GUI for each platform, bundles it, and publishes a GitHub Release with the archives attached. Release platforms:

Dependencies are ordinary published modules fetched from the Go proxy at build time (the UI toolkit lives in-tree at pkg/zenui), so CI needs no vendored tree. The release version is taken from the tag and written through the VERSION file and scripts/syncver.sh, keeping the compiled-in version consistent with the tag.

Note: the purego GUI embeds the raylib runtime for its target platform inside the compiled binary at build time and extracts it to a local cache on first run, rather than loading a separate system-installed raylib library — a released binary needs no additional runtime install beyond the OS's own graphics stack (X11/GLX and a GL implementation on Linux; the equivalent is already present on macOS and Windows).