@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
745 lines (635 loc) • 15.2 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.
*
* @implements Iterable<number>
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class Vector2 {
/**
*
* @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');
/**
*
* @type {number}
*/
this.x = x;
/**
*
* @type {number}
*/
this.y = y;
/**
* @readonly
* @type {Signal<number,number,number,number>}
*/
this.onChanged = new Signal();
}
/**
*
* @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.x;
array[offset + 1] = this.y;
}
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.x;
const oldY = this.y;
if (oldX !== x || oldY !== y) {
this.x = x;
this.y = 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.x = x;
this.y = y;
return this;
}
/**
*
* @param {number} x
* @returns {this}
*/
setX(x) {
return this.set(x, this.y);
}
/**
*
* @param {number} y
* @returns {this}
*/
setY(y) {
return this.set(this.x, y);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
_sub(x, y) {
return this.set(this.x - x, this.y - 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.x), Math.floor(this.y));
}
/**
* performs Math.ceil operation on x and y
* @returns {this}
*/
ceil() {
return this.set(Math.ceil(this.x), Math.ceil(this.y));
}
/**
* Round both components to the nearest integer
* @returns {this}
*/
round() {
const x = Math.round(this.x);
const y = Math.round(this.y);
return this.set(x, y);
}
/**
* performs Math.abs operation on x and y
* @returns {this}
*/
abs() {
return this.set(Math.abs(this.x), Math.abs(this.y));
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
_mod(x, y) {
return this.set(this.x % x, this.y % 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.x / other.x, this.y / 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.x * x, this.y * y);
}
/**
* Component-size MAX operator
* @param {Vector2} other
* @returns {this}
*/
max(other) {
const x = max2(this.x, other.x);
const y = max2(this.y, other.y);
return this.set(x, y);
}
/**
*
* @param {Vector2} other
* @returns {number}
*/
dot(other) {
return v2_dot(this.x, this.y, 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.x, this.y);
}
/**
*
* @returns {this}
*/
negate() {
return this.set(-this.x, -this.y);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
_add(x, y) {
return this.set(this.x + x, this.y + y);
}
/**
*
* @param {Vector2} other
* @returns {this}
*/
add(other) {
return this._add(other.x, other.y);
}
/**
*
* @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.x * val, this.y * val);
}
toJSON() {
return { x: this.x, y: this.y };
}
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.x);
buffer.writeFloat64(this.y);
}
/**
*
* @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.x);
buffer.writeFloat32(this.y);
}
/**
*
* @param {BinaryBuffer} buffer
*/
fromBinaryBufferFloat32(buffer) {
const x = buffer.readFloat32();
const y = buffer.readFloat32();
this.set(x, y);
}
/**
*
* @returns {boolean}
*/
isZero() {
return this.x === 0 && this.y === 0;
}
/**
*
* @param {number} minX
* @param {number} minY
* @param {number} maxX
* @param {number} maxY
* @returns {this}
*/
clamp(minX, minY, maxX, maxY) {
const x = clamp(this.x, minX, maxX);
const y = clamp(this.y, minY, maxY);
return this.set(x, y);
}
/**
*
* @param {number} lowX
* @param {number} lowY
* @returns {this}
*/
clampLow(lowX, lowY) {
const x = max2(this.x, lowX);
const y = max2(this.y, lowY);
return this.set(x, y);
}
/**
*
* @param {number} highX
* @param {number} highY
* @returns {this}
*/
clampHigh(highX, highY) {
const x = min2(this.x, highX);
const y = min2(this.y, 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.x - x;
const dy = this.y - 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.x;
const y = this.y;
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.x - other.x);
const dy = Math.abs(this.y - other.y);
return dx + dy;
}
/**
* @returns {number}
*/
length() {
return v2_length(this.x, this.y);
}
/**
* Normalizes the vector, preserving its direction, but making magnitude equal to 1
* @returns {this}
*/
normalize() {
const l = this.length();
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.x);
const y = computeHashFloat(this.y);
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.x;
const _y = this.y;
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.x, this.y);
this.onChanged.add(processor, thisArg);
return this;
}
toString() {
return `Vector2{ x:${this.x}, y:${this.y} }`;
}
/**
*
* @param {Vector2} other
* @returns {boolean}
*/
equals(other) {
return this.x === other.x
&& this.y === 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.x, x, tolerance)
&& epsilonEquals(this.y, y, tolerance);
}
get 0() {
return this.x;
}
get 1() {
return this.y;
}
set 0(v) {
this.x = v;
}
set 1(v) {
this.y = v;
}
* [Symbol.iterator]() {
yield this.x;
yield this.y;
}
}
/**
* @readonly
* @type {Vector2}
*/
Vector2.up = Object.freeze(new Vector2(0, 1));
/**
* @readonly
* @type {Vector2}
*/
Vector2.down = Object.freeze(new Vector2(0, -1));
/**
* @readonly
* @type {Vector2}
*/
Vector2.left = Object.freeze(new Vector2(-1, 0));
/**
* @readonly
* @type {Vector2}
*/
Vector2.right = Object.freeze(new Vector2(1, 0));
/**
* @readonly
* @type {Vector2}
*/
Vector2.zero = Object.freeze(new Vector2(0, 0));
/**
* @readonly
* @type {Vector2}
*/
Vector2.one = Object.freeze(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;