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

  1. Add the polygon definition to SHAPES in ship-creator.js: javascript shapename: [{x: -r, y: -r}, {x: r, y: -r}, {x: r, y: r}, {x: -r, y: r}],
  2. The palette button is built automatically by buildPalette().
  3. To make it available as a paint brush, add the name to the brushNames array 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