Files
Guncircle.io/packages/shared/src/math.ts
T

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();
}
}