adding initial world map frontend
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// ponytail: pure layout math for the WorldMap, extracted so it's testable
|
||||
// without konva. Positions are normalized 0..1 so the map survives a resize.
|
||||
|
||||
export type PinKind = "region" | "landmark";
|
||||
|
||||
export interface PinPos {
|
||||
name: string;
|
||||
x: number; // 0..1
|
||||
y: number; // 0..1
|
||||
kind: PinKind;
|
||||
}
|
||||
|
||||
// Clamp a normalized coordinate into the canvas with a small margin so a pin
|
||||
// can't be dragged half-off the edge.
|
||||
export function clamp(v: number): number {
|
||||
return Math.max(0.04, Math.min(0.96, v));
|
||||
}
|
||||
|
||||
// Distribute N items on a centered ellipse so an unedited world reads as a
|
||||
// map rather than a stacked list. Deterministic by index.
|
||||
export function autoLayout(index: number, total: number, kind: PinKind): { x: number; y: number } {
|
||||
if (total <= 0) return { x: 0.5, y: 0.5 };
|
||||
if (total === 1) return { x: 0.5, y: 0.5 };
|
||||
if (kind === "region") {
|
||||
const angle = (index / total) * Math.PI * 2 - Math.PI / 2;
|
||||
return { x: 0.5 + Math.cos(angle) * 0.32, y: 0.5 + Math.sin(angle) * 0.34 };
|
||||
}
|
||||
// landmarks: inner ring, offset by half a step so they don't overlap regions
|
||||
const angle = (index / total) * Math.PI * 2;
|
||||
return { x: 0.5 + Math.cos(angle) * 0.18, y: 0.5 + Math.sin(angle) * 0.2 };
|
||||
}
|
||||
|
||||
// Build the pin list: saved positions win, else auto-layout. Returns one pin
|
||||
// per name; duplicates keep distinct pins by kind+index but share a position
|
||||
// key, so the last duplicate's saved position wins for that name.
|
||||
export function buildPins(
|
||||
regions: string[],
|
||||
landmarks: string[],
|
||||
pins: Record<string, { x: number; y: number }>,
|
||||
): PinPos[] {
|
||||
const out: PinPos[] = [];
|
||||
regions.forEach((name, i) => {
|
||||
const pos = pins[name] ?? autoLayout(i, regions.length, "region");
|
||||
out.push({ name, x: clamp(pos.x), y: clamp(pos.y), kind: "region" });
|
||||
});
|
||||
landmarks.forEach((name, i) => {
|
||||
const pos = pins[name] ?? autoLayout(i, landmarks.length, "landmark");
|
||||
out.push({ name, x: clamp(pos.x), y: clamp(pos.y), kind: "landmark" });
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── Self-check (run: node src/lib/worldMap.ts) ──────────────
|
||||
// ponytail: the smallest thing that fails if the layout math breaks. No
|
||||
// framework — plain asserts. Verifies clamp bounds, auto-layout spread, and
|
||||
// that saved positions survive a buildPins round-trip.
|
||||
function assert(cond: boolean, msg: string): void {
|
||||
if (!cond) throw new Error(`worldMap self-check failed: ${msg}`);
|
||||
}
|
||||
|
||||
export function demo(): void {
|
||||
assert(clamp(-1) === 0.04, "clamp floors at margin");
|
||||
assert(clamp(2) === 0.96, "clamp ceilings at margin");
|
||||
assert(clamp(0.5) === 0.5, "clamp passes through mid");
|
||||
|
||||
// Single item centers; many items spread (no two identical on a ring).
|
||||
const single = autoLayout(0, 1, "region");
|
||||
assert(single.x === 0.5 && single.y === 0.5, "single item centers");
|
||||
|
||||
const many = Array.from({ length: 6 }, (_, i) => autoLayout(i, 6, "region"));
|
||||
const uniq = new Set(many.map((p) => `${p.x.toFixed(3)},${p.y.toFixed(3)}`));
|
||||
assert(uniq.size === 6, "6 regions spread to 6 distinct spots");
|
||||
|
||||
// Saved positions survive buildPins; unsaved ones get auto-laid-out + clamped.
|
||||
const built = buildPins(["A", "B"], ["X"], { A: { x: 0.2, y: 0.3 } });
|
||||
assert(built.length === 3, "buildPins returns regions + landmarks");
|
||||
const a = built.find((p) => p.name === "A")!;
|
||||
assert(a.x === 0.2 && a.y === 0.3, "saved position preserved");
|
||||
const b = built.find((p) => p.name === "B")!;
|
||||
assert(b.x >= 0.04 && b.x <= 0.96, "auto-laid-out pin is clamped");
|
||||
|
||||
console.log("worldMap self-check passed ✓");
|
||||
}
|
||||
Reference in New Issue
Block a user