93 lines
3.7 KiB
JavaScript
93 lines
3.7 KiB
JavaScript
// Layout math for campaign map node positions. All coordinates are in 960x540
|
|
// design units (the scene multiplies by WORLD_SCALE when drawing).
|
|
//
|
|
// Node badges render at ~58-71 design units in diameter (see nodeDisplaySize
|
|
// in util/mapArt.js). The free band of the map (below the title plate, whose
|
|
// ink ends at y≈158, and above the bottom chrome at y≈490) is ~330 units
|
|
// tall, which leaves room for two lanes of nodes with the ribbon's loop
|
|
// between them:
|
|
//
|
|
// * lane 1 (top, y=268): runs RIGHT -> LEFT, ending short of the left edge
|
|
// * lane 2 (bottom, y=412): runs LEFT -> RIGHT, ending short of the right
|
|
// edge so the finish flag fits in the bottom-right corner past it
|
|
//
|
|
// ...which reads as one continuous road: start top-right, across the top,
|
|
// down the left side, across the bottom, flag at the bottom-right. Both
|
|
// lanes use ~660 of the 780 usable width; the 120-unit ends are what make
|
|
// the left connector and the right flag fit without touching a node.
|
|
// Comfortable for up to 12 levels (13 starts getting tight - give explicit
|
|
// `positions` for exotic counts).
|
|
|
|
const MAP_WIDTH = 960;
|
|
const LANE_TOP = 268;
|
|
const LANE_BOTTOM = 412;
|
|
// Usable span of each lane (keeps a 120-unit end zone free, see above).
|
|
const LANE_SPAN = 660;
|
|
// Where each lane starts. Lane 1 starts at the top-right, lane 2 at the
|
|
// bottom-left.
|
|
const LANE1_X0 = 870; // lane 1 runs from here LEFTWARDS
|
|
const LANE2_X0 = 90; // lane 2 runs from here RIGHTWARDS
|
|
|
|
// Auto-places a campaign's levels along the 2-lane circuit. Handles any
|
|
// count >= 1. For a bespoke look, give explicit `positions` in index.js -
|
|
// those are used verbatim.
|
|
export function autoLayoutPoints(count) {
|
|
if (count <= 0) return [];
|
|
if (count === 1) return [{ x: 480, y: Math.round((LANE_TOP + LANE_BOTTOM) / 2) }];
|
|
// Two levels: one per lane, offset toward each other so the connector is
|
|
// a diagonal (not a vertical pair, which would be closer than the badges
|
|
// are wide).
|
|
if (count === 2) {
|
|
return [
|
|
{ x: (LANE1_X0 + LANE2_X0) / 2 - 60, y: LANE_TOP },
|
|
{ x: (LANE1_X0 + LANE2_X0) / 2 + 60, y: LANE_BOTTOM },
|
|
];
|
|
}
|
|
const firstRow = Math.ceil(count / 2);
|
|
const secondRow = count - firstRow;
|
|
const points = [];
|
|
// Lane 1: right -> left.
|
|
for (let k = 0; k < firstRow; k++) {
|
|
const t = firstRow === 1 ? 0 : k / (firstRow - 1);
|
|
points.push({ x: Math.round(LANE1_X0 - LANE_SPAN * t), y: LANE_TOP });
|
|
}
|
|
// Lane 2: left -> right.
|
|
for (let k = 0; k < secondRow; k++) {
|
|
const t = secondRow === 1 ? 0 : k / (secondRow - 1);
|
|
points.push({ x: Math.round(LANE2_X0 + LANE_SPAN * t), y: LANE_BOTTOM });
|
|
}
|
|
return points;
|
|
}
|
|
|
|
// Catmull-Rom spline sampled into a polyline. `points` must have >= 2 entries;
|
|
// the first/last are duplicated so the curve passes exactly through every
|
|
// node (standard end-anchor trick).
|
|
export function sampleSpline(points, samplesPerSegment = 14) {
|
|
if (points.length < 2) return [];
|
|
const pts = [points[0], ...points, points[points.length - 1]];
|
|
const out = [];
|
|
for (let i = 0; i < pts.length - 3; i++) {
|
|
const [p0, p1, p2, p3] = [pts[i], pts[i + 1], pts[i + 2], pts[i + 3]];
|
|
for (let s = 0; s < samplesPerSegment; s++) {
|
|
const t = s / samplesPerSegment;
|
|
const t2 = t * t;
|
|
const t3 = t2 * t;
|
|
const x =
|
|
0.5 *
|
|
(2 * p1.x +
|
|
(-p0.x + p2.x) * t +
|
|
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
|
|
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
|
|
const y =
|
|
0.5 *
|
|
(2 * p1.y +
|
|
(-p0.y + p2.y) * t +
|
|
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
|
|
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
|
|
out.push({ x, y });
|
|
}
|
|
}
|
|
out.push(points[points.length - 1]);
|
|
return out;
|
|
}
|