UNPKG

@thi.ng/boids

Version:

n-dimensional boids simulation with modular behavior system

125 lines (124 loc) 3.5 kB
import { identity } from "@thi.ng/api"; import { DIST_SQ2, DIST_SQ3 } from "@thi.ng/distance/squared"; import { VectorState, defVector } from "@thi.ng/timestep/state"; import { integrateAll, interpolateAll } from "@thi.ng/timestep/timestep"; import { VEC2 } from "@thi.ng/vectors/vec2-api"; import { VEC3 } from "@thi.ng/vectors/vec3-api"; import { blendedBehaviorUpdate } from "./behaviors/update.js"; import { Radial } from "./region.js"; class Boid { pos; vel; api; accel; behaviors; region; opts; /** * Pre-allocated vector for force accumulation, used by/for behavior * updates. */ force; constructor(opts, api, distance, pos, vel) { this.api = api; this.opts = { maxForce: 1, ...opts }; this.accel = this.opts.accel; this.behaviors = this.opts.behaviors; const update = this.opts.update || blendedBehaviorUpdate; const constrain = this.opts.constrain || identity; const { add, limit, maddN } = api; this.vel = defVector( api, vel, (vel2) => limit(vel2, add(vel2, vel2, update(this)), this.opts.maxSpeed) ); this.pos = defVector( api, pos, (pos2, dt) => constrain(maddN(pos2, this.vel.curr, dt, pos2), this) ); this.region = new Radial(distance, pos, 1); this.force = api.zeroes(); } /** * Integration step of the thi.ng/timestep update cycle. See * [`ITimeStep`](https://docs.thi.ng/umbrella/timestep/interfaces/ITimeStep.html) * * @param dt * @param ctx */ integrate(dt, ctx) { integrateAll(dt, ctx, this.vel, this.pos); } /** * Interplation step of the thi.ng/timestep update cycle. See * [`ITimeStep`](https://docs.thi.ng/umbrella/timestep/interfaces/ITimeStep.html) * * @param alpha * @param ctx */ interpolate(alpha, ctx) { interpolateAll(alpha, ctx, this.vel, this.pos); } /** * Queries the spatial index for other boids in the current region, or if * `pos` is given also moves the search region to new position before * querying. * * @remarks * IMPORTANT: The returned array will always contain the current boid itself * too. Filtering has been left out here for performance reasons and is left * to downstream code. * * @param r * @param pos */ neighbors(r, pos) { const region = this.region; if (pos) region.target = pos; region.setRadius(r); return this.accel.queryNeighborhood(region).deref(); } steerTowards(target, out = target) { return this.limitForce(this.api.sub(out, target, this.pos.curr)); } /** * Mutably divides given `force` by `num` (if > 0) and limits result via * {@link Boid.limitForce}. * * @param force * @param num */ averageForce(force, num) { return this.limitForce( num > 0 ? this.api.mulN(force, force, 1 / num) : force ); } /** * If force > 0, computes: `limit(normalize(force, maxSpeed) - vel, maxForce)`. * Otherwise, returns input as is. * * @param force */ limitForce(force) { const { limit, magSq, msubN } = this.api; const m = magSq(force); return m > 0 ? limit( force, msubN( force, force, this.opts.maxSpeed / Math.sqrt(m), this.vel.curr ), this.opts.maxForce ) : force; } } const defBoid2 = (pos, vel, opts) => new Boid(opts, VEC2, DIST_SQ2, pos, vel); const defBoid3 = (pos, vel, opts) => new Boid(opts, VEC3, DIST_SQ3, pos, vel); export { Boid, defBoid2, defBoid3 };