373 lines
7.5 KiB
JavaScript
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;
|
|
}
|