UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

772 lines (664 loc) • 16.4 kB
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;