<!-- HAWAIIAN-COMPASS · part 1 -->
===== FILE: public/hawaiian-compass/README.md =====
# Hawaiian compass

A 360° navigational card in gold on black, taken from the Hawaiian directional names on the manuscript rose.

- **000** Ākau, north
- **090** Hikina, east
- **180** Hema, south
- **270** Komohana, west

The card is divided into 32 points of exactly 11.25°, and the outer limb is ticked at every degree. Drag the card, or type a bearing, to read the Hawaiian point, the offset from that point, the quadrant notation, the reciprocal, and the northing and easting of a unit course.

```bash
npm install
npm run dev
```

The manuscript scan, with the torn ink in the hub repaired, is at `compass-restored.png`.


===== FILE: public/hawaiian-compass/bearing.js =====
/** Hawaiian 32-point card. Four roots, joined by ma ("toward") and me ("with"). */

export const CARDS = [
  { short: "A", name: "Ākau", english: "north" },
  { short: "Hi", name: "Hikina", english: "east" },
  { short: "He", name: "Hema", english: "south" },
  { short: "K", name: "Komohana", english: "west" },
];

const STEP = 360 / 32;

function wrap(deg) {
  const x = ((deg % 360) + 360) % 360;
  return x === 0 ? 0 : x;
}

function round(n, places) {
  const p = 10 ** places;
  return Math.round((n + Number.EPSILON) * p) / p;
}

/** One of the 32 points. Index 0 is north. */
export function pointAt(index) {
  const i = ((index % 32) + 32) % 32;
  const q = Math.floor(i / 8);
  const p = i % 8;
  const a = CARDS[q];
  const b = CARDS[(q + 1) % 4];
  const forms = [
    [a.short, a.name],
    [`${a.short} ma ${b.short}`, `${a.name} ma ${b.name}`],
    [`${a.short} ${a.short} ${b.short}`, `${a.name} ${a.name} ${b.name}`],
    [`${a.short}${b.short} ma ${a.short}`, `${a.name}-${b.name} ma ${a.name}`],
    [`${a.short}${b.short}`, `${a.name}-${b.name}`],
    [`${a.short}${b.short} ma ${b.short}`, `${a.name}-${b.name} ma ${b.name}`],
    [`${b.short} me ${a.short}`, `${b.name} me ${a.name}`],
    [`${b.short} ma ${a.short}`, `${b.name} ma ${a.name}`],
  ];
  const [mark, spoken] = forms[p];
  return {
    index: i,
    bearing: round(i * STEP, 4),
    mark,
    spoken,
    rank: p === 0 ? "cardinal" : p === 4 ? "intercardinal" : p % 2 === 0 ? "half" : "by",
  };
}

export const POINTS = Array.from({ length: 32 }, (_, i) => pointAt(i));

export function normalize(deg) {
  return round(wrap(deg), 4);
}

/** Parameters of a course, measured clockwise from north. */
export function describe(deg) {
  const bearing = normalize(deg);
  const rad = (bearing * Math.PI) / 180;
  const nearestIndex = Math.round(bearing / STEP) % 32;
  const point = pointAt(nearestIndex);
  let offset = bearing - point.bearing;
  if (offset > 180) offset -= 360;
  if (offset < -180) offset += 360;
  const reciprocal = normalize(bearing + 180);
  const reciprocalPoint = pointAt(Math.round(reciprocal / STEP) % 32);

  let quadrant;
  if (bearing === 0) quadrant = "north";
  else if (bearing === 90) quadrant = "east";
  else if (bearing === 180) quadrant = "south";
  else if (bearing === 270) quadrant = "west";
  else if (bearing < 90) quadrant = `north ${formatDeg(bearing)} east`;
  else if (bearing < 180) quadrant = `south ${formatDeg(180 - bearing)} east`;
  else if (bearing < 270) quadrant = `south ${formatDeg(bearing - 180)} west`;
  else quadrant = `north ${formatDeg(360 - bearing)} west`;

  return {
    bearing,
    radians: round(rad, 6),
    northing: round(Math.cos(rad), 6),
    easting: round(Math.sin(rad), 6),
    point,
    offset: round(offset, 4),
    reciprocal,
    reciprocalPoint,
    quadrant,
    next: pointAt((nearestIndex + 1) % 32),
    previous: pointAt((nearestIndex + 31) % 32),
  };
}

export function formatDeg(deg, places = 1) {
  const n = round(wrap(deg), places);
  const shown = n === 360 ? 0 : n;
  return shown.toFixed(places);
}

export function formatSigned(deg, places = 1) {
  const n = round(deg, places);
  const body = Math.abs(n).toFixed(places);
  if (n > 0) return `+${body}°`;
  if (n < 0) return `−${body}°`;
  return `${body}°`;
}

//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJlYXJpbmcuanMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqIEhhd2FpaWFuIDMyLXBvaW50IGNhcmQuIEZvdXIgcm9vdHMsIGpvaW5lZCBieSBtYSAoXCJ0b3dhcmRcIikgYW5kIG1lIChcIndpdGhcIikuICovXG5cbmV4cG9ydCBjb25zdCBDQVJEUyA9IFtcbiAgeyBzaG9ydDogXCJBXCIsIG5hbWU6IFwixIBrYXVcIiwgZW5nbGlzaDogXCJub3J0aFwiIH0sXG4gIHsgc2hvcnQ6IFwiSGlcIiwgbmFtZTogXCJIaWtpbmFcIiwgZW5nbGlzaDogXCJlYXN0XCIgfSxcbiAgeyBzaG9ydDogXCJIZVwiLCBuYW1lOiBcIkhlbWFcIiwgZW5nbGlzaDogXCJzb3V0aFwiIH0sXG4gIHsgc2hvcnQ6IFwiS1wiLCBuYW1lOiBcIktvbW9oYW5hXCIsIGVuZ2xpc2g6IFwid2VzdFwiIH0sXG5dO1xuXG5jb25zdCBTVEVQID0gMzYwIC8gMzI7XG5cbmZ1bmN0aW9uIHdyYXAoZGVnKSB7XG4gIGNvbnN0IHggPSAoKGRlZyAlIDM2MCkgKyAzNjApICUgMzYwO1xuICByZXR1cm4geCA9PT0gMCA/IDAgOiB4O1xufVxuXG5mdW5jdGlvbiByb3VuZChuLCBwbGFjZXMpIHtcbiAgY29uc3QgcCA9IDEwICoqIHBsYWNlcztcbiAgcmV0dXJuIE1hdGgucm91bmQoKG4gKyBOdW1iZXIuRVBTSUxPTikgKiBwKSAvIHA7XG59XG5cbi8qKiBPbmUgb2YgdG