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:

  1. 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.
  2. 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
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

  1. Add the polygon definition to the SHAPES object 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() (iterates Object.keys(SHAPES)).

Dependencies

  • polybooljs 1.2.0 — loaded via CDN before ship-creator.js. Provides PolyBool.union, PolyBool.segments, and PolyBool.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.