UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

745 lines (635 loc) • 15.2 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. * * @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;