@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
308 lines (255 loc) • 7.12 kB
JavaScript
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));