@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
772 lines (664 loc) • 16.4 kB
JavaScript
import { assert } from "../assert.js";
import Signal from "../events/signal/Signal.js";
import { clamp } from "../math/clamp.js";
import { EPSILON } from "../math/EPSILON.js";
import { epsilonEquals } from "../math/epsilonEquals.js";
import { lerp } from "../math/lerp.js";
import { max2 } from "../math/max2.js";
import { min2 } from "../math/min2.js";
import { computeHashFloat } from "../primitives/numbers/computeHashFloat.js";
import { v2_distance } from "./vec2/v2_distance.js";
import { v2_dot } from "./vec2/v2_dot.js";
import { v2_length } from "./vec2/v2_length.js";
import { v2_length_sqr } from "./vec2/v2_length_sqr.js";
/**
* Class representing a 2D vector. A 2D vector is an ordered pair of numbers (labeled x and y), which can be used to represent a number of things, such as:
*
* A point in 2D space (i.e. a position on a plane).
* A direction and length across a plane. The length will always be the Euclidean distance (straight-line distance) from `(0, 0)` to `(x, y)` and the direction is also measured from `(0, 0)` towards `(x, y)`.
* Any arbitrary ordered pair of numbers.
* There are other things a 2D vector can be used to represent, such as momentum vectors, complex numbers and so on, however these are the most common uses.
*
* Iterating through a Vector2 instance will yield its components `(x, y)` in the corresponding order.
*
* Backed by a `Float64Array` of length 2, so instances are usable directly as typed-array views and indexed access `v[0]`, `v[1]` aliases `v.x`, `v.y`.
*
* @implements Iterable<number>
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class Vector2 extends Float64Array {
/**
*
* @param {number} [x=0]
* @param {number} [y=0]
*/
constructor(x = 0, y = 0) {
assert.isNumber(x, 'x');
assert.isNumber(y, 'y');
assert.notNaN(x, 'x');
assert.notNaN(y, 'y');
super(2);
this[0] = x;
this[1] = y;
/**
* @readonly
* @type {Signal<number,number,number,number>}
*/
this.onChanged = new Signal();
}
/**
* Ensure that built-in `TypedArray` methods (`map`, `slice`, `subarray`, ...) do not
* try to construct a `Vector2` via the buffer-form constructor.
* @returns {Float64ArrayConstructor}
*/
static get [Symbol.species]() {
return Float64Array;
}
/**
*
* @returns {number}
*/
get x() {
return this[0];
}
/**
*
* @param {number} v
*/
set x(v) {
this[0] = v;
}
/**
*
* @returns {number}
*/
get y() {
return this[1];
}
/**
*
* @param {number} v
*/
set y(v) {
this[1] = v;
}
/**
*
* @param {number[]|Float32Array} array
* @param {number} [offset]
* @returns {this}
*/
fromArray(array, offset = 0) {
return this.set(
array[offset],
array[offset + 1]
);
}
/**
*
* @param {number[]|Float32Array} array
* @param {number} [offset]
*/
toArray(array, offset = 0) {
assert.isArrayLike(array, 'array');
assert.isNonNegativeInteger(offset, 'offset');
array[offset] = this[0];
array[offset + 1] = this[1];
}
asArray() {
const r = [];
this.toArray(r, 0);
return r;
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
set(x, y) {
assert.isNumber(x, 'x');
assert.isNumber(y, 'y');
assert.notNaN(x, 'x');
assert.notNaN(y, 'y');
const oldX = this[0];
const oldY = this[1];
if (oldX !== x || oldY !== y) {
this[0] = x;
this[1] = y;
if (this.onChanged.hasHandlers()) {
this.onChanged.send4(x, y, oldX, oldY);
}
}
return this;
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
setSilent(x, y) {
assert.isNumber(x, 'x');
assert.isNumber(y, 'y');
assert.notNaN(x, 'x');
assert.notNaN(y, 'y');
this[0] = x;
this[1] = y;
return this;
}
/**
*
* @param {number} x
* @returns {this}
*/
setX(x) {
return this.set(x, this[1]);
}
/**
*
* @param {number} y
* @returns {this}
*/
setY(y) {
return this.set(this[0], y);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
_sub(x, y) {
return this.set(this[0] - x, this[1] - y);
}
/**
*
* @param {Vector2} other
* @returns {this}
*/
sub(other) {
return this._sub(other.x, other.y);
}
/**
*
* @param {Vector2} a
* @param {Vector2} b
* @returns {this}
*/
subVectors(a, b) {
return this.set(a.x - b.x, a.y - b.y);
}
/**
* performs Math.floor operation on x and y
* @returns {this}
*/
floor() {
return this.set(Math.floor(this[0]), Math.floor(this[1]));
}
/**
* performs Math.ceil operation on x and y
* @returns {this}
*/
ceil() {
return this.set(Math.ceil(this[0]), Math.ceil(this[1]));
}
/**
* Round both components to the nearest integer
* @returns {this}
*/
round() {
const x = Math.round(this[0]);
const y = Math.round(this[1]);
return this.set(x, y);
}
/**
* performs Math.abs operation on x and y
* @returns {this}
*/
abs() {
return this.set(Math.abs(this[0]), Math.abs(this[1]));
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
_mod(x, y) {
return this.set(this[0] % x, this[1] % y);
}
/**
*
* @param {Vector2} other
* @returns {this}
*/
mod(other) {
return this._mod(other.x, other.y);
}
/**
*
* @param {Vector2} other
* @returns {this}
*/
divide(other) {
return this.set(this[0] / other.x, this[1] / other.y);
}
/**
*
* @param {Vector2} other
* @returns {this}
*/
multiply(other) {
return this._multiply(other.x, other.y);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
_multiply(x, y) {
return this.set(this[0] * x, this[1] * y);
}
/**
* Component-size MAX operator
* @param {Vector2} other
* @returns {this}
*/
max(other) {
const x = max2(this[0], other.x);
const y = max2(this[1], other.y);
return this.set(x, y);
}
/**
*
* @param {Vector2} other
* @returns {number}
*/
dot(other) {
return v2_dot(this[0], this[1], other.x, other.y);
}
/**
*
* @param {Vector2} other
* @returns {this}
*/
copy(other) {
return this.set(other.x, other.y);
}
/**
*
* @returns {Vector2}
*/
clone() {
return new Vector2(this[0], this[1]);
}
/**
*
* @returns {this}
*/
negate() {
return this.set(-this[0], -this[1]);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
_add(x, y) {
return this.set(this[0] + x, this[1] + y);
}
/**
*
* @param {Vector2} other
* @returns {this}
*/
add(other) {
return this._add(other.x, other.y);
}
/**
* `this = this + other * scale`
* @param {Vector2} other
* @param {number} scale
* @returns {this}
*/
addScaled(other, scale) {
return this._add(other.x * scale, other.y * scale);
}
/**
*
* @param {number} val
* @returns {this}
*/
addScalar(val) {
return this._add(val, val);
}
/**
*
* @param {number} val
* @returns {this}
*/
setScalar(val) {
return this.set(val, val);
}
/**
*
* @param {number} val
* @returns {this}
*/
divideScalar(val) {
return this.multiplyScalar(1 / val);
}
/**
*
* @param {number} val
* @returns {this}
*/
multiplyScalar(val) {
assert.isNumber(val, 'val');
assert.notNaN(val, 'val');
return this.set(this[0] * val, this[1] * val);
}
toJSON() {
return { x: this[0], y: this[1] };
}
fromJSON(json) {
if (typeof json === "number") {
this.set(json, json);
} else {
const { x = 0, y = 0 } = json;
this.set(x, y);
}
}
/**
*
* @param {BinaryBuffer} buffer
*/
toBinaryBuffer(buffer) {
buffer.writeFloat64(this[0]);
buffer.writeFloat64(this[1]);
}
/**
*
* @param {BinaryBuffer} buffer
*/
fromBinaryBuffer(buffer) {
const x = buffer.readFloat64();
const y = buffer.readFloat64();
this.set(x, y);
}
/**
*
* @param {BinaryBuffer} buffer
*/
toBinaryBufferFloat32(buffer) {
buffer.writeFloat32(this[0]);
buffer.writeFloat32(this[1]);
}
/**
*
* @param {BinaryBuffer} buffer
*/
fromBinaryBufferFloat32(buffer) {
const x = buffer.readFloat32();
const y = buffer.readFloat32();
this.set(x, y);
}
/**
*
* @returns {boolean}
*/
isZero() {
return this[0] === 0 && this[1] === 0;
}
/**
*
* @param {number} minX
* @param {number} minY
* @param {number} maxX
* @param {number} maxY
* @returns {this}
*/
clamp(minX, minY, maxX, maxY) {
const x = clamp(this[0], minX, maxX);
const y = clamp(this[1], minY, maxY);
return this.set(x, y);
}
/**
*
* @param {number} lowX
* @param {number} lowY
* @returns {this}
*/
clampLow(lowX, lowY) {
const x = max2(this[0], lowX);
const y = max2(this[1], lowY);
return this.set(x, y);
}
/**
*
* @param {number} highX
* @param {number} highY
* @returns {this}
*/
clampHigh(highX, highY) {
const x = min2(this[0], highX);
const y = min2(this[1], highY);
return this.set(x, y);
}
/**
*
* @param {Vector2} other
* @returns {number}
*/
distanceSqrTo(other) {
return this._distanceSqrTo(other.x, other.y);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {number}
*/
_distanceSqrTo(x, y) {
const dx = this[0] - x;
const dy = this[1] - y;
return v2_length_sqr(dx, dy);
}
/**
*
* @param {Vector2} a
* @param {Vector2} b
* @param {number} fraction
* @returns {this}
*/
lerpVectors(a, b, fraction) {
const x = lerp(a.x, b.x, fraction);
const y = lerp(a.y, b.y, fraction);
return this.set(x, y);
}
/**
*
* @param {number[]} matrix3
* @returns {this}
*/
applyMatrix3(matrix3) {
const x = this[0];
const y = this[1];
const _x = matrix3[0] * x + matrix3[3] * y + matrix3[6];
const _y = matrix3[1] * x + matrix3[4] * y + matrix3[7];
return this.set(_x, _y);
}
/**
*
* @param {Vector2} other
* @returns {number}
*/
distanceTo(other) {
return this._distanceTo(other.x, other.y);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {number}
*/
_distanceTo(x, y) {
return Math.sqrt(this._distanceSqrTo(x, y));
}
/**
*
* @param {Vector2} other
* @returns {number}
*/
manhattanDistanceTo(other) {
const dx = Math.abs(this[0] - other.x);
const dy = Math.abs(this[1] - other.y);
return dx + dy;
}
/**
* @returns {number}
*/
length() {
return v2_length(this[0], this[1]);
}
/**
* Normalizes the vector, preserving its direction, but making magnitude equal to 1
* @returns {this}
*/
normalize() {
// NOTE: call v2_length directly rather than via `this.length()`.
// Float64Array exposes `length` as an exotic instance property that V8 inlines
// through a fast path which bypasses the prototype chain; once an optimized
// function caller has been specialized for Float64Array shape, `this.length`
// is read as the array length (a number) rather than our prototype method.
const l = v2_length(this[0], this[1]);
if (l === 0) {
//special case, can't normalize 0 length vector
return this;
}
const m = 1 / l;
return this.multiplyScalar(m);
}
/**
*
* @returns {number}
*/
hash() {
const x = computeHashFloat(this[0]);
const y = computeHashFloat(this[1]);
return ((x << 5) - x) + y;
}
/**
* Rotation is counter-clockwise
* @param {number} angle in radians
* @returns {this}
*/
rotate(angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
const _x = this[0];
const _y = this[1];
const x = _x * cos - _y * sin
const y = _x * sin + _y * cos;
return this.set(x, y);
}
/**
*
* @param {function} processor
* @param {*} [thisArg]
* @returns {this}
*/
process(processor, thisArg) {
processor.call(thisArg, this[0], this[1]);
this.onChanged.add(processor, thisArg);
return this;
}
toString() {
return `Vector2{ x:${this[0]}, y:${this[1]} }`;
}
/**
*
* @param {Vector2} other
* @returns {boolean}
*/
equals(other) {
return this[0] === other.x
&& this[1] === other.y
;
}
/**
*
* @param {Vector2} other
* @param {number} [tolerance]
* @return {boolean}
*/
roughlyEquals(other, tolerance) {
return this._roughlyEquals(other.x, other.y, tolerance);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} [tolerance] acceptable deviation
* @return {boolean}
*/
_roughlyEquals(x, y, tolerance = EPSILON) {
return epsilonEquals(this[0], x, tolerance)
&& epsilonEquals(this[1], y, tolerance);
}
}
/**
* @readonly
* @type {Vector2}
*/
Vector2.up = new Vector2(0, 1);
/**
* @readonly
* @type {Vector2}
*/
Vector2.down = new Vector2(0, -1);
/**
* @readonly
* @type {Vector2}
*/
Vector2.left = new Vector2(-1, 0);
/**
* @readonly
* @type {Vector2}
*/
Vector2.right = new Vector2(1, 0);
/**
* @readonly
* @type {Vector2}
*/
Vector2.zero = new Vector2(0, 0);
/**
* @readonly
* @type {Vector2}
*/
Vector2.one = new Vector2(1, 1);
/**
* @deprecated use {@link Vector2#toArray} instead
*/
Vector2.prototype.writeToArray = Vector2.prototype.toArray;
/**
* @deprecated use {@link Vector2#fromArray} instead
*/
Vector2.prototype.readFromArray = Vector2.prototype.fromArray;
Vector2._distance = v2_distance;
/**
* @readonly
* @type {boolean}
*/
Vector2.prototype.isVector2 = true;
/**
* @readonly
* @type {string}
*/
Vector2.typeName = "Vector2";
export default Vector2;