223 lines
7.1 KiB
TypeScript
223 lines
7.1 KiB
TypeScript
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// GunCircle.io — Math Utilities
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
import type { Vec2 } from './types.js';
|
|
|
|
// ─── Vector Operations ───────────────────────────────────────────────────────
|
|
|
|
export const vec2 = {
|
|
zero: (): Vec2 => ({ x: 0, y: 0 }),
|
|
|
|
fromAngle: (angle: number, magnitude: number = 1): Vec2 => ({
|
|
x: Math.cos(angle) * magnitude,
|
|
y: Math.sin(angle) * magnitude,
|
|
}),
|
|
|
|
add: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y }),
|
|
sub: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y }),
|
|
mul: (v: Vec2, s: number): Vec2 => ({ x: v.x * s, y: v.y * s }),
|
|
div: (v: Vec2, s: number): Vec2 => ({ x: v.x / s, y: v.y / s }),
|
|
|
|
len: (v: Vec2): number => Math.sqrt(v.x * v.x + v.y * v.y),
|
|
lenSq: (v: Vec2): number => v.x * v.x + v.y * v.y,
|
|
|
|
normalize: (v: Vec2): Vec2 => {
|
|
const l = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l };
|
|
},
|
|
|
|
dot: (a: Vec2, b: Vec2): number => a.x * b.x + a.y * b.y,
|
|
|
|
dist: (a: Vec2, b: Vec2): number => {
|
|
const dx = a.x - b.x;
|
|
const dy = a.y - b.y;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
},
|
|
|
|
distSq: (a: Vec2, b: Vec2): number => {
|
|
const dx = a.x - b.x;
|
|
const dy = a.y - b.y;
|
|
return dx * dx + dy * dy;
|
|
},
|
|
|
|
clamp: (v: Vec2, maxLen: number): Vec2 => {
|
|
const l = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
if (l <= maxLen) return v;
|
|
const scale = maxLen / l;
|
|
return { x: v.x * scale, y: v.y * scale };
|
|
},
|
|
|
|
angle: (v: Vec2): number => Math.atan2(v.y, v.x),
|
|
angleBetween: (a: Vec2, b: Vec2): number => Math.atan2(b.y - a.y, b.x - a.x),
|
|
|
|
lerp: (a: Vec2, b: Vec2, t: number): Vec2 => ({
|
|
x: a.x + (b.x - a.x) * t,
|
|
y: a.y + (b.y - a.y) * t,
|
|
}),
|
|
|
|
copy: (v: Vec2): Vec2 => ({ x: v.x, y: v.y }),
|
|
|
|
equals: (a: Vec2, b: Vec2, eps: number = 0.001): boolean =>
|
|
Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps,
|
|
};
|
|
|
|
// ─── Scalar Utilities ────────────────────────────────────────────────────────
|
|
|
|
export const lerp = (a: number, b: number, t: number): number =>
|
|
a + (b - a) * t;
|
|
|
|
export const lerpAngle = (a: number, b: number, t: number): number => {
|
|
let diff = b - a;
|
|
while (diff > Math.PI) diff -= Math.PI * 2;
|
|
while (diff < -Math.PI) diff += Math.PI * 2;
|
|
return a + diff * t;
|
|
};
|
|
|
|
export const clamp = (v: number, min: number, max: number): number =>
|
|
Math.max(min, Math.min(max, v));
|
|
|
|
export const clamp01 = (v: number): number => clamp(v, 0, 1);
|
|
|
|
export const remap = (
|
|
v: number,
|
|
inMin: number,
|
|
inMax: number,
|
|
outMin: number,
|
|
outMax: number
|
|
): number => outMin + ((v - inMin) / (inMax - inMin)) * (outMax - outMin);
|
|
|
|
/** Return random float in [min, max) */
|
|
export const randFloat = (min: number, max: number): number =>
|
|
min + Math.random() * (max - min);
|
|
|
|
/** Return random int in [min, max) */
|
|
export const randInt = (min: number, max: number): number =>
|
|
Math.floor(randFloat(min, max));
|
|
|
|
/** Return 1 or -1 randomly */
|
|
export const randSign = (): number => (Math.random() < 0.5 ? 1 : -1);
|
|
|
|
/** Check if circle a overlaps circle b */
|
|
export const circleOverlap = (
|
|
ax: number, ay: number, ar: number,
|
|
bx: number, by: number, br: number
|
|
): boolean => {
|
|
const dx = ax - bx;
|
|
const dy = ay - by;
|
|
const r = ar + br;
|
|
return dx * dx + dy * dy < r * r;
|
|
};
|
|
|
|
/** Solve circle-circle collision: push a out of b. Returns correction vector for a. */
|
|
export const circleCollisionResolve = (
|
|
ax: number, ay: number, ar: number,
|
|
bx: number, by: number, br: number
|
|
): Vec2 | null => {
|
|
const dx = ax - bx;
|
|
const dy = ay - by;
|
|
const distSq = dx * dx + dy * dy;
|
|
const r = ar + br;
|
|
if (distSq >= r * r || distSq === 0) return null;
|
|
const dist = Math.sqrt(distSq);
|
|
const overlap = r - dist;
|
|
return {
|
|
x: (dx / dist) * overlap,
|
|
y: (dy / dist) * overlap,
|
|
};
|
|
};
|
|
|
|
// ─── Spatial Hash Grid ───────────────────────────────────────────────────────
|
|
|
|
export class SpatialHashGrid<T extends { x: number; y: number; id: number }> {
|
|
private cells = new Map<string, T[]>();
|
|
private itemToCell = new Map<number, string>();
|
|
|
|
constructor(private cellSize: number) {}
|
|
|
|
private hash(cx: number, cy: number): string {
|
|
return `${cx},${cy}`;
|
|
}
|
|
|
|
insert(item: T): void {
|
|
const cx = Math.floor(item.x / this.cellSize);
|
|
const cy = Math.floor(item.y / this.cellSize);
|
|
const key = this.hash(cx, cy);
|
|
|
|
let cell = this.cells.get(key);
|
|
if (!cell) {
|
|
cell = [];
|
|
this.cells.set(key, cell);
|
|
}
|
|
cell.push(item);
|
|
this.itemToCell.set(item.id, key);
|
|
}
|
|
|
|
remove(item: T): void {
|
|
const key = this.itemToCell.get(item.id);
|
|
if (!key) return;
|
|
const cell = this.cells.get(key);
|
|
if (!cell) return;
|
|
const idx = cell.findIndex((i) => i.id === item.id);
|
|
if (idx >= 0) cell.splice(idx, 1);
|
|
this.itemToCell.delete(item.id);
|
|
}
|
|
|
|
update(item: T): void {
|
|
this.remove(item);
|
|
this.insert(item);
|
|
}
|
|
|
|
query(x: number, y: number, radius: number): T[] {
|
|
const results: T[] = [];
|
|
const minCx = Math.floor((x - radius) / this.cellSize);
|
|
const maxCx = Math.floor((x + radius) / this.cellSize);
|
|
const minCy = Math.floor((y - radius) / this.cellSize);
|
|
const maxCy = Math.floor((y + radius) / this.cellSize);
|
|
const rSq = radius * radius;
|
|
|
|
for (let cx = minCx; cx <= maxCx; cx++) {
|
|
for (let cy = minCy; cy <= maxCy; cy++) {
|
|
const key = this.hash(cx, cy);
|
|
const cell = this.cells.get(key);
|
|
if (!cell) continue;
|
|
for (const item of cell) {
|
|
const dx = item.x - x;
|
|
const dy = item.y - y;
|
|
if (dx * dx + dy * dy <= rSq) {
|
|
results.push(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
queryRect(x: number, y: number, w: number, h: number): T[] {
|
|
const results: T[] = [];
|
|
const minCx = Math.floor(x / this.cellSize);
|
|
const maxCx = Math.floor((x + w) / this.cellSize);
|
|
const minCy = Math.floor(y / this.cellSize);
|
|
const maxCy = Math.floor((y + h) / this.cellSize);
|
|
|
|
for (let cx = minCx; cx <= maxCx; cx++) {
|
|
for (let cy = minCy; cy <= maxCy; cy++) {
|
|
const key = this.hash(cx, cy);
|
|
const cell = this.cells.get(key);
|
|
if (!cell) continue;
|
|
for (const item of cell) {
|
|
if (item.x >= x && item.x <= x + w && item.y >= y && item.y <= y + h) {
|
|
results.push(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
clear(): void {
|
|
this.cells.clear();
|
|
this.itemToCell.clear();
|
|
}
|
|
}
|