Add SPDX headers to the source, a License section to the README, the OFL notice for the bundled fonts, and ship LICENSE in the Docker image.
77 lines
4.2 KiB
JavaScript
77 lines
4.2 KiB
JavaScript
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 Andy Dixon
|
|
// Minimal column-major 4x4 matrix helpers and the cameras built on them. World axes: x right,
|
|
// y "down" the 2D map, z up. Every 3D view renders and projects through one view-projection
|
|
// matrix, so WebGL, the overlay and picking agree exactly.
|
|
|
|
// The world is left-handed (y runs down the map, z up), so x is negated to keep the map unmirrored.
|
|
export function perspective(fovY, aspect, near, far) {
|
|
const f = 1 / Math.tan(fovY / 2), nf = 1 / (near - far);
|
|
return new Float32Array([-f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0]);
|
|
}
|
|
|
|
export function lookAt([ex, ey, ez], [tx, ty, tz], [ux, uy, uz] = [0, 0, 1]) {
|
|
let zx = ex - tx, zy = ey - ty, zz = ez - tz;
|
|
let len = Math.hypot(zx, zy, zz) || 1; zx /= len; zy /= len; zz /= len;
|
|
let xx = uy * zz - uz * zy, xy = uz * zx - ux * zz, xz = ux * zy - uy * zx;
|
|
len = Math.hypot(xx, xy, xz);
|
|
if (len < 1e-6) { xx = 1; xy = 0; xz = 0; len = 1; } // looking straight along up
|
|
xx /= len; xy /= len; xz /= len;
|
|
const yx = zy * xz - zz * xy, yy = zz * xx - zx * xz, yz = zx * xy - zy * xx;
|
|
return new Float32Array([
|
|
xx, yx, zx, 0, xy, yy, zy, 0, xz, yz, zz, 0,
|
|
-(xx * ex + xy * ey + xz * ez), -(yx * ex + yy * ey + yz * ez), -(zx * ex + zy * ey + zz * ez), 1,
|
|
]);
|
|
}
|
|
|
|
export function multiply(a, b) {
|
|
const out = new Float32Array(16);
|
|
for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {
|
|
out[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Clip coordinates of a world point.
|
|
export function transform(m, x, y, z) {
|
|
return [
|
|
m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
m[2] * x + m[6] * y + m[10] * z + m[14],
|
|
m[3] * x + m[7] * y + m[11] * z + m[15],
|
|
];
|
|
}
|
|
|
|
// Unit view direction for a yaw (around z, 0 = looking toward -y) and pitch (up positive).
|
|
export function direction(yaw, pitch) {
|
|
return [-Math.sin(yaw) * Math.cos(pitch), -Math.cos(yaw) * Math.cos(pitch), Math.sin(pitch)];
|
|
}
|
|
|
|
// Orbit camera matching the original landscape projection: focal length 2.7, world scaled by
|
|
// 1/scale, with the view lifted by `lift` along the camera's up axis. `pitch` is the elevation.
|
|
export function orbitPose({ x, y, yaw, pitch, distance }, scale = 420, lift = .35) {
|
|
const cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch), d = distance * scale;
|
|
const up = [-sy * sp, -cy * sp, cp], shift = lift * scale;
|
|
const target = [x + up[0] * shift, y + up[1] * shift, up[2] * shift];
|
|
return { eye: [target[0] + sy * cp * d, target[1] + cy * cp * d, target[2] + sp * d], target, up };
|
|
}
|
|
|
|
export const ORBIT_FOV = 2 * Math.atan(1 / 2.7);
|
|
|
|
// General 4x4 inverse (null when singular), for turning screen points back into world rays.
|
|
export function invert(m) {
|
|
const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = m;
|
|
const b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11;
|
|
const b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30;
|
|
const b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32;
|
|
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
|
|
if (!det) return null;
|
|
det = 1 / det;
|
|
return new Float32Array([
|
|
(a11 * b11 - a12 * b10 + a13 * b09) * det, (a02 * b10 - a01 * b11 - a03 * b09) * det, (a31 * b05 - a32 * b04 + a33 * b03) * det, (a22 * b04 - a21 * b05 - a23 * b03) * det,
|
|
(a12 * b08 - a10 * b11 - a13 * b07) * det, (a00 * b11 - a02 * b08 + a03 * b07) * det, (a32 * b02 - a30 * b05 - a33 * b01) * det, (a20 * b05 - a22 * b02 + a23 * b01) * det,
|
|
(a10 * b10 - a11 * b08 + a13 * b06) * det, (a01 * b08 - a00 * b10 - a03 * b06) * det, (a30 * b04 - a31 * b02 + a33 * b00) * det, (a21 * b02 - a20 * b04 - a23 * b00) * det,
|
|
(a11 * b07 - a10 * b09 - a12 * b06) * det, (a00 * b09 - a01 * b07 + a02 * b06) * det, (a31 * b01 - a30 * b03 - a32 * b00) * det, (a20 * b03 - a21 * b01 + a22 * b00) * det,
|
|
]);
|
|
}
|