UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

308 lines (255 loc) • 7.12 kB
import { assert } from "../../assert.js"; import Signal from "../../events/signal/Signal.js"; import { computeHashFloat } from "../../primitives/numbers/computeHashFloat.js"; import { inverseLerp } from "../inverseLerp.js"; /** * Defines a numeric interval, by min and max. * Provides a suite of interval logic operations. * Interval can be of 0 width. */ export class NumericInterval { /** * * @param {number} [min=-Infinity] * @param {number} [max=Infinity] * @constructor */ constructor( min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY ) { assert.isNumber(min, 'min'); assert.isNumber(max, 'max'); assert.greaterThanOrEqual(max, min, `max [${max}] must be >= than min[${min}]`); /** * * @type {number} */ this.min = min; /** * * @type {number} */ this.max = max; /** * @readonly * @type {Signal<number, number, number, number>} */ this.onChanged = new Signal(); } /** * * @param {number} min * @param {number} max * @returns {this} */ set(min, max) { assert.isNumber(min, 'min'); assert.isNumber(max, 'max'); assert.notNaN(min, 'min'); assert.notNaN(max, 'max'); assert.greaterThanOrEqual(max, min, `max [${max}] must be >= than min[${min}]`); const oldMin = this.min; const oldMax = this.max; if (min === oldMin && max === oldMax) { // no change return this; } this.min = min; this.max = max; if (!this.onChanged.hasHandlers()) { return this; } this.onChanged.send4(min, max, oldMin, oldMax); return this; } /** * * @param {number} value * @returns {this} */ multiplyScalar(value) { const v0 = this.min * value; const v1 = this.max * value; if (v0 > v1) { //probably negative scale this.set(v1, v0); } else { this.set(v0, v1); } return this; } /** * Compute normalized position of input within this interval, where result is 0 if input is equal to `min`, and 1 when input is equal to `max` * @param {number} v value to be normalized * @returns {number} value between 0..1 if input is within [min,max] range, otherwise result will be extrapolated proportionately outside the 0,1 region */ normalizeValue(v) { return inverseLerp(this.min, this.max, v); } /** * Both min and max are exactly 0 * @returns {boolean} */ isZero() { return this.min === 0 && this.max === 0; } /** * Whether min and max are the same * In other words if span is 0 * @returns {boolean} */ isExact() { return this.min === this.max; } /** * * @param {function} random Random number generator function, must return values between 0 and 1 * @returns {number} */ sampleRandom(random) { assert.isFunction(random, "random"); const span = this.max - this.min; return this.min + random() * span; } fromJSON(json) { this.set(json.min, json.max); } toJSON() { return { min: this.min, max: this.max }; } /** * * @param {number[]|Float32Array|Float64Array} array * @param {number} [offset=0] */ fromArray(array, offset = 0) { return this.set(array[offset], array[offset + 1]); } /** * * @param {number[]|Float32Array|Float64Array} [destination=[]] * @param {number} [offset=0] * @return {number[]} */ toArray(destination = [], offset = 0) { destination[offset] = this.min; destination[offset + 1] = this.max; return destination; } toString() { return `NumericInterval{ min=${this.min}, max=${this.max} }`; } /** * * @param {BinaryBuffer} buffer */ toBinaryBuffer(buffer) { buffer.writeFloat64(this.min); buffer.writeFloat64(this.max); } /** * * @param {BinaryBuffer} buffer */ fromBinaryBuffer(buffer) { this.min = buffer.readFloat64(); this.max = buffer.readFloat64(); } /** * * @param {NumericInterval} other * @returns {this} */ copy(other) { return this.set(other.min, other.max); } /** * Performs union operation with another interval, mutates current interval. * @param {NumericInterval} other * @returns {this} */ union(other) { assert.equal(other.isNumericInterval, true, "other must be a NumericInterval"); return this.set( Math.min(this.min, other.min), Math.max(this.max, other.max) ); } /** * * @param {NumericInterval} other * @return {boolean} */ overlaps(other) { assert.equal(other.isNumericInterval, true, "other must be a NumericInterval"); return this.min < other.max && this.max > other.min; } /** * * @param {NumericInterval} other * @return {boolean} */ contains(other) { assert.equal(other.isNumericInterval, true, "other must be a NumericInterval"); return this.min <= other.min && this.max >= other.max; } /** * * @param {NumericInterval} other * @returns {boolean} */ equals(other) { assert.equal(other.isNumericInterval, true, "other must be a NumericInterval"); return this.min === other.min && this.max === other.max; } /** * * @returns {number} */ hash() { let hash = computeHashFloat(this.min); hash = ((hash << 5) - hash) + computeHashFloat(this.max); return hash; } /** * Distance between min and max (`= max - min`) * @returns {number} */ get span() { return this.max - this.min; } get middle() { return (this.max + this.min) * 0.5; } /** * @deprecated use {@link middle} instead * @returns {number} */ computeAverage() { return this.middle; } } /** * @readonly * @type {boolean} */ NumericInterval.prototype.isNumericInterval = true; /** * @readonly * @type {NumericInterval} */ NumericInterval.zero_zero = Object.freeze(new NumericInterval(0, 0)); /** * @readonly * @type {NumericInterval} */ NumericInterval.zero_one = Object.freeze(new NumericInterval(0, 1)); /** * @readonly * @type {NumericInterval} */ NumericInterval.one_one = Object.freeze(new NumericInterval(1, 1));