Ship Creator
Interactive ship builder at /playground/ship-creator. Build symmetrical space-ship polygons by placing, rotating, and unioning geometric shapes on a mirrored canvas. Paint your designs with procedural passes.
Overview
| Aspect | Detail |
|---|---|
| URL | /playground/ship-creator |
| JS source | static/js/ship-creator.js — IIFE, ~2600 lines |
| Template | templates/ship-creator.html |
| Libraries | polybooljs (boolean polygon union), JSZip (zip save/load) |
| Server | src/routers/ships.py — ship + brush API endpoints |
| Auth | Token-based (crypto.randomUUID(), stored in localStorage) |
| Persistence (session) | localStorage key ship-creator-autosave |
| Persistence (server) | Zip files in data/ships/{token}/, brushes in data/brushes/{token}/ |
| Palettes | data/palettes/*.hex — 13 Lospec palettes served on page load |
Page Layout
Four main tabs at the top:
Editor tab
Canvas (800×600 logical, HiDPI-scaled) + sidebar. Build your ship polygon here.
Sidebar controls:
| Section | Controls |
|---|---|
| Status | Context-sensitive message ("Select a shape from the Brushes tab", etc.) |
| Rotation | ±5° fine-tune, 0/45/90/180 snap buttons, Shift+scroll |
| Size | 0.25×–2.50× range slider |
| View | Zoom (−/⊡/+ buttons), Ctrl+scroll zoom, two-finger pan, right-click pan |
| Actions | Undo (infinite stack), Clear, ✂ Cut Brush (drag a rectangle to extract a shape) |
Brushes tab
Three sections: - Brushes — 8 built-in primitives (Triangle, Square, Pentagon, Hexagon, Star, Rectangle, Diamond, Circle) - Saved Ships — ships saved to the server, loaded as reusable shapes - Custom Brushes — brush shapes cut from your ship via ✂ Cut Brush, with × delete button
Selecting any shape/brush navigates to whichever workspace tab (Editor or Paint) was last active.
Paint tab
Dedicated paint canvas (auto-sized to ship) + three-column sidebar: Canvas | Sub-tabs | Palette.
Sub-tabs: - Brush Settings — brush size, rotation + snap, opacity (spray effect), Paint/Erase mode toggle - Procedural Tools — pass selector dropdown, configurable parameters, Apply Pass button
Palette column (always visible): - Colour swatches (from active palette) - Custom colour picker - Palette selector, Save, Delete, Import (.hex/.txt), Export buttons - Ship Fill colour picker - Ship Outline colour picker + stroke width slider
Undo and Clear buttons sit in their own row above the sub-tabs.
Ships tab
Save current ship to server, list saved ships, Load/Delete each.
Core Concepts
Shapes
All shapes defined as arrays of {x, y} points centred at (0, 0).
| Shape | Vertices | Notes |
|---|---|---|
| Triangle | 3 | Equilateral |
| Square | 4 | Axis-aligned |
| Pentagon | 5 | Regular |
| Hexagon | 6 | Regular |
| Star | 10 | 5-pointed |
| Rectangle | 4 | Wider than tall |
| Diamond | 4 | 45° rotated square |
| Circle | 32 | Approximated as polygon |
Saved ships and custom brushes add to _savedShapeDefs / _customBrushDefs and appear in the Brushes tab.
Mirroring
Shapes placed on the left half (x ≤ 400) are mirrored to x' = 800 - x and unioned via PolyBool. The result spans both halves with no internal seam.
Boolean Union
PolyBool's union() merges shapes. The toPBR/fromPBR helpers convert between {x,y} objects and [x,y] arrays (PolyBool's internal format).
Overlap Enforcement
After the first shape, every subsequent shape must overlap the existing ship. Preview colour: green = valid, red = rejected.
Painting
Manual paint
Select a colour from the palette or custom picker, choose a brush from the Brushes tab, set size and opacity, then paint on the dedicated paint canvas (or the editor canvas in paint mode). Opacity is baked into each stamp at creation time as rgba(). Mirror symmetry is automatic.
Procedural passes
The Procedural Tools tab allows configuring and running algorithmic paint passes:
| Pass | Default parameters | Description |
|---|---|---|
| Random | density: 1, colours: 3, rotations: 0/90 | Hull panels + panel lines |
| Shadow | density: 1, edgeDist: 0.2 | Edge gradient (black → fill colour) |
| Stripes | density: 1, orientation: both, thin: 0.01–0.03, long: 0.15–0.55 | Long thin rectangles |
| Bulkheads | density: 1, 0.04–0.1 × 0.12–0.37 | Thicker structural bands |
| Borders | density: 1, edgeThresh: 0.04 | Thin rectangles along polygon edges |
| Scratches | density: 1, maxAngle: 15° | Very thin, slightly rotated |
| Rivets | density: 1, size: 0.008–0.028 | Tiny square dots |
| Decals | density: 1, radius: 0.15, near centre | High-contrast coloured stamps |
Each pass has density, min/max width/height, and pass-specific parameters (orientation, edge threshold, max angle, etc.). Parameters persist in localStorage.
Paint mask
On save, stamps are flattened to a PNG mask at DPR resolution and stored in the zip. On load, the mask image is drawn clipped to the ship regions. Stamps are cleared — reloading is stamp-free.
Cut Brush
Click ✂ Cut Brush in the Editor sidebar, then drag a rectangle on the editor canvas. The intersection with the ship is computed via PolyBool.intersect(), centred, and posted to the server as a new custom brush. It appears in the Custom Brushes section of the Brushes tab with a delete × button.
Save / Load
Server API
| Method | Route | Purpose |
|---|---|---|
POST |
/api/ships/save |
Save ship as zip |
GET |
/api/ships/list |
List user's ships |
GET |
/api/ships/{slug} |
Download ship zip |
DELETE |
/api/ships/{slug} |
Delete ship |
POST |
/api/brushes/custom |
Save cut brush |
GET |
/api/brushes/custom |
List cut brushes |
DELETE |
/api/brushes/custom/{slug} |
Delete cut brush |
Token is crypto.randomUUID() stored in localStorage key ship-creator-token. Max 50 ships, 50 brushes per token.
Zip structure
ship.zip
├── regions.json — shipRegions array
├── polygon.json — merged polygon, centred at origin (for Brushes tab preview)
├── mask.png — flattened paint mask (HiDPI)
└── meta.json — name, dates, fillColor, strokeColor, strokeWidth
Save flow
Save button → renderPaintMask() → build zip via JSZip → POST /api/ships/save
Load flow
Load button → GET /api/ships/{slug} → JSZip.loadAsync() → restore regions + meta + mask
Colour Palettes
Built-in
13 Lospec 8-colour palettes served from data/palettes/*.hex and injected into the page template on load. Always available in the palette dropdown.
User palettes
Saved to localStorage key ship-creator-palettes. Import .hex or Paint.NET .txt files via the Import button. Export the active palette as a .txt file.
Palette structure
{
"Default": ["#ffffff","#aaaaaa","#555555","#000000","#e74c3c","#e67e22","#f1c40f","#2ecc71","#1abc9c","#3498db","#9b59b6","#e84393"],
"_active": "Default",
"ammo-8": ["#040C06", "#112318", ...],
...
}
Interaction Reference
Editor canvas
| Gesture | Behaviour |
|---|---|
| Click left half | Place selected shape + mirror |
| Click right half | Mirrored to left, then placed |
| Hold + drag left half | Paint shapes continuously |
| Ctrl+scroll | Zoom toward cursor |
| Scroll pan | Pan (when no shape selected) |
| Shift+scroll | Rotate selected shape |
| Right/middle-click + drag | Pan |
| Escape | Deselect shape, cancel cut mode |
Paint canvas
| Gesture | Behaviour |
|---|---|
| Click (within ship) | Stamp colour with current brush |
| Hold + drag | Paint stamps continuously |
| Ctrl+scroll | Zoom toward cursor |
| Scroll | Pan |
| Shift+scroll | Rotate brush |
| Right/middle-click + drag | Pan |
| Escape | Clear brush cursor from canvas |
Keyboard
| Shortcut | Action |
|---|---|
Ctrl+Z |
Undo |
Escape |
Deselect shape / clear brush preview / cancel cut mode |
Theme Integration
Canvas colours read from Bootswatch CSS variables. The theme colour cache is cleared on the #theme-css link's load event (not MutationObserver), so canvases update after the new stylesheet finishes loading.
| Element | CSS Variable | Alpha |
|---|---|---|
| Background | --bs-body-bg |
1.0 |
| Grid lines | --bs-border-color |
0.12 |
| Mirror axis | --bs-border-color |
0.40 |
| Labels | --bs-body-color |
0.18 |
| Ship fill | --bs-primary or custom |
0.20 |
| Ship stroke | --bs-primary or custom |
1.0 |
| Preview (valid) | --bs-success |
0.12 / 0.65 |
| Preview (invalid) | --bs-danger |
0.12 / 0.65 |
Code Structure
static/js/ship-creator.js (IIFE, ~2600 lines)
├─ Configuration (W, H, CX, SHAPE_R, SHAPES, PAINT_COLORS)
├─ Polygon Math (pointInPoly, segsInter, polysOverlap, transformPoly, mirrorPoly, centroid, scalePoly, scalePolyXY)
├─ PolyBool Helpers (toPts, fromPts, toPBR, fromPBR, unionRegionSets)
├─ State (shipRegions, paintStamps, paintMask, undo, zoom, pan, cutMode, ...)
├─ Theme Helpers (themeColor, themeAlpha, clearThemeCache)
├─ Canvas Setup (initCanvas)
├─ Drawing (drawRegions, draw — editor canvas with stamps/mask/preview/cut rect)
├─ Paint Canvas (initPaintCanvas, drawPaintCanvas, paintCanvasWorldPos)
├─ Interaction (getCanvasPos, mouse/wheel handlers, _doPlace, cut mode)
├─ Paint Stamp (renderPaintMask, _stampColor, _doPaintStamp)
├─ Undo / Autosave (pushUndo, saveAutosave, loadAutosave, snapRegions, snapStamps)
├─ Save / Load Server (saveShip, loadShip, deleteShip, refreshSavedList, getToken, apiUrl)
├─ Rebuild Shapes (rebuildSavedShapes, _rebuildShapeButtons, _rebuildCustomBrushButtons)
├─ Palette (buildPalette, selectShape)
├─ Pass Config (PASS_CONFIG, loadPassConfig, savePassConfig)
├─ Procedural Passes (autoPaint, addEdgeShadow, addStripes, addBulkheads, addPanelBorders, addScratches, addRivets, addDecals, _runPass)
├─ Shared Helpers (_shipBox, _randLeftInShip, _mirrorPair, _darksFromPalette, _lightsFromPalette)
├─ Cut Brush (_doCutBrush, _loadCustomBrushes, deleteCustomBrush)
├─ Paint UI (buildPaintUI, loadActivePalette, refreshPaletteDropdown, setPaintStatus)
├─ Button Wiring (wireButtons — includes palette import/export, pass wiring, procedural UI)
├─ Events (wireEvents)
├─ Loop (rAF)
└─ Init (init — ensures Default palette, loads server palettes, loads autosave)
Server Routes
All in src/routers/ships.py:
| Route | Method | Description |
|---|---|---|
/api/ships/save |
POST | Save ship zip (token, slug, overwrite flag, file) |
/api/ships/list |
GET | List ships for token |
/api/ships/{slug} |
GET | Download ship zip |
/api/ships/{slug} |
DELETE | Delete ship |
/api/brushes/custom |
POST | Save custom brush polygon |
/api/brushes/custom |
GET | List custom brushes |
/api/brushes/custom/{slug} |
DELETE | Delete custom brush |
Storage paths: data/ships/{token}/{slug}.zip, data/brushes/{token}/{slug}.json
Palettes: data/palettes/*.hex (read by src/routers/ship_creator.py and injected into the page)
Adding a New Basic Shape
- Add the polygon definition to
SHAPESinship-creator.js:javascript shapename: [{x: -r, y: -r}, {x: r, y: -r}, {x: r, y: r}, {x: -r, y: r}], - The palette button is built automatically by
buildPalette(). - To make it available as a paint brush, add the name to the
brushNamesarray in any procedural pass.
Dependencies
- polybooljs 1.2.0 — CDN, provides
PolyBool.union,PolyBool.intersect,PolyBool.segments,PolyBool.polygon - JSZip 3.10.1 — CDN, client-side zip creation/extraction for save/load
- Bootstrap 5.3 — tabs, cards, utility classes
- python-multipart — form data parsing for save endpoint