Ship Creator
Interactive ship builder on the playground page. Build symmetrical space-ship polygons by placing, rotating, and unioning geometric shapes on a mirrored canvas.
Overview
The Ship Creator is a client-side Canvas 2D tool (no server component). It lives as the third experiment tab on the playground page, alongside Random Polygons and Random Continents.
| Aspect | Detail |
|---|---|
| Entry point | #ship-creator hash on the playground page |
| JS source | static/js/ship-creator.js — IIFE, ~680 lines |
| Template | templates/playground.html — experiment div + sidebar |
| Library | polybooljs (CDN) for boolean polygon union |
| Persistence | localStorage key ship-creator-saves |
UI Layout
The interface is split into two tabs at the top of the section:
Editor tab (default)
A flex row with the canvas on the left and a sidebar on the right.
Canvas — 800×600 logical pixels, HiDPI-scaled via devicePixelRatio. A dashed vertical line marks the mirror axis at x = 400. The left half is labelled "edit here →", the right half "← auto-mirror". A faint 40px grid aids positioning.
Sidebar — 170px wide, contains: - Status message ("Select a shape from the Shapes tab") - Rotation: fine-tune buttons (±5°), snap buttons (0°, 45°, 90°, 180°), live display - Size: range slider (0.25× – 2.50×, step 0.25) - Actions: Undo, Clear - Save / Load: name input + Save button, saved ships list with Load/× buttons
Shapes tab
A standalone grid of selectable shape icons (no canvas or editor controls). Two sections:
- Basic — 6 built-in primitives (Triangle, Square, Pentagon, Hexagon, Star, Rectangle)
- Saved Ships — dynamically populated from localStorage, appears only when saved ships exist
Selecting a shape from the Shapes tab automatically switches to the Editor tab and updates the status to "Click on the left half to place".
Core Concepts
Shapes
All shapes are defined as arrays of {x, y} points centred at (0, 0).
| Shape | Vertices | Generation |
|---|---|---|
| Triangle | 3 | regularPoly(3, 22) |
| Square | 4 | regularPoly(4, 22) |
| Pentagon | 5 | regularPoly(5, 22) |
| Hexagon | 6 | regularPoly(6, 22) |
| Star | 10 | starPoly(5, 22, 8.8) — 5-pointed, outer radius 22, inner radius 8.8 |
| Rectangle | 4 | [{x:-22, y:-16}, {x:22, y:-16}, {x:22, y:16}, {x:-22, y:16}] |
Saved ships loaded as shapes are centred and stored in _savedShapeDefs — they appear alongside the basic shapes in the palette.
Mirroring
When a shape is placed on the left half of the canvas (click at x ≤ 400), a mirror copy is automatically created at x' = 800 - x (reflected across the vertical centre axis). The left shape and its mirror are immediately unioned via PolyBool so the result is a single merged polygon with no internal seam where the two sides overlap near the centre.
Boolean Union
PolyBool's union() operation is used at two points:
- Left + Mirror merge — when placing a shape, its left and mirror copies are unioned together. If they overlap (near the centre), the result is a single region. If they don't (far from centre), PolyBool returns two separate regions.
- Ship integration — the merged shape is unioned with the existing
shipRegions(array of polygon regions). PolyBool handles all complexity: merging overlapping regions, keeping disconnected regions separate, and producing clean vertex output.
The toPBR/fromPBR helpers convert between {x, y} objects (used throughout the code) and [x, y] arrays (expected by PolyBool's internal algorithms).
Overlap Enforcement
After the first shape is placed, every subsequent shape must overlap the existing ship. The preview uses anyRegionOverlap() to check every region pair between the new shape and the existing ship, colouring the preview:
- Green — valid (will union)
- Red — invalid (will be rejected)
If the overlap check fails on click, a red status message is shown and the shape is not placed.
Painting
Holding the left mouse button and dragging places shapes continuously. The paint step distance is 25 × shapeSize pixels (so small shapes paint densely, large shapes coarsely). A single undo snaps the entire paint stroke. Status messages are suppressed during painting.
Interaction Reference
Mouse
| Action | Behaviour |
|---|---|
| Click left half | Place selected shape + mirror at cursor |
| Click right half | Mirrored to left side, then placed |
| Hold + drag left half | Paint: place shapes along drag path (throttled) |
| Scroll | Rotate selected shape by ±5° |
| Move over canvas | Show preview of shape + mirror at cursor |
Keyboard
| Shortcut | Action |
|---|---|
Ctrl+Z |
Undo last placement / paint stroke |
Palette
| Element | Behaviour |
|---|---|
| Basic shape icon | Selects that shape, resets rotation, switches to Editor tab |
| Saved ship icon | Same — selectable as a reusable building block |
Sidebar controls
| Control | Behaviour |
|---|---|
| ← / → (rotation) | Rotate ±5° |
| 0° / 45° / 90° / 180° (snap) | Snap to exact angle |
| Size slider | Scale shape (0.25× – 2.50×) |
| Undo | Pop state stack (infinite undo) |
| Clear | Reset canvas (push undo first) |
| Name input + Save | Save current ship to localStorage |
| Load button | Load saved ship regions onto canvas |
| × (delete) | Remove saved ship from localStorage |
Data Flow
Placing a Shape
User clicks left half at (x, y)
→ getCanvasPos(e) converts CSS coords to logical coords (800×600)
→ placeShape(x, y)
→ getShape(selected) returns the polygon definition
→ scalePoly(shape, shapeSize) applies the size slider
→ transformPoly(scaled, x, y, rotation) rotates + translates
→ mirrorPoly(placed) reflects across x = 400
→ [placed, mirrored] are unioned (PolyBool)
→ if no existing ship: store directly as newRegs
→ if exists and overlaps: unionRegionSets(shipRegions, newRegs)
→ pushUndo() saves snapshot
Saving a Ship
Save button click
→ saveShip(name)
→ deep-copy shipRegions to localStorage (key: ship-creator-saves)
→ refreshSavedList() rebuilds the Load/× list
→ rebuildSavedShapes() centres each saved ship's polygon
and adds it to the Shapes tab palette
Loading a Ship (to canvas)
Load button click
→ loadShip(name)
→ pushUndo()
→ restore shipRegions from localStorage
→ canvas re-renders via rAF loop
Theme Integration
All canvas colours are read from Bootswatch CSS variables via getComputedStyle:
| 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 |
0.20 |
| Ship stroke | --bs-primary |
0.70 |
| Preview (valid) | --bs-success |
0.12 / 0.65 |
| Preview (invalid) | --bs-danger |
0.12 / 0.65 |
Colours are cached per session and cleared when data-bs-theme changes (via MutationObserver), so switching themes in the navbar updates the canvas immediately.
Code Structure
static/js/ship-creator.js — single IIFE, ~680 lines:
┌─ Configuration (W, H, CX, SHAPE_R)
├─ Shape Primitives (regularPoly, starPoly, SHAPES)
├─ Polygon Math (pointInPoly, segsInter, polysOverlap,
│ anyRegionOverlap, transformPoly, mirrorPoly,
│ centroid, scalePoly)
├─ PolyBool Helpers (toPts, fromPts, toPBR, fromPBR, unionRegionSets)
├─ State
├─ Status
├─ Canvas Setup (initCanvas)
├─ Theme Helpers (themeColor, themeAlpha, clearThemeCache)
├─ Drawing (drawRegions, draw)
├─ Interaction (getCanvasPos, mouse handlers, wheel, _doPlace)
├─ Save / Load (saveShip, loadShip, deleteShip, refreshSavedList,
│ rebuildSavedShapes)
├─ Palette (buildPalette, selectShape)
├─ Buttons (wireButtons)
├─ Events (wireEvents)
├─ Loop (rAF)
└─ Init
Key data structures
state.shipRegions = [ [{x, y}, ...], ... ] // array of polygons (regions)
state.undo = [ shipRegionsSnapshot, ... ] // infinite undo stack
_savedShapeDefs = { name: [{x, y}, ...], ... } // centred polygons for palette
localStorage = { "ship-creator-saves": { name: [regions], ... } }
Adding a New Basic Shape
- Add the polygon definition to the
SHAPESobject inship-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()(iteratesObject.keys(SHAPES)).
Dependencies
- polybooljs 1.2.0 — loaded via CDN before ship-creator.js. Provides
PolyBool.union,PolyBool.segments, andPolyBool.polygon. - Bootstrap 5.3 (already in base template) — tab component for Editor/Shapes switching, utility classes for layout.
- No server-side endpoints, no Python code.