diff --git a/deno.json b/deno.json index 6fe80672..3c92acbf 100644 --- a/deno.json +++ b/deno.json @@ -1,11 +1,10 @@ { - "workspace": ["./workspaces/enums", "./workspaces/utils"], + "workspace": ["./workspaces/enums", "./workspaces/utils", "./workspaces/math"], "tasks": { "bench": "deno bench", "bench:utils": "deno bench --filter utils", "check": "deno check workspaces/**/src/**/*.ts", - "doc": "deno doc --html --name=\"@g43/tools\" --output=docs workspaces/enums/src/index.ts workspaces/utils/src/index.ts", - "doc:enums": "deno doc --html --name=\"@g43/enums\" --output=docs workspaces/enums/src/index.ts", + "doc": "deno doc --html --name=\"@g43/tools\" --output=docs workspaces/math/src/index.ts workspaces/enums/src/index.ts workspaces/utils/src/index.ts", "test": "deno test workspaces/**/src/**/*.spec.ts", "test:doc": "deno test --doc workspaces/**/src/**/*.ts", "test:coverage": "deno test --clean --coverage workspaces/**/src/**/*.spec.ts", diff --git a/workspaces/math/README.md b/workspaces/math/README.md new file mode 100644 index 00000000..f4084358 --- /dev/null +++ b/workspaces/math/README.md @@ -0,0 +1,7 @@ +[![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/G43riko/GTools/blob/master/LICENSE) +[![JSR](https://jsr.io/badges/@g43/math)](https://jsr.io/@g43/math) +[![JSR Score](https://jsr.io/badges/@g43/math/score)](https://jsr.io/@g43/math) + +# #g43/enums + +[Documentation](https://g43riko.github.io/GTools/) diff --git a/workspaces/math/deno.json b/workspaces/math/deno.json new file mode 100644 index 00000000..7eaef316 --- /dev/null +++ b/workspaces/math/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@g43/math", + "version": "0.0.1", + "exports": "./src/index.ts" +} diff --git a/workspaces/math/src/index.ts b/workspaces/math/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/workspaces/math/src/simple-vector2.ts b/workspaces/math/src/simple-vector2.ts new file mode 100644 index 00000000..394c6241 --- /dev/null +++ b/workspaces/math/src/simple-vector2.ts @@ -0,0 +1,8 @@ +export interface SimpleVector2 { + x: number; + y: number; +} +export interface ReadonlySimpleVector2 { + readonly x: number; + readonly y: number; +} diff --git a/workspaces/math/src/vector2.spec.ts b/workspaces/math/src/vector2.spec.ts new file mode 100644 index 00000000..f36fe8c9 --- /dev/null +++ b/workspaces/math/src/vector2.spec.ts @@ -0,0 +1,52 @@ +import { assertEquals } from "@std/assert"; +import { Vector2 } from "./vector2.ts"; + +const vec0_0 = new Vector2(); +const vec5_0 = new Vector2(5, 0); +const vec0_5 = new Vector2(0, 5); +const vec5_5 = new Vector2(5, 5); +const vecm5_0 = new Vector2(-5, 0); +const vec0_m5 = new Vector2(0, -5); +const vecm5_m5 = new Vector2(-5, -5); + +Deno.test("Vector2.prototype.lerp", () => { + assertEquals({ ...Vector2.lerp({ x: 0, y: 0 }, { x: 10, y: 10 }, 0) }, { x: 0, y: 0 }); + assertEquals({ ...Vector2.lerp({ x: 0, y: 0 }, { x: 10, y: 10 }, 0.25) }, { x: 2.5, y: 2.5 }); + assertEquals({ ...Vector2.lerp({ x: 0, y: 0 }, { x: 10, y: 10 }, 0.5) }, { x: 5, y: 5 }); + assertEquals({ ...Vector2.lerp({ x: 0, y: 0 }, { x: 10, y: 10 }, 0.75) }, { x: 7.5, y: 7.5 }); + assertEquals({ ...Vector2.lerp({ x: 0, y: 0 }, { x: 10, y: 10 }, 1) }, { x: 10, y: 10 }); +}) +Deno.test("Vector2.prototype.min and Vector2.prototype.max", () => { + assertEquals(5, vec5_0.max); + assertEquals(0, vec5_0.min); +}); + +Deno.test("Vector2.prototype.avg", () => { + assertEquals(vec5_0.avg, 2.5); + assertEquals(vec0_5.avg, 2.5) +}) + +Deno.test("Vector2.prototype.dist", () => { + assertEquals(vec0_0.dist(vec5_0), 5); + assertEquals(vec5_0.dist(vec0_0), 5); +}); + +Deno.test("Vector2.sqrtDist", () => { + assertEquals(Vector2.sqrtDist(vec0_0, vec5_0), 25); + assertEquals(Vector2.sqrtDist(vec5_0, vec0_0), 25); +}); +/* +Deno.test("Vector2.prototype.angle", () => { + assertEquals(Math.abs(vec5_0.angle(vec0_5) - Math.PI / 4), 0); + assertEquals(Math.abs(vec0_5.angle(vec5_0) - Math.PI / 4), 0); +}); + +Deno.test("Vector2.angle", () => { + assertEquals(Math.abs(Vector2.angle(vec5_0, vec0_5) - Math.PI / 4), 0); + assertEquals(Math.abs(Vector2.angle(vec0_5, vec5_0) - Math.PI / 4), 0); +}); +*/ +Deno.test("Vector2.prototype.normalize", () => { + assertEquals(vec5_0.normalize().toString(), "[1, 0]"); + assertEquals(vec0_5.normalize().toString(), "[0, 1]"); +}); diff --git a/workspaces/math/src/vector2.ts b/workspaces/math/src/vector2.ts new file mode 100644 index 00000000..f4d8735c --- /dev/null +++ b/workspaces/math/src/vector2.ts @@ -0,0 +1,486 @@ +import { ReadonlySimpleVector2, SimpleVector2 } from "./simple-vector2.ts"; + +type ReadonlyPair = any; +type Vector = any; +type ReadonlyMinMax2D = any; + +// eslint-disable-next-line no-use-before-define +export class Vector2 implements SimpleVector2, Vector { + public constructor( + public x = 0, + public y = 0, + ) { + } + + public static average(points: ReadonlySimpleVector2[]): ReadonlySimpleVector2 { + const result = { x: 0, y: 0 }; + points.forEach((point) => { + result.x += point.x; + result.y += point.y; + }); + result.x /= points.length; + result.y /= points.length; + + return result; + } + + public perpendicular(): Vector2 { + return new Vector2(this.y, -this.x); + } + + + public equals(vector: any): boolean { + return Vector2.equals(this, vector); + } + + public dot(vector: ReadonlySimpleVector2): number { + return Vector2.dot(this, vector); + } + + public dist(vector: ReadonlySimpleVector2): number { + return Vector2.dist(this, vector); + } + + public toArray(): ReadonlyPair { + return [this.x, this.y]; + } + + public angle(v: ReadonlySimpleVector2): number { + return Vector2.angle(this, v); + } + + public static fromAngle(angle: number): Vector2 { + return new Vector2(Math.cos(angle), Math.sin(angle)); + } + + public cross(vector: ReadonlySimpleVector2): number { + return Vector2.cross(this, vector); + } + + /** + * Rotate point around anchor + * @param angle - rotation angle in radians + * @param point + */ + public static rotate(angle: number, point: ReadonlySimpleVector2, anchor?: ReadonlySimpleVector2): Vector2 { + const anchorX = anchor?.x ?? 0; + const anchorY = anchor?.x ?? 0; + const sinAngle = Math.sin(angle); + const cosAngle = Math.cos(angle); + const x = cosAngle * (point.x - anchorX) - sinAngle * (point.y - anchorY) + anchorX; + const y = sinAngle * (point.x - anchorX) + cosAngle * (point.y - anchorY) + anchorY; + + return new Vector2(x, y); + } + + /** + * + * @param angle - rotation angle in radians + * @param anchor + */ + public rotate(angle: number, anchor?: ReadonlySimpleVector2): Vector2 { + return Vector2.rotate(angle, this, anchor); + } + + public toString(): string { + return Vector2.toString(this); + } + public static toString(vector?: unknown, toFixed = NaN): string { + + if (Vector2.isVector(vector)) { + if (!isNaN(toFixed)) { + return `[${vector.x.toFixed(toFixed)}, ${vector.y.toFixed(toFixed)}]`; + } + + return `[${vector.x}, ${vector.y}]`; + } + + return String(vector); + } + + public static sizeOf(points: readonly ReadonlySimpleVector2[], result: T = new Vector2() as unknown as T): T { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + points.forEach((point) => { + if (point.x < minX) { + minX = point.x; + } + if (point.y < minY) { + minY = point.y; + } + if (point.x > maxX) { + maxX = point.x; + } + if (point.y > maxY) { + maxY = point.y; + } + }); + result.x = maxX - minX; + result.y = maxY - minY; + + return result; + } + + public static center(points: readonly ReadonlySimpleVector2[], result: T = new Vector2() as unknown as T): T { + result.x = 0; + result.y = 0; + points.forEach((point) => { + result.x += point.x; + result.y += point.y; + }); + result.x /= points.length; + result.y /= points.length; + + return result; + } + + public static get ZERO(): Vector2 { + return new Vector2(0, 0); + } + + public static get UP(): Vector2 { + return new Vector2(0, 1); + } + + public static get LEFT(): Vector2 { + return new Vector2(-1, 0); + } + + public static get DOWN(): Vector2 { + return new Vector2(0, -1); + } + + public static get RIGHT(): Vector2 { + return new Vector2(1, 0); + } + + public static get ONE(): Vector2 { + return new Vector2(1, 1); + } + + public getAbs(result = new Vector2()): Vector2 { + result.x = Math.abs(this.x); + result.y = Math.abs(this.y); + + return result; + } + + public invert(): this { + this.x = -this.x; + this.y = -this.y; + + return this; + } + + public get avg(): number { + return this.sum / 2; + } + + public get sum(): number { + return this.x + this.y; + } + + public get max(): number { + return Math.max(this.x, this.y); + } + + public get min(): number { + return Math.min(this.x, this.y); + } + + public static fromArray(val: ReadonlyPair | Float32Array): Vector2 { + return new Vector2(val[0], val[1]); + } + + public get length(): number { + return Vector2.size(this); + } + + public static equals(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): boolean { + if (vecA === vecB) { + return true; + } + + return vecA.x === vecB.x && vecA.y === vecB.y; + } + + public static sub(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): Vector2; + public static sub(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2, result?: T): T; + public static sub(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2, result: T = new Vector2() as unknown as T): T { + result.x = vecA.x - vecB.x; + result.y = vecA.y - vecB.y; + + return result; + } + + public static cross(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): number { + return vecA.x * vecB.y - vecA.y * vecB.x; + } + + public static dot(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): number { + return vecA.x * vecB.x + vecA.y * vecB.y; + } + + public static sizeSQ(vector: ReadonlySimpleVector2): number { + return vector.x * vector.x + vector.y * vector.y; + } + + public static size(vec: ReadonlySimpleVector2): number { + return Math.sqrt(Vector2.sizeSQ(vec)); + } + + public static lerp(start: ReadonlySimpleVector2, end: ReadonlySimpleVector2, ratio: number): Vector2 + public static lerp(start: ReadonlySimpleVector2, end: ReadonlySimpleVector2, ratio: number, result?: T): T; + public static lerp(start: ReadonlySimpleVector2, end: ReadonlySimpleVector2, ratio: number, result: T = new Vector2() as unknown as T): T { + result.x = start.x + (end.x - start.x) * ratio; + result.y = start.y + (end.y - start.y) * ratio; + + return result; + } + + public static getAbs(vec: ReadonlySimpleVector2): Vector2 + public static getAbs(vec: ReadonlySimpleVector2, result?: T): T; + public static getAbs(vec: ReadonlySimpleVector2, result: T = new Vector2() as unknown as T): T { + result.x = Math.abs(vec.x); + result.y = Math.abs(vec.y); + + return result; + } + + public static from(valA: number, valB = valA): Vector2 { + return new Vector2(valA, valB); + } + + /** + * returns angle between two vectors + */ + public static angle(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): number { + const dot = Vector2.dot(vecA, vecB); + const lenA = Vector2.size(vecA); + const lenB = Vector2.size(vecB); + const cos = dot / (lenA * lenB); + + return Math.acos(cos); + } + + public static isVisible(obsX: number, obsY: number, angle: number, cutOff: number, px: number, py: number): boolean { + return angle - Math.atan2( + py - obsY, + px - obsX, + ) <= cutOff; + } + + public static createOutlineMinMax(points: readonly ReadonlySimpleVector2[]): ReadonlyMinMax2D { + const min = { + x: Infinity, + y: Infinity, + }; + const max = { + x: -Infinity, + y: -Infinity, + }; + + points.forEach((p) => { + if (p.x < min.x) { + min.x = p.x; + } + if (p.y < min.y) { + min.y = p.y; + } + if (p.x > max.x) { + max.x = p.x; + } + if (p.y > max.y) { + max.y = p.y; + } + }); + + return { min, max }; + } + + public static angleBetweenPoints(obsX: number, obsY: number, px1: number, py1: number, px2: number, py2: number): number { + return Math.atan2( + py1 - obsY, + px1 - obsX, + ) - Math.atan2( + py2 - obsY, + px2 - obsX, + ); + } + + public static isVector(item: any): item is SimpleVector2 { + return item && !isNaN(item.x) && !isNaN(item.y); + } + + public static sum(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): Vector2; + public static sum(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2, result?: T): T; + public static sum(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2, result: T = new Vector2() as unknown as T): T { + result.x = vecA.x + vecB.x; + result.y = vecA.y + vecB.y; + + return result; + } + + public static min(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2, result = new Vector2()): Vector2 { + return result.setData(Math.min(vecA.x, vecB.x), Math.min(vecA.y, vecB.y)); + } + + public static max(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2, result = new Vector2()): Vector2 { + return result.setData(Math.max(vecA.x, vecB.x), Math.max(vecA.y, vecB.y)); + } + + public static dist(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): number { + return Math.sqrt(Vector2.sqrtDist(vecA, vecB)); + } + + public static sqrtDist(vecA: ReadonlySimpleVector2, vecB: ReadonlySimpleVector2): number { + return (vecA.x - vecB.x) ** 2 + (vecA.y - vecB.y) ** 2; + } + + + public static fromVec(vec: ReadonlySimpleVector2): Vector2 { + return new Vector2(vec.x, vec.y); + } + + public isZero(): boolean { + return this.x === 0 && this.y === 0; + } + + public clone(): Vector2 { + return new Vector2(this.x, this.y); + } + + public getNormalized(result = this.clone()): Vector2 { + return Vector2.normalize(this, result); + } + + public getInverted(result = this.clone()): Vector2 { + return Vector2.invert(this, result); + } + + public normalize(): this { + const length = Vector2.size(this); + this.x /= length; + this.y /= length; + + return this; + } + + public static normalize(vec: T): T; + public static normalize(vec: SimpleVector2, result: T): T + public static normalize(vec: T, result: T = vec): T { + const length = Vector2.size(vec); + + result.x = vec.x / length; + result.y = vec.y / length; + + return result; + } + + public static invert(vec: T, result: T = vec): T { + result.x = -vec.x; + result.y = -vec.y; + + return result; + } + + public static mulNum(vecA: ReadonlySimpleVector2, val: number): Vector2; + public static mulNum(vecA: ReadonlySimpleVector2, val: number, result?: T): T; + public static mulNum(vecA: ReadonlySimpleVector2, val: number, result: T = new Vector2() as unknown as T): T { + result.x = vecA.x * val; + result.y = vecA.y * val; + + return result; + } + + public static sumNum(vecA: ReadonlySimpleVector2, val: number): Vector2; + public static sumNum(vecA: ReadonlySimpleVector2, val: number, result?: T): T; + public static sumNum(vecA: ReadonlySimpleVector2, val: number, result: T = new Vector2() as unknown as T): T { + result.x = vecA.x + val; + result.y = vecA.y + val; + + return result; + } + + public mulNums(x: number, y: number): this { + this.x *= x; + this.y *= y; + + return this; + } + + public mulNum(value: number): this { + return this.mulNums(value, value); + } + + public mul(value: ReadonlySimpleVector2): this { + return this.mulNums(value.x, value.y); + } + + public addNums(x: number, y: number): this { + this.x += x; + this.y += y; + + return this; + } + + public addNum(value: number): this { + return this.addNums(value, value); + } + + public add(value: ReadonlySimpleVector2): this { + return this.addNums(value.x, value.y); + } + + public subNums(x: number, y: number): this { + this.x -= x; + this.y -= y; + + return this; + } + + public subNum(value: number): this { + return this.subNums(value, value); + } + + public sub(value: ReadonlySimpleVector2): this { + return this.subNums(value.x, value.y); + } + + public divNums(x: number, y: number): this { + this.x /= x; + this.y /= y; + + return this; + } + + public divNum(value: number): this { + return this.divNums(value, value); + } + + public div(value: ReadonlySimpleVector2): this { + return this.divNums(value.x, value.y); + } + + public setData(x: number, y: number): this { + this.x = x; + this.y = y; + + return this; + } + + public set(vec: ReadonlySimpleVector2): this { + return this.setData(vec.x, vec.y); + } + + public static crossNum(num: number, vec: ReadonlySimpleVector2): Vector2; + public static crossNum(num: number, vec: ReadonlySimpleVector2, result?: T): T; + public static crossNum(num: number, vec: ReadonlySimpleVector2, result: T = new Vector2() as unknown as T): T { + result.x = -num * vec.y; + result.y = num * vec.x; + + return result; + } +}