silphius/app/util/math.js
2024-07-27 09:54:03 -05:00

373 lines
7.5 KiB
JavaScript

import alea from 'alea';
import {createNoise2D as createSimplexNoise2D} from 'simplex-noise';
export const {
abs,
acos,
acosh,
asin,
asinh,
atan,
atanh,
atan2,
ceil,
cbrt,
expm1,
clz32,
cos,
cosh,
exp,
floor,
fround,
hypot,
imul,
log,
log1p,
log2,
log10,
max,
min,
pow,
round,
sign,
sin,
sinh,
sqrt,
tan,
tanh,
trunc,
E,
LN10,
LN2,
LOG10E,
LOG2E,
PI,
SQRT1_2,
SQRT2,
} = Math;
export const SQRT_2_2 = Math.sqrt(2) / 2;
export const EIGHTH_PI = Math.PI / 8;
export const QUARTER_PI = Math.PI / 4;
export const HALF_PI = Math.PI / 2;
export const TAU = Math.PI * 2;
export const PI_180 = Math.PI / 180;
export function bresenham({x: x1, y: y1}, {x: x2, y: y2}) {
const points = [];
let x; let y; let px; let py; let xe; let ye;
const dx = x2 - x1;
const dy = y2 - y1;
const dx1 = Math.abs(dx);
const dy1 = Math.abs(dy);
px = 2 * dy1 - dx1;
py = 2 * dx1 - dy1;
if (dy1 <= dx1) {
if (dx >= 0) {
x = x1; y = y1; xe = x2;
}
else {
x = x2; y = y2; xe = x1;
}
points.push({x, y});
for (let i = 0; x < xe; i++) {
x += 1;
if (px < 0) {
px += 2 * dy1;
}
else {
if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {
y += 1;
}
else {
y -= 1;
}
px += 2 * (dy1 - dx1);
}
points.push({x, y});
}
}
else {
if (dy >= 0) {
x = x1; y = y1; ye = y2;
}
else {
x = x2; y = y2; ye = y1;
}
points.push({x, y});
for (let i = 0; y < ye; i++) {
y += 1;
if (py <= 0) {
py += 2 * dx1;
}
else {
if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {
x += 1;
}
else {
x -= 1;
}
py += 2 * (dx1 - dy1);
}
points.push({x, y});
}
}
return points;
}
export function clamp(n, min, max) {
return Math.max(min, Math.min(max, n));
}
export function createNoise2D(seed = 0) {
return createSimplexNoise2D(createRandom(seed));
}
export function createRandom(seed = 0) {
return alea(seed);
}
export const directionToVector = [
{x: 0, y: -1},
{x: 1, y: 0},
{x: 0, y: 1},
{x: -1, y: 0},
];
export function distance({x: lx, y: ly}, {x: rx, y: ry}) {
const xd = lx - rx;
const yd = ly - ry;
return Math.sqrt(xd * xd + yd * yd);
}
export function floodwalk2D(eligible, data, {x, y, w, h}, {diagonal = false} = {}) {
const n = data.length;
let i = x + w * y;
const points = new Set();
if (i < 0 || i >= n) {
return points;
}
const seen = [];
seen[-1] = true;
seen[i] = true;
const todo = [i];
while (todo.length > 0) {
i = todo.pop();
const v = data[i];
if (!eligible.has(v)) {
continue;
}
points.add(i);
const xx = i % w;
const yy = Math.floor(i / w);
const us = yy >= 1;
const rs = xx + 1 < w;
const ds = yy + 1 < h;
const ls = xx >= 1;
const sel = [
us ? i - w : -1,
rs ? i + 1 : -1,
ds ? i + w : -1,
ls ? i - 1 : -1,
...(
diagonal
? [
us && rs ? i - w + 1 : -1,
rs && ds ? i + w + 1 : -1,
ds && ls ? i + w - 1 : -1,
ls && us ? i - w - 1 : -1,
]
: []
)
];
for (let i = 0; i < sel.length; ++i) {
const j = sel[i];
if (!seen[j]) {
todo.push(j);
seen[j] = true;
}
}
}
return points;
}
export class Generator {
constructor({
calculate,
children = [],
covers,
size: {w, h},
}) {
this.calculate = calculate;
this.children = children;
this.covers = covers;
this.size = {w, h};
this.matrix = new Array(w * h).fill(0);
}
compute(i, position) {
if (!this.covers(position)) {
return;
}
this.matrix[i] = this.calculate(position);
if (!this.children) {
return;
}
for (let j = 0; j < this.children.length; j++) {
this.children[j].compute(i, position);
}
}
generate() {
const {w, h} = this.size;
let i = 0;
const position = {x: 0, y: 0};
for (let y = 0; y < h; ++y) {
for (let x = 0; x < w; ++x) {
this.compute(i++, position);
position.x += 1;
}
position.x -= w;
position.y += 1;
}
}
}
export function intersects(l, r) {
if (l.x0 > r.x1) return false;
if (l.y0 > r.y1) return false;
if (l.x1 < r.x0) return false;
if (l.y1 < r.y0) return false;
return true;
}
export function normalizeVector({x, y}) {
if (0 === y && 0 === x) {
return {x: 0, y: 0};
}
const k = 1 / Math.sqrt(x * x + y * y);
return {x: x * k, y: y * k};
}
export function random() {
return Math.random();
}
export function isCollinear({x: ax, y: ay}, {x: bx, y: by}, {x: cx, y: cy}) {
return (ay - by) * (ax - cx) === (ay - cy) * (ax - bx);
}
export function ortho(points, k = {x: 1, y: 1}) {
if (points.length < 4) {
throw new TypeError('Math.ortho(): points.length < 4');
}
const index = Object.create(null);
for (const i in points) {
const point = points[i];
if (!index[point.y]) {
index[point.y] = Object.create(null);
}
index[point.y][point.x] = i;
}
const sorted = [points[0]];
let direction = 0;
let walk = 0;
navigate:
while (true) {
for (let i = 0; i < 4; i++) {
// ccw rotation
const nextDirection = (i + direction + 3) & 3;
const {x: px, y: py} = points[walk];
const {x: dx, y: dy} = directionToVector[nextDirection];
const nextIndex = index[py + k.y * dy]?.[px + k.x * dx];
const nextPoint = points[nextIndex];
// loop = done
if (points[0] === nextPoint) {
return sorted;
}
if (nextPoint) {
direction = nextDirection;
sorted.push(nextPoint);
walk = nextIndex;
continue navigate;
}
}
return [];
}
}
export function removeCollinear([...vertices]) {
if (vertices.length < 3) {
return vertices;
}
vertices.push(vertices[0]);
vertices.push(vertices[1]);
const trimmed = [];
let i = 0;
while (i < vertices.length - 2) {
if (isCollinear(vertices[i], vertices[i + 1], vertices[i + 2])) {
vertices.splice(i + 1, 1);
}
else {
trimmed.push(vertices[i]);
i += 1;
}
}
return trimmed;
}
export const smoothstep = (x) => x * x * (3 - 2 * x);
export const smootherstep = (x) => x * x * x * (x * (x * 6 - 15) + 10);
export function transform(
vertices,
{
rotation = 0,
scale = 1,
translation = {x: 0, y: 0},
origin = {x: 0, y: 0},
},
) {
// nop
if (0 === rotation && 1 === scale && 0 === translation.x && 0 === translation.y) {
return vertices;
}
const transformed = [];
// scale
for (const vertice of vertices) {
if (1 === scale) {
transformed.push({x: vertice.x, y: vertice.y});
continue;
}
transformed.push({
x: origin.x + (vertice.x - origin.x) * scale,
y: origin.y + (vertice.y - origin.y) * scale,
});
}
// rotation
rotation = rotation % TAU;
if (0 !== rotation) {
for (const vertice of transformed) {
let a = rotation + Math.atan2(
vertice.y - origin.y,
vertice.x - origin.x,
);
a = (a >= 0 || a < TAU) ? a : (a % TAU + TAU) % TAU;
const d = distance(vertice, origin);
vertice.x = origin.x + d * Math.cos(a);
vertice.y = origin.y + d * Math.sin(a);
}
}
// translation
if (0 !== translation.x || 0 !== translation.y) {
for (const vertice of transformed) {
vertice.x += translation.x;
vertice.y += translation.y;
}
}
return transformed;
}