UNPKG

@mousepox/math

Version:

Math-related objects and utilities

221 lines (220 loc) 6.66 kB
import { DirectionMask } from "./core"; import { BitFlags } from "./BitFlags"; import { Vector2 } from "./Vector2"; const ScratchBitFlags = new BitFlags(); const ScratchVector2 = new Vector2(); const OrthogonalAdjacentOffsets = [ { x: 0, y: -1 }, { x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }, ]; const AdjacentOffsets = [ { x: -1, y: -1, mask: DirectionMask.NorthWest }, { x: 0, y: -1, mask: DirectionMask.North }, { x: 1, y: -1, mask: DirectionMask.NorthEast }, { x: -1, y: 0, mask: DirectionMask.West }, { x: 1, y: 0, mask: DirectionMask.East }, { x: -1, y: 1, mask: DirectionMask.SouthWest }, { x: 0, y: 1, mask: DirectionMask.South }, { x: 1, y: 1, mask: DirectionMask.SouthEast }, ]; export class Grid { static fromStrings(rows, map) { const grid = new Grid(rows[0].length, rows.length); grid.forEach((_, x, y) => { grid.set(x, y, map.indexOf(rows[y][x])); }); return grid; } width = 0; height = 0; cells = []; center = { x: 0, y: 0 }; constructor(width = 0, height = 0) { this.resize(width, height); } resize(width, height) { this.width = width; this.height = height; this.center.x = Math.floor(width / 2); this.center.y = Math.floor(height / 2); this.cells.length = width * height; this.fill(0); } fill(value) { this.cells.fill(value); } valid(x, y) { return x >= 0 && y >= 0 && x < this.width && y < this.height; } get(x, y) { if (this.valid(x, y)) { const index = this.xyToIndex(x, y); return this.cells[index]; } else { throw new Error(`Invalid cell coordinates: ${x}, ${y}`); } } getAdjacentFlags(x, y) { const v = this.get(x, y); ScratchBitFlags.clear(); for (const offset of AdjacentOffsets) { const ox = x + offset.x; const oy = y + offset.y; if (!this.valid(ox, oy) || this.get(ox, oy) === v) { ScratchBitFlags.set(offset.mask); } } return ScratchBitFlags.value; } set(x, y, value) { if (this.valid(x, y)) { const index = this.xyToIndex(x, y); this.setIndex(index, value); } else { throw new Error(`Invalid cell coordinates: ${x}, ${y}`); } } setIndex(index, value) { if (index >= 0 && index < this.cells.length) { this.cells[index] = value; } else { throw new Error(`Invalid cell index: ${index}`); } } copy(grid) { grid.forEach((value, x, y) => this.set(x, y, value)); } forEach(handler) { const len = this.cells.length; for (let index = 0; index < len; ++index) { const x = this.indexToX(index); const y = this.indexToY(index); handler(this.get(x, y), x, y); } } forEachInArea(x, y, width, height, handler) { for (let iy = y; iy < y + height; ++iy) { for (let ix = x; ix < x + width; ++ix) { if (!this.valid(ix, iy)) { continue; } handler(this.get(ix, iy), ix, iy); } } } forEachAdjacent(x, y, handler) { for (const offset of OrthogonalAdjacentOffsets) { const ax = x + offset.x; const ay = y + offset.y; if (!this.valid(ax, ay)) { continue; } handler(this.get(ax, ay), ax, ay); } } rotate(rotations = 1) { Scratch.resize(this.height, this.width); for (let i = 0; i < rotations; ++i) { for (let y = 0; y < this.height; ++y) { for (let x = 0; x < this.width; ++x) { Scratch.set(Scratch.width - y - 1, x, this.get(x, y)); } } this.copy(Scratch); } } rotatePoint(p, angle) { ScratchVector2.set(p.x, p.y); ScratchVector2.rotate(angle, this.center); p.x = Math.round(ScratchVector2.x); p.y = Math.round(ScratchVector2.y); } paste(grid, x, y) { grid.forEach((value, sx, sy) => { this.set(x + sx, y + sy, value); }); } raycast(origin, direction, result) { if (result === undefined) { result = { distance: 0, side: 0, value: undefined, }; } else { result.distance = 0; result.side = 0; result.value = undefined; } let x = Math.floor(origin.x); let y = Math.floor(origin.y); const deltaDistX = Math.sqrt(1 + (direction.y * direction.y) / (direction.x * direction.x)); const deltaDistY = Math.sqrt(1 + (direction.x * direction.x) / (direction.y * direction.y)); let sideDistX = 0; let sideDistY = 0; let stepX = 0; let stepY = 0; let hit = false; if (direction.x < 0) { stepX = -1; sideDistX = (origin.x - x) * deltaDistX; } else { stepX = 1; sideDistX = (x + 1.0 - origin.x) * deltaDistX; } if (direction.y < 0) { stepY = -1; sideDistY = (origin.y - y) * deltaDistY; } else { stepY = 1; sideDistY = (y + 1.0 - origin.y) * deltaDistY; } while (!hit) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; x += stepX; result.side = 0; } else { sideDistY += deltaDistY; y += stepY; result.side = 1; } if (this.valid(x, y)) { const value = this.get(x, y); if (value !== 0) { result.value = value; hit = true; } } else { break; } } if (result.side === 0) { result.distance = (x - origin.x + (1 - stepX) / 2) / direction.x; } else { result.distance = (y - origin.y + (1 - stepY) / 2) / direction.y; } return result; } xyToIndex(x, y) { return y * this.width + x; } indexToX(index) { return index % this.width; } indexToY(index) { return Math.floor(index / this.width); } } const Scratch = new Grid();