App.svelte (root state, RAF loop, history, keyboard shortcuts)
├── TopBar.svelte (artboard size, presets, export, file I/O, theme)
├── Viewport.svelte (canvas, zoom/pan, hit test, draw, select, drag)
├── RightPanel.svelte (tabs: Layers, Palettes, Patterns, Samples)
│ ├── CodeEditor.svelte (CodeMirror 6, JS syntax, completions)
│ ├── ColorPicker.svelte (HSV square + hue slider + hex input)
│ ├── GradColorEditor (gradient stops, linear/radial, angle)
│ └── SliderRow.svelte (labeled slider + number input)
├── BottomCodePanel.svelte (docked code editor, resizable)
└── StatusBar.svelte (zoom controls, CMYK proof toggle)
All state lives in App.svelte using Svelte 5 runes. Child components receive props + callbacks.
| Variable | Type | Persistence |
|---|---|---|
layers |
Layer[] |
autosave (localStorage) |
activeLayerId |
string | null |
autosave (index) |
activeShapeId |
string | null |
session only |
selectedShapeIds |
string[] |
session only |
artW, artH |
number |
autosave |
customPalettes |
Palette[] |
autosave |
customPatterns |
Pattern[] |
autosave |
zoom, panX, panY |
number |
session only |
theme |
'dark' | 'light' |
localStorage fooorma_theme |
panelWidth |
number |
localStorage fooorma_panel_w |
codePanelPos |
'right' | 'bottom' |
localStorage fooorma_code_pos |
codePanelH |
number |
localStorage fooorma_code_h |
activeTab |
tab union | session only |
exportScale |
number |
session only |
exportFormat |
'png' | 'cmyk-tiff' |
session only |
- Key:
fooorma_autosave - Debounced 500ms via
$effect - Saves:
{ layers, artW, artH, customPalettes, customPatterns, activeIdx }
past/futurearrays ofHistoryEntry(max 50)HistoryEntry = { layers, activeShapeId, selectedShapeIds }- Call
commit()before any mutation - Code editor changes do NOT commit (CodeMirror has its own undo)
resolvedLayers— layers with code-mode shapes evaluatedallPalettes—[...BUILTIN_PALETTES, ...customPalettes]allPatterns—[...BUILTIN_PATTERNS, ...customPatterns]drawableLayerId— null if active layer is code mode (disables canvas draw)
Shape
├── type: ShapeType (rect|ellipse|line|curve|triangle|arc|spline|group|cube|sphere|cylinder|torus)
├── color: ShapeColor { hex, opacity, gradient? }
├── geom: ShapeGeom { x, y, w, h } ← all shapes have this (center + size)
├── pts?: number[] ← line/curve/triangle/spline flat coords
├── strokeWidth?: number ← line/curve only
├── stroke?: ShapeStroke ← border (color, width, align, join, gradient)
├── transform?: ShapeTransform ← rotate, scale, skew, 3D angles
├── effects?: ShapeEffect[] ← shadow, blur, bevel, noise, warp, material
└── children?: Shape[] ← group only
- Position:
x= fraction of artW,y= fraction of artH.(0.5, 0.5)= center. - Size:
wandhare both artW-fractions (uniform space). Equal w/h = square regardless of aspect ratio. - pts: flat
[x0,y0, x1,y1, ...], each in normalized 0–1 space.
Layer— id, name, visible, bgColor?, mode (manual|code), shapes[], queryPattern— id, name, type, shape, color, opacity, count, cols, rows, builtin?, code?Palette— id, name, colors[], builtin?Gradient— LinearGradient (angle + stops) | RadialGradient (cx, cy + stops)
The code sandbox runs user code via new Function(...) with these globals:
| Function | Signature |
|---|---|
rect |
(x, y, w, h, color?, opacity?, ...effects) |
ellipse |
(x, y, w, h, color?, opacity?, ...effects) |
triangle |
(x1, y1, x2, y2, x3, y3, color?, opacity?, ...effects) |
arc |
(cx, cy, r, startDeg, endDeg, color?, opacity?, ...effects) |
line |
(x1, y1, x2, y2, color?, opacity?, width?, ...effects) |
curve |
(x1, y1, cx, cy, x2, y2, color?, opacity?, width?, ...effects) |
spline |
(pts[], color?, opacity?, width?, ...effects) |
beginSpline/vertex/endSpline |
spline builder |
cube/sphere/torus |
(x, y, size, color?, opacity?, ...effects) |
cylinder |
(x, y, w, h, color?, opacity?, ...effects) |
| Function | Returns |
|---|---|
stroke(color, opacity?, width?, align?, join?) |
ShapeStroke |
rotate(deg) |
ShapeTransform |
transform({rotate?, scaleX?, ...}) |
ShapeTransform |
shadow(color?, opacity?, blur?, offsetX?, offsetY?) |
ShapeEffect |
blur(amount?) |
ShapeEffect |
bevel(intensity?) |
ShapeEffect |
noise(amount?) |
ShapeEffect |
warp(strength?, freq?) |
ShapeEffect |
material(kind, roughness?, intensity?) |
ShapeEffect |
grad(angle, ...stops) |
LinearGradient |
radGrad(...stops) |
RadialGradient |
| Function | Callback params |
|---|---|
repeat(n, cb) |
(i, t) — index, normalized 0→1 |
grid(cols, rows, cb) |
(c, r, ct, rt) — indices + normalized |
wave(n, amp, freq, cb) |
(i, t, x, y) — position on sine wave |
circular(n, cx, cy, r, cb) |
(i, t, x, y, angle) — radial position |
tile(cols, rows, cb, opts?) |
(c, r, ct, rt) — shapes in local 0–1 space |
- Shapes inside
tile()are drawn in tile-local 0–1 space, auto-mapped to grid cells mirror('x'|'y'|'xy')— flip shapes inside tile callback- Options:
{ offsetX, offsetY, gapX, gapY } - Remaps geom, pts, transforms from local to cell coordinates
w(v)/width(v)— identity (sizes are width-relative)h(v)/height(v)— converts height-fraction to width-fraction (v * H / W)W,H— artboard width/height constants
sin, cos, tan, abs, floor, ceil, round, sqrt, pow, min, max, randomlerp(a, b, t),clamp(v, lo, hi),map(v, a, b, c, d),fract(v),smoothstep(e0, e1, x)nz(x, y?)— Perlin-like noise 0–1PI, TAU, E
palette(name)→string[]of hex colorspalette(name, index)→ single hex (wraps modulo)
- MAX_SHAPES = 5000 (silently ignored beyond)
- Grid max 200×200 cells
After positional args, shapes accept stroke/transform/effects in any order. The collectTrailing() function auto-detects types via predicates (isEffect, isTransform, has 'width' for stroke).
- Iterates visible layers in order
- Layer background fill if
bgColorset - Per-shape: apply transform → fill → stroke → effects
| Effect | Technique |
|---|---|
| Shadow | ctx.shadowColor/Blur/OffsetX/Y |
| Blur | ctx.filter = 'blur(Npx)' |
| Bevel | Clipped gradients (light/dark) |
| Noise | 128×128 noise texture, overlay composite |
| Warp | Per-shape displacement via offscreen canvas |
- Euler rotation: Y→X→Z order
- Orthographic→perspective projection
- Painter's algorithm (face depth sorting)
- Light direction:
[-0.4, 0.6, -0.7] - Materials: default, metal (mirror specular), plastic (dull), marble (noise veins), glass (rim+transparency)
- Tessellation:
smoothparam (3–128 segments, default 32)
applyTransform(ctx, transform, cx, cy)— translate→skew→rotate→scalehexToRgba(hex, opacity)→ rgba stringmakeGradient(ctx, gradient, bbox)→ CanvasGradient
| Action | Behavior |
|---|---|
| Click empty | Deselect (+ start draw in manual mode) |
| Click shape | Select (switches layer if needed) |
| Shift+click | Toggle multi-selection |
| Drag from empty | Draw new rect |
| Drag selected | Move shape(s) |
| Space+drag | Pan |
| Wheel | Zoom (mouse-centered) |
| Middle-mouse | Pan |
- Top-to-bottom layer order, last shape first
- 6px pad for pts shapes (bounding box)
- Ellipse:
(dx/rw)² + (dy/rh)² ≤ 1 - 3D: uses
get3DBounds()for isometric bounds
MIN_ZOOM = 0.05,MAX_ZOOM = 20,MAX_RENDER_SCALE = 6
Key fooorma_autosave — JSON with layers, dimensions, palettes, patterns, active index.
// fooorma v1
// Artboard: 794 × 1123
// @palette "Name" #hex1 #hex2 ...
// @layer "Name" [hidden] [bg:#hex] [mode:manual]
rect(0.5, 0.5, 0.3, 0.3, '#8b5cf6', 0.85)
serializeProject()→ text,parseProject()→{ layers, artW, artH, customPalettes }
- PNG: render at
exportScale→canvas.toDataURL('image/png') - CMYK TIFF:
encodeCmykTiff(imageData)— naive RGB→CMYK, uncompressed, 300 DPI - CMYK proof:
applyCmykSoftProof(imageData)— in-place round-trip for preview
| Key | Action |
|---|---|
| Ctrl/Cmd+Z | Undo |
| Ctrl/Cmd+Shift+Z / Ctrl/Cmd+Y | Redo |
| Ctrl/Cmd+D | Duplicate selected |
| Ctrl/Cmd+C | Copy |
| Ctrl/Cmd+X | Cut |
| Ctrl/Cmd+V | Paste |
| Ctrl/Cmd+A | Select all in layer |
| Escape | Deselect |
| 0 | Fit to view |
| 1 | Zoom 100% |
| +/= | Zoom in |
| - | Zoom out |
data-theme="dark|light" on <html>. All colors via CSS variables.
Key variables: --bg-bar, --bg-panel, --bg-selected, --border, --text-1 through --text-6, --accent (purple), --viewport-bg.
CodeMirror has its own set: --cm-bg, --cm-text, --cm-keyword, --cm-string, --cm-number, etc.
- Add to
ShapeTypeunion intypes.ts - Add rendering in
renderer2d.ts(draw function + hit test) - Add to code API in
query/index.ts(shape function + expose innew Function+shapesToCode) - Add to RightPanel: shape type button, property controls, stroke/effects conditions
- Add to
apiSnippetsinapi-snippets.ts - Add to Viewport hit testing if non-standard bounds
- Add to
ShapeEffect.typeunion intypes.ts - Add rendering in
renderer2d.ts - Add API function in
query/index.ts+ expose in sandbox - Add toggle + controls in RightPanel Shaders section
- Add to
apiSnippets
- Add function in
query/index.ts - Expose in
new Functioncall - Add to
apiSnippets - Optionally add to template builder in RightPanel
Add to BUILTIN_PALETTES in src/lib/palettes/index.ts.
Add to BUILTIN_PATTERNS in src/lib/patterns/index.ts. Patterns with code field are stamp-type (raw code snippet in 0–1 local space).
function handleFoo(id: string, ...) {
commit() // snapshot for undo
layers = layers.map(l => ...) // immutable update
}const { layers, activeLayerId, onUpdateShape, ... }: { ... } = $props()Always create new arrays/objects — never mutate. Use layers.map(), spread operators, filter().
forAll(mkPatch) applies a patch to all selected shapes (or just active if single). forAllGeom(mkGeom) for position/size.
| File | Lines | Purpose |
|---|---|---|
src/App.svelte |
~1100 | Root state, RAF, history, keyboard, clipboard |
src/components/RightPanel.svelte |
~2300 | Tabs, shape editor, template builder, code editor |
src/components/Viewport.svelte |
~350 | Canvas, zoom/pan, draw, select, drag |
src/components/CodeEditor.svelte |
~200 | CodeMirror 6 wrapper |
src/components/TopBar.svelte |
~150 | Size presets, export, file I/O |
src/components/StatusBar.svelte |
~110 | Zoom display, CMYK toggle |
src/components/BottomCodePanel.svelte |
~80 | Docked code panel |
src/components/ColorPicker.svelte |
~80 | HSV color picker |
src/components/GradColorEditor.svelte |
~80 | Gradient editor |
src/components/SliderRow.svelte |
~80 | Reusable slider |
src/lib/query/index.ts |
~700 | Code sandbox, all shape/loop/effect functions |
src/lib/layers/renderer2d.ts |
~1000 | Canvas 2D rendering, 3D, effects |
src/lib/layers/types.ts |
~95 | All type definitions |
src/lib/persist/index.ts |
~120 | .ooo serialization |
src/lib/export-cmyk-tiff.ts |
~155 | CMYK TIFF encoding |
src/lib/palettes/index.ts |
~70 | Palette type + 8 builtins |
src/lib/patterns/index.ts |
~80 | Pattern type + 6 builtins |
src/lib/api-snippets.ts |
~200 | 35+ API snippet definitions |
src/lib/viewport.ts |
~5 | Zoom/scale constants |
src/lib/editor-font.ts |
~30 | Code editor font size (9–20px) |
| Key | Content |
|---|---|
fooorma_autosave |
Full project JSON |
fooorma_theme |
'dark' or 'light' |
fooorma_panel_w |
Right panel width (px) |
fooorma_code_pos |
'right' or 'bottom' |
fooorma_code_h |
Bottom panel height (px) |
fooorma_code_font |
Editor font size (px) |