Build Stencil

YESback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

ShellTauri 2 with a React and TypeScript interfaceRenderingOne pure render function over a document, drawing to a 2D canvasDocumentA versioned JSON document with assets stored beside itTextFonts loaded and awaited before any render, with the resolved family recordedExportOffscreen render at an explicit scale, then read the bytes back and check them

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DOCUMENTS_DIRrequired~/Documents/StencilWhere bundles are saved. Back this up like any other document folder.
ASSETS_DIRrequired~/Documents/Stencil/.assetsImported originals, addressed by hash. Never written to after import.
AUTOSAVE_SECONDSrequired20Also runs on window blur. Writes to a temporary file and replaces atomically.
MAX_EXPORT_SCALErequired4Upper bound offered in the interface, further limited by the probed canvas maximum.
EXPORT_DIRrequired~/Pictures/StencilWhere finished images land. Failed exports leave nothing here.

The build, in order

  1. A document and one renderer

    Every pixel on screen comes from a pure function of the document.

    1. One function from document and scale to pixels, with no hidden state.

      Files src/render/render.tssrc/model/document.ts

    2. Undo is then a list, not a pile of special cases.

      Files src/model/patch.tssrc/model/history.ts

    3. An incremental draw and a full redraw must produce identical pixels.

      Files tests/render-parity.test.ts

    done when · tick each as it passes
    watch 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.
  2. Text that wraps the same at every scale

    A headline exported at four times the preview scale breaks lines in the same places.

    1. The font loading interface tells you when the face is really there.

      Files src/render/fonts.tstests/font-race.test.ts

    2. Scaling a wrapped line is fine, wrapping a scaled line is not.

      Files src/render/text.ts

    3. So 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 passes
    watch 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.
  3. Images that keep their original

    Importing a photograph never damages the file the user chose.

    1. The asset folder holds originals. Derived copies live elsewhere.

      Files src/assets/import.ts

    2. Applying it twice is how a photograph ends up sideways in the export only.

      Files src/render/image.tstests/orientation.test.ts

    3. The preview copy exists for speed and nothing else.

      Files src/render/render.ts

    done when · tick each as it passes
    watch 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.
  4. An export that cannot fail quietly

    Either a correct file lands on disk or the user is told why not.

    1. Draw, read a pixel back, and store what actually worked.

      Files src/export/limits.ts

    2. Bytes on disk that no decoder accepts are not an export.

      Files src/export/write.tstests/export-verify.test.ts

    3. A half-written image in the output folder will be sent to someone.

      Files src/export/write.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more design.