Build Stencil
A local graphic editor for social images whose hard problems are one renderer shared by preview and export, text that lays out identically at every scale, and an export that fails loudly rather than handing back a blank canvas.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Builds the interface and runs the test suite.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why Tauri's shell is compiled, and the platform build tools have to be present before anything runs.
Get it rustup.rs, then follow the Tauri prerequisites for your operating system. open ↗
Verify
cargo --version prints a version and a Tauri dev build launches a window - have readyfree
Why Text is most of a social graphic, and the licence that lets you render with a font is not always the licence that lets you redistribute the file.
Get it Open licence families, or fonts already installed on your machine, with the licence noted for each.
Verify
Each font renders and its licence is recorded in the project - have readyfree
Why Orientation metadata and the canvas size limit are the two export bugs that only appear with awkward inputs.
Get it A phone photograph taken sideways, and an image of at least six thousand pixels on its long edge.
Verify
Both import and export correctly - decidefree
Why They are shipped as editable data with a checked date, so the source and the date have to exist before the first release.
Get it Write down each size, the platform it is for, and the date you read it from that platform's own guidance.
Verify
Every preset row has a source note and a date
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- documents(id, format_version, name, width, height, background, created_at, updated_at) - layers(id, document_id, ordinal, kind, x, y, w, h, rotation, opacity, blend, locked, hidden) · kind is text, image, shape or group - text_layers(layer_id, content, font_family, font_file_ref, size, line_height, letter_spacing, align, colour, wrap_width) - image_layers(layer_id, asset_id, crop_x, crop_y, crop_w, crop_h, fit) - assets(id, path, sha256, mime, width, height, bytes, imported_at) · the original file, never the resized copy - presets(id, name, width, height, source_note, edited_at) · sizes are data the user can edit, not constants in code - exports(id, document_id, preset_id, scale, path, pixel_w, pixel_h, bytes, renderer_version, at) - history(document_id, ordinal, patch_json, at) · undo is a stack of document patches, so the canvas can be rebuilt from nothing
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DOCUMENTS_DIR | required | ~/Documents/Stencil | Where bundles are saved. Back this up like any other document folder. |
ASSETS_DIR | required | ~/Documents/Stencil/.assets | Imported originals, addressed by hash. Never written to after import. |
AUTOSAVE_SECONDS | required | 20 | Also runs on window blur. Writes to a temporary file and replaces atomically. |
MAX_EXPORT_SCALE | required | 4 | Upper bound offered in the interface, further limited by the probed canvas maximum. |
EXPORT_DIR | required | ~/Pictures/Stencil | Where finished images land. Failed exports leave nothing here. |
The build, in order
A document and one renderer
Every pixel on screen comes from a pure function of the document.
One function from document and scale to pixels, with no hidden state.
Files
src/render/render.tssrc/model/document.tsUndo is then a list, not a pile of special cases.
Files
src/model/patch.tssrc/model/history.tsAn incremental draw and a full redraw must produce identical pixels.
Files
tests/render-parity.test.ts
done when · tick each as it passeswatch out- Mutating canvas state directly for a drag and syncing the document afterwards means undo restores a document the canvas disagrees with, and the disagreement only shows up after a redraw when the user has forgotten what they did. Apply the patch, then redraw from the document, every time, even while dragging.
Text that wraps the same at every scale
A headline exported at four times the preview scale breaks lines in the same places.
The font loading interface tells you when the face is really there.
Files
src/render/fonts.tstests/font-race.test.tsScaling a wrapped line is fine, wrapping a scaled line is not.
Files
src/render/text.tsSo a missing font is a visible state, not a different picture.
Files
src/model/document.tssrc/ui/LayerPanel.tsx
done when · tick each as it passeswatch out- Rendering text before the font has loaded draws the fallback face at the fallback metrics, and since it happens in milliseconds the preview looks right by the time anyone looks at it while the export, which runs once, keeps the wrong face forever. Await the font, and make the renderer refuse to draw a text layer whose font is not loaded.
Images that keep their original
Importing a photograph never damages the file the user chose.
The asset folder holds originals. Derived copies live elsewhere.
Files
src/assets/import.tsApplying it twice is how a photograph ends up sideways in the export only.
Files
src/render/image.tstests/orientation.test.tsThe preview copy exists for speed and nothing else.
Files
src/render/render.ts
done when · tick each as it passeswatch out- Downscaling on import to keep the preview quick means the export can only ever be as sharp as the preview copy, and the loss is discovered when someone prints or posts at full size. Keep the original, render the preview from a derived copy, and render the export from the original.
An export that cannot fail quietly
Either a correct file lands on disk or the user is told why not.
Draw, read a pixel back, and store what actually worked.
Files
src/export/limits.tsBytes on disk that no decoder accepts are not an export.
Files
src/export/write.tstests/export-verify.test.tsA half-written image in the output folder will be sent to someone.
Files
src/export/write.ts
done when · tick each as it passeswatch out- A canvas larger than the platform maximum is not an error: the documentation states that exceeding the maximum dimensions or area renders the canvas unusable and drawing commands do not work, and one mobile platform caps it at 4,096 by 4,096, so a four times export of a large design returns a blank or empty file. Probe the real maximum at startup, refuse or tile above it, and verify the written file by decoding it.
Files, presets and getting outproduct builder
A document opens next year, presets are editable data, and nothing is trapped in the app.
A file that references images you no longer have is not a file.
Files
src/file/bundle.tsTemporary file, flush, replace, then recover on next launch.
Files
src/file/autosave.tstests/recovery.test.tsPlatform sizes change and a constant in code cannot be corrected by the user.
Files
src/presets/presets.tssrc/ui/PresetEditor.tsx
done when · tick each as it passeswatch out- Hard-coding the platform sizes in the code means the day a network changes its recommended dimensions every graphic made after that is quietly the wrong shape, and the fix needs a release. Make presets rows with a checked-on date shown in the interface, and let the user edit them.
That is the whole plan for Stencil. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A stock and template library.
- Cloud sync or collaboration.
- Direct publishing to social networks.
- Print colour management.
- cloud collaboration and mobile apps
- direct publishing reliability as platform APIs change
- pixel-perfect professional tooling
- large template and asset libraries
- color-management edge cases
- A background removal step, once you have decided which model licence you are willing to ship under
- Brand kits that are just saved colour and font sets applied to a document
- Batch export of one document across every preset, which reuses the export verification you already wrote
Need the files? The project pack on the verdict page hands your agent the whole brief · more design.