Scritch
The .scritch scene format
A Scritch drawing is plain JSON. You can write one by hand, generate it from a script, or have a model emit it — then drop the file on the canvas and keep editing. This page is the whole format.
Start from a working example
flowchart.scritch is a complete diagram — bound elbow arrows, labels on shapes and on arrows, a wrapped caption. Download it, drop it on the canvas, then open it in an editor to see how each piece is written.
Getting a file onto the canvas
There are three ways in, and they all run the same validation:
- The import button in the toolbar — the one beside Download. It works on an empty canvas.
- Drag and drop a
.scritchfile anywhere onto the canvas. - Sketches menu (the folder icon) → “Open .scritch file…”. The same menu has “Save as .scritch file” to get one back out.
- Scripted —
window.scritch.load(), covered at the bottom.
Opening a file replaces the canvas, but it lands on the undo stack — ⌘Z brings your drawing back.
The smallest useful file
A bare array of shapes is accepted, so the shortest thing that draws something is one line. Ids, colours and stroke widths all have defaults.
[
{ "type": "rect", "x": 0, "y": 0, "w": 160, "h": 80 }
]The full wrapper adds a type tag and a version, and is what “Save as .scritch file” writes:
{
"type": "scritch",
"version": 1,
"shapes": [
{ "id": 1, "type": "rect", "x": 40, "y": 40, "w": 200, "h": 100,
"color": "#3b82f6", "width": 4 },
{ "id": 2, "type": "text", "text": "for each card",
"size": 22, "color": "#3b82f6", "containerId": 1 },
{ "id": 3, "type": "diamond", "x": 480, "y": 260, "w": 220, "h": 140,
"color": "#f97316", "width": 4 },
{ "id": 4, "type": "text", "text": "threw?",
"size": 22, "color": "#f97316", "containerId": 3 },
{ "id": 5, "type": "arrow", "x1": 240, "y1": 90, "x2": 480, "y2": 330,
"color": "#64748b", "width": 4,
"startBinding": 1, "endBinding": 3, "route": "orthogonal" },
{ "id": 6, "type": "text", "text": "next",
"size": 18, "color": "#64748b", "containerId": 5 }
]
}That example is worth reading twice: the two labels have no coordinates because containerId centres them in their box, and the arrow needs no elbow maths because startBinding/endBinding plus route let the renderer work out the path.
Every shape
Each entry needs a type. Coordinates are world pixels — y grows downwards, and there is no canvas size to stay inside.
idint- Unique. Referenced by bindings, labels and groups. Assigned for you if you leave it out.
colorstring- Stroke/text ink.
#rgb,#rrggbb(aa),rgb()/rgba(). Defaults to the theme ink. bgColorstring | null- Fill for a closed shape, painted as a light wash of the colour.
fillStyleenumsolid·hachure·cross-hatchwidthnumber- Stroke width in px.
strokeStyleenumsolid·dashed·dottedopacity0–1- Defaults to 1.
rotationradians- About the shape’s centre.
groupIdint- Shapes sharing one move and select together.
lockedboolean- Not selectable until unlocked from the right-click menu.
x, y, w, hnumberrequired- Top-left corner and size. Required.
x1, y1, x2, y2numberrequired- The two endpoints. Required even when bound — they’re where the connector starts from.
startBinding, endBindingint | null- Ids of the shapes the ends attach to. A bound end follows its shape when you move it, and gives an elbow route the box geometry to leave from.
routeenumdirect(default) draws a straight or hand-bent line.orthogonaldraws right-angle elbows that leave and enter bound boxes perpendicular to an edge and step around other boxes in the way.bend, bendAlongnumber- Curve the shaft: perpendicular and along-line offset of its apex. Ignored by an orthogonal route.
startCap, endCapenum- Crow’s-foot cardinality for ER diagrams:
none·one·many·zero-one·one-many·zero-many. An end cap replaces the arrowhead.
textstringrequired- The content.
\nforces a line break. sizenumber- Font size in px.
fontFamilyenumcode(default) ·sans·mono·handx, ynumber- Top-left origin, for a free-standing label.
containerIdint- Makes this the label of another shape. Drop
x/y: it centres in a box (shrinking to fit) or sits in a gap in a connector’s shaft. boxWnumber- Wrap boundary for a free-standing label, in px — the text flows and wraps inside this width instead of running on. Drag a label’s side handle on the canvas to set it visually.
anchor{ nx, ny }- For a container label, where it sits in the box as a 0–1 fraction. Omit for dead-centre.
points[{ x, y }]required- A freehand stroke. An eraser stroke cuts through what’s beneath it.
x, y, w, hnumberrequired- Placement box.
srcdata URLrequired- A
data:image/png|jpeg|gif|webp;base64,…URL. Remote URLs and SVG data URLs are refused.
Everything the canvas can do, and how to write it
Anything you can draw or set in the UI has a field here — with two exceptions noted below, the file is the complete picture. Working the other way, a few things are only reachable by writing the file.
Rectangle · Ellipse · Triangle · Diamondtyperect·ellipse·triangle·diamond, each withx, y, w, h.Line · Arrowtypeline·arrowwithx1, y1, x2, y2. Drawing one from a shape to a shape is what setsstartBinding/endBinding.Pen · Erasertypepath·eraserwithpoints.Texttypetext. Double-clicking a shape instead of the canvas is what setscontainerId.Paste or drop an imagetypeimage. The app downscales and embeds it as a data URL, which is whatsrcholds.Eyedropper—- Copies styling between shapes. Nothing to store — it just writes the same style fields.
Stroke · Backgroundcolor, bgColor- The palette resolves to hex before it’s stored, so a file names colours directly.
Stroke width · Stroke stylewidth, strokeStyleFill stylefillStyle- Solid, hachure or cross-hatch, used when
bgColoris set. Routeroute- Direct or orthogonal — see the connector fields above.
Connector endsstartCap, endCapFont size · Fontsize, fontFamilyOpacityopacity
Bring to front / Send to backarray order- There is no z-index. Shapes paint in array order, so later in
shapesis on top — reordering the array is reordering the drawing. Group / UngroupgroupId- Any int; shapes sharing one select and move together.
LocklockedRotaterotation- Radians, clockwise, about the shape’s centre.
Align / Distributex, y- Pure geometry — the UI just computes coordinates, so a generator writes the result directly.
Drag a label’s side handleboxW- Sets the wrap boundary described above.
Drag an arrow’s midpointbend, bendAlong
sloppinessnumber0is the clean stroke the UI draws.1or2render hand-drawn and progressively rougher — there’s no control for this on the canvas.seednumber- Fixes the randomness of a sketchy stroke, so re-exporting gives a byte-identical result.
anchor{ nx, ny }- Places a container label off-centre at an exact fraction of its box, rather than wherever you clicked.
What a scene file doesn’t hold
A .scritch file is the drawing, not the session. These live in your browser and are deliberately left out, so opening someone’s file can’t disturb your setup:
- Pan and zoom. Opening a file frames the drawing for you;
load(scene, { fit: false })leaves the view alone. - Saved sketches and their names. Those are a browser-local library — a file is one drawing. Use the Sketches menu to name and keep it.
- Tool and panel state — the selected tool, snap-to-grid, canvas lock, and the styles queued up for the next shape you draw.
- Presentation mode and any laser marks made during it. Marks are never persisted at all.
- Undo history. Opening a file is itself one undoable step.
Sharing the whole thing instead? The share button packs the same shapes into a link, and the workspace backup in the top bar exports every sketch, snippet and setting at once.
What gets refused, and why
Import is strict on purpose, and it tells you what it did rather than failing silently. A shape is dropped if its type is unknown, its required geometry is missing, or it reuses an id. A field is stripped if it isn’t in this page or its value is the wrong type — so a typo means a missing colour, never an unnoticed one.
- Colours must match the notations above. A colour is written straight into an SVG attribute when you export, so free-form strings aren’t accepted.
- Images must be raster data URLs. An
image/svg+xmldata URL can carry script that would run when someone opens your exported SVG. - A binding or
containerIdpointing at a shape that didn’t survive is cleared, so nothing dangles. - Up to 5,000 shapes per file. A file claiming a version newer than 1 is refused rather than half-read.
Driving the canvas from code
While /scritch is open it exposes window.scritch. Everything goes through the same validation and the same canvas state as the UI — no storage key to guess at, and no reload.
// On /scritch, in the console or a Playwright script:
await new Promise((r) => (window.scritch ? r() : setTimeout(r, 100)));
window.scritch.load(scene); // replace the canvas (undoable)
const png = await window.scritch.exportPng({ scale: 2 });
const svg = await window.scritch.exportSvg();
window.scritch.save(); // -> a .scritch scene object
window.scritch.getShapes(); // -> the raw shapes array
window.scritch.bounds(3); // -> a shape's box (or the scene's)
window.scritch.route(5); // -> an elbow route's points
window.scritch.clear();
window.scritch.fit();load() takes a scene object, a bare shapes array, or the JSON text of either, and returns { ok, count, dropped, strippedFields } so you can see what it refused. Exports always frame the whole drawing, never the visible viewport — the scale option multiplies the bitmap, and is lowered automatically if the result would exceed what a browser canvas can hold.