Files
Battle-for-Tismo/client/js/map_view/utils.js
T

152 lines
4.8 KiB
JavaScript

// Pure helpers for the map view: geometry, path timing and colour blending.
// Nothing here touches the DOM or the MapView instance, so it is all trivially
// testable.
import { HEX_W, HEX_H } from "../../../shared/hex.js";
import { OVERVIEW_COLOURS, OVERVIEW_SEA } from "./constants.js";
export function parity(x) {
return x & 1;
}
// Order-independent FNV-style hash of an [x, y, ...] entry array, used to skip
// rebuilding a layer when the snapshot did not actually change it.
export function hashEntries(entries) {
let hash = 2166136261;
for (const entry of entries) {
hash = Math.imul(hash ^ (entry[0] + 4096), 16777619);
hash = Math.imul(hash ^ (entry[1] + 4096), 16777619);
hash = Math.imul(hash ^ (entry.length > 2 ? entry[2] + 8 : 0), 16777619);
}
return `${entries.length}:${(hash >>> 0).toString(36)}`;
}
// How far along its path a unit is, in segment-hours, from a snapshot.
export function hoursAlong(data, segmentHours) {
const index = Math.min(data.pathIndex || 0, segmentHours.length);
let total = 0;
for (let i = 0; i < index; i++) total += segmentHours[i];
return total + (data.progressHours || 0);
}
export function samePath(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].x !== b[i].x || a[i].y !== b[i].y) return false;
}
return true;
}
// The segment index a motion is currently on and how far into it it is.
export function pathProgress(motion) {
let remaining = motion.pathHours;
let index = 0;
while (index < motion.segmentHours.length) {
const step = motion.segmentHours[index];
if (step <= 0) {
index += 1;
continue;
}
if (remaining < step) break;
remaining -= step;
index += 1;
}
return { index, remaining };
}
export function nearestCorners(center, target) {
const half = { x: HEX_W / 2, y: HEX_H / 2 };
const corners = [
{ x: half.x, y: 0 },
{ x: half.x * 0.5, y: -half.y },
{ x: -half.x * 0.5, y: -half.y },
{ x: -half.x, y: 0 },
{ x: -half.x * 0.5, y: half.y },
{ x: half.x * 0.5, y: half.y },
];
let first = 0;
let second = 1;
let firstDistance = Infinity;
let secondDistance = Infinity;
for (let i = 0; i < corners.length; i++) {
const point = { x: center.x + corners[i].x, y: center.y + corners[i].y };
const distance = (point.x - target.x) ** 2 + (point.y - target.y) ** 2;
if (distance < firstDistance) {
secondDistance = firstDistance;
second = first;
firstDistance = distance;
first = i;
} else if (distance < secondDistance) {
secondDistance = distance;
second = i;
}
}
return [
{ x: center.x + corners[first].x, y: center.y + corners[first].y },
{ x: center.x + corners[second].x, y: center.y + corners[second].y },
];
}
// Signed distance to nudge an edge segment along its normal so it moves towards
// `center` (the owning tile). Used to keep neighbours' borders side by side
// instead of overlapping on the shared edge.
export function inwardOffset(a, b, center, distance) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const length = Math.hypot(dx, dy) || 1;
const nx = -dy / length;
const ny = dx / length;
const midX = (a.x + b.x) / 2;
const midY = (a.y + b.y) / 2;
const dot = (center.x - midX) * nx + (center.y - midY) * ny;
return dot >= 0 ? distance : -distance;
}
export function makeSegment(a, b, colour, width, offset = 0) {
const div = document.createElement("div");
div.className = "border-seg";
const dx = b.x - a.x;
const dy = b.y - a.y;
const length = Math.hypot(dx, dy);
const angle = Math.atan2(dy, dx);
div.style.left = `${a.x}px`;
div.style.top = `${a.y - width / 2}px`;
div.style.width = `${length}px`;
div.style.height = `${width}px`;
div.style.background = colour;
div.style.transform = offset === 0
? `rotate(${angle}rad)`
: `rotate(${angle}rad) translateY(${offset}px)`;
div.style.borderRadius = `${width / 2}px`;
return div;
}
export function overviewColour(tile) {
if (tile.terrainType && OVERVIEW_COLOURS[tile.terrainType]) {
return OVERVIEW_COLOURS[tile.terrainType];
}
return tile.terrainClass === "Sea" ? OVERVIEW_SEA : OVERVIEW_COLOURS.Land;
}
// Mixes two #rrggbb colours, `t` being how much of `b` to use.
export function blendColours(a, b, t) {
const ca = parseColour(a);
const cb = parseColour(b);
const mix = (i) => Math.round(ca[i] + (cb[i] - ca[i]) * t);
return `rgb(${mix(0)}, ${mix(1)}, ${mix(2)})`;
}
function parseColour(value) {
const hex = value.charAt(0) === "#" ? value.slice(1) : value;
return [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
];
}
export function clamp(value, min, max) {
if (min > max) return (min + max) / 2;
return Math.max(min, Math.min(max, value));
}