From 601bc512331a5c5f9c0df5c8edde7d07f026e950 Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 3 Nov 2023 04:51:50 -0600 Subject: [PATCH 1/4] Three types of collision and a polygon class --- .vscode/settings.json | 27 +++--- bundle.js | 211 ++++++++++++++++++++++++++++++++---------- collision/aa.ts | 16 ++++ collision/circular.ts | 15 +++ collision/sat.ts | 105 +++++++++++++++++++++ deno.jsonc | 4 +- geometry/polygon.ts | 97 +++++++++++++++++++ geometry/vector.ts | 92 ++++++++++++------ main.ts | 117 ++++++++++++++++------- zoomableCanvas.ts | 2 +- 10 files changed, 561 insertions(+), 125 deletions(-) create mode 100644 collision/aa.ts create mode 100644 collision/circular.ts create mode 100644 collision/sat.ts create mode 100644 geometry/polygon.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index fe50924..be43d98 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,28 @@ { "workbench.colorCustomizations": { - "activityBar.activeBackground": "#d816d8", - "activityBar.background": "#d816d8", + "activityBar.activeBackground": "#2f7c47", + "activityBar.background": "#2f7c47", "activityBar.foreground": "#e7e7e7", "activityBar.inactiveForeground": "#e7e7e799", - "activityBarBadge.background": "#caca15", - "activityBarBadge.foreground": "#15202b", + "activityBarBadge.background": "#422c74", + "activityBarBadge.foreground": "#e7e7e7", "commandCenter.border": "#e7e7e799", - "sash.hoverBorder": "#d816d8", - "statusBar.background": "#aa11aa", + "sash.hoverBorder": "#2f7c47", + "statusBar.background": "#215732", "statusBar.foreground": "#e7e7e7", - "statusBarItem.hoverBackground": "#d816d8", - "statusBarItem.remoteBackground": "#aa11aa", + "statusBarItem.hoverBackground": "#2f7c47", + "statusBarItem.remoteBackground": "#215732", "statusBarItem.remoteForeground": "#e7e7e7", - "titleBar.activeBackground": "#aa11aa", + "titleBar.activeBackground": "#215732", "titleBar.activeForeground": "#e7e7e7", - "titleBar.inactiveBackground": "#aa11aa99", + "titleBar.inactiveBackground": "#21573299", "titleBar.inactiveForeground": "#e7e7e799" }, - "peacock.remoteColor": "aa11aa", + "peacock.remoteColor": "#215732", "deno.enable": true, "deno.unstable": true, - "liveServer.settings.port": 5501 + "liveServer.settings.port": 5501, + "cSpell.words": [ + "deadzone" + ] } \ No newline at end of file diff --git a/bundle.js b/bundle.js index 98e3b92..95dbaac 100644 --- a/bundle.js +++ b/bundle.js @@ -2,6 +2,9 @@ // deno-lint-ignore-file // This code was bundled using `deno bundle` and it's not recommended to edit it manually +const axisAlignedCollision = (aa1, aa2)=>{ + return aa1.x < aa2.x + aa2.w && aa1.x + aa1.w > aa2.x && aa1.y < aa2.y + aa2.h && aa1.y + aa1.h > aa2.y; +}; const Constants = { TWO_PI: Math.PI * 2 }; @@ -10,9 +13,15 @@ class Vector { y; z; constructor(x = 0, y = 0, z = 0){ - this.x = x; - this.y = y; - this.z = z; + if (typeof x === "number") { + this.x = x; + this.y = y; + this.z = z; + } else { + this.x = x.x; + this.y = x.y || y; + this.z = x.z || z; + } } set(v, y, z) { if (arguments.length === 1 && typeof v !== "number") { @@ -47,7 +56,7 @@ class Vector { } } add(v, y, z) { - if (arguments.length === 1 && typeof v !== 'number') { + if (arguments.length === 1 && typeof v !== "number") { this.x += v.x; this.y += v.y; this.z += v.z; @@ -62,7 +71,7 @@ class Vector { return this; } sub(v, y, z) { - if (arguments.length === 1 && typeof v !== 'number') { + if (arguments.length === 1 && typeof v !== "number") { this.x -= v.x; this.y -= v.y; this.z -= v.z; @@ -77,7 +86,7 @@ class Vector { return this; } mult(v) { - if (typeof v === 'number') { + if (typeof v === "number") { this.x *= v; this.y *= v; this.z *= v; @@ -89,7 +98,7 @@ class Vector { return this; } div(v) { - if (typeof v === 'number') { + if (typeof v === "number") { this.x /= v; this.y /= v; this.z /= v; @@ -113,7 +122,7 @@ class Vector { return Math.sqrt(dx * dx + dy * dy + dz * dz); } dot(v, y, z) { - if (arguments.length === 1 && typeof v !== 'number') { + if (arguments.length === 1 && typeof v !== "number") { return this.x * v.x + this.y * v.y + this.z * v.z; } return this.x * v + this.y * y + this.z * z; @@ -127,7 +136,7 @@ class Vector { return start + (stop - start) * amt; }; let x, y; - if (arguments.length === 2 && typeof v_or_x !== 'number') { + if (arguments.length === 2 && typeof v_or_x !== "number") { amt = amt_or_y; x = v_or_x.x; y = v_or_x.y; @@ -174,13 +183,28 @@ class Vector { copy() { return new Vector(this.x, this.y, this.z); } - drawDot() { + drawDot(color) { if (!doodler) return; doodler.dot(this, { weight: 2, - color: 'red' + color: color || "red" }); } + draw() { + if (!doodler) return; + const startPoint = new Vector(); + doodler.dot(new Vector(), { + weight: 4, + color: "orange" + }); + doodler.line(startPoint, startPoint.copy().add(this.copy().normalize().mult(700))); + doodler.line(startPoint, startPoint.copy().sub(this.copy().normalize().mult(700))); + } + normal(v) { + const dx = v.x - this.x; + const dy = v.y - this.y; + return new Vector(-dy, dx); + } static fromAngle(angle, v) { if (v === undefined || v === null) { v = new Vector(); @@ -224,9 +248,9 @@ class Vector { return Math.acos(v1.dot(v2) / Math.sqrt(v1.magSq() * v2.magSq())); } static lerp(v1, v2, amt) { - const retval = new Vector(v1.x, v1.y, v1.z); - retval.lerp(v2, amt); - return retval; + const val = new Vector(v1.x, v1.y, v1.z); + val.lerp(v2, amt); + return val; } static vectorProjection(v1, v2) { v2 = v2.copy(); @@ -235,6 +259,16 @@ class Vector { v2.mult(sp); return v2; } + static vectorProjectionAndDot(v1, v2) { + v2 = v2.copy(); + v2.normalize(); + const sp = v1.dot(v2); + v2.mult(sp); + return [ + v2, + sp + ]; + } static hypot2(a, b) { return Vector.dot(Vector.sub(a, b), Vector.sub(a, b)); } @@ -618,7 +652,7 @@ class ZoomableDoodler extends Doodler { e.preventDefault(); this.dragging = false; }); - this._canvas.addEventListener("mouseleave", (e)=>{ + this._canvas.addEventListener("mouseleave", (_e)=>{ this.dragging = false; }); this._canvas.addEventListener("mousemove", (e)=>{ @@ -831,50 +865,131 @@ const init = (opt, zoomable, postInit)=>{ window.doodler = zoomable ? new ZoomableDoodler(opt, postInit) : new Doodler(opt, postInit); window.doodler.init(); }; +class Polygon { + points; + center; + constructor(points){ + this.points = points.map((p)=>new Vector(p)); + this.center = this.calcCenter(); + } + draw(color) { + for(let i = 0; i < this.points.length; i++){ + const p1 = this.points[i]; + const p2 = this.points.at(i - this.points.length + 1); + doodler.line(p1.copy().add(this.center), p2.copy().add(this.center), { + color + }); + const { x, y, w, h } = this.aaHitbox(); + doodler.drawRect(new Vector(x, y), w, h, { + color: "lime" + }); + } + } + calcCenter() { + if (!this.points.length) return new Vector(); + const center = new Vector(); + for (const point of this.points){ + center.add(point); + } + center.div(this.points.length); + return center; + } + circularHitbox() { + let greatestDistance = 0; + for (const p of this.points){ + greatestDistance = Math.max(p.copy().add(this.center).dist(this.center), greatestDistance); + } + return { + center: this.center.copy(), + radius: greatestDistance + }; + } + aaHitbox() { + let smallestX, biggestX, smallestY, biggestY; + smallestX = smallestY = Infinity; + biggestX = biggestY = -Infinity; + for (const p of this.points){ + const temp = p.copy().add(this.center); + smallestX = Math.min(temp.x, smallestX); + biggestX = Math.max(temp.x, biggestX); + smallestY = Math.min(temp.y, smallestY); + biggestY = Math.max(temp.y, biggestY); + } + return { + x: smallestX, + y: smallestY, + w: biggestX - smallestX, + h: biggestY - smallestY + }; + } + static createPolygon(sides = 3, radius = 100) { + sides = Math.round(sides); + if (sides < 3) { + throw "You need at least 3 sides for a polygon"; + } + const poly = new Polygon([]); + const rotangle = Math.PI * 2 / sides; + let angle = 0; + for(let i = 0; i < sides; i++){ + angle = i * rotangle + (Math.PI - rotangle) * 0.5; + const pt = new Vector(Math.cos(angle) * radius, Math.sin(angle) * radius); + poly.points.push(pt); + } + poly.center = poly.calcCenter(); + return poly; + } +} init({ - width: 400, - height: 400 + width: 2400, + height: 1200, + bg: "#333" }, true, (ctx)=>{ ctx.imageSmoothingEnabled = false; }); -new Vector(100, 300); -const v = new Vector(30, 30); -doodler.registerDraggable(v, 20); const img = new Image(); img.src = "./skeleton.png"; img.hidden; document.body.append(img); -const p = new Vector(200, 200); -doodler.createLayer(()=>{ +const p = new Vector(500, 500); +const poly = new Polygon([ + { + x: -50, + y: -25 + }, + { + x: 25, + y: -50 + }, + { + x: 25, + y: 25 + }, + { + x: -25, + y: 25 + } +]); +const poly2 = Polygon.createPolygon(5, 75); +poly2.center = p.copy(); +poly.center.add(p); +doodler.createLayer((c)=>{ + for(let i = 0; i < c.canvas.width; i += 50){ + for(let j = 0; j < c.canvas.height; j += 50){ + doodler.drawSquare(new Vector(i, j), 50, { + color: "#00000010" + }); + } + } + const color = axisAlignedCollision(poly.aaHitbox(), poly2.aaHitbox()) ? "red" : "black"; + poly.draw(color); + poly2.draw(color); const [gamepad] = navigator.getGamepads(); if (gamepad) { const leftX = gamepad.axes[0]; const leftY = gamepad.axes[1]; - p.add(Math.min(Math.max(leftX - 0.04, 0), leftX + 0.04), Math.min(Math.max(leftY - 0.04, 0), leftY + 0.04)); - const rigthX = gamepad.axes[2]; - const rigthY = gamepad.axes[3]; - doodler.moveOrigin({ - x: -rigthX * 5, - y: -rigthY * 5 - }); - if (gamepad.buttons[7].value) { - doodler.scaleAt({ - x: 200, - y: 200 - }, 1 + gamepad.buttons[7].value / 5); - } - if (gamepad.buttons[6].value) { - doodler.scaleAt({ - x: 200, - y: 200 - }, 1 - gamepad.buttons[6].value / 5); - } - } - doodler.drawImageWithOutline(img, p); -}); -document.addEventListener("keyup", (e)=>{ - e.preventDefault(); - if (e.key === " ") { - doodler.unregisterDraggable(v); + poly.center.add(new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)).mult(10)); + const rightX = gamepad.axes[2]; + const rightY = gamepad.axes[3]; + poly2.center.add(new Vector(Math.min(Math.max(rightX - 0.05, 0), rightX + 0.05), Math.min(Math.max(rightY - 0.05, 0), rightY + 0.05)).mult(10)); } }); diff --git a/collision/aa.ts b/collision/aa.ts new file mode 100644 index 0000000..7387e69 --- /dev/null +++ b/collision/aa.ts @@ -0,0 +1,16 @@ +import { Point } from "../geometry/vector.ts"; + +export type axisAlignedBoundingBox = { + w: number; + h: number; +} & Point; + +export const axisAlignedCollision = ( + aa1: axisAlignedBoundingBox, + aa2: axisAlignedBoundingBox, +) => { + return aa1.x < aa2.x + aa2.w && + aa1.x + aa1.w > aa2.x && + aa1.y < aa2.y + aa2.h && + aa1.y + aa1.h > aa2.y; +}; diff --git a/collision/circular.ts b/collision/circular.ts new file mode 100644 index 0000000..a430ba6 --- /dev/null +++ b/collision/circular.ts @@ -0,0 +1,15 @@ +import { Point } from "../geometry/vector.ts"; +import { Vector } from "../mod.ts"; + +export type CircleLike = { + center: Point; + radius: number; +}; + +export const circularCollision = (c1: CircleLike, c2: CircleLike) => { + const center1 = new Vector(c1.center); + const center2 = new Vector(c2.center); + const maxDist = c1.radius + c2.radius; + + return Vector.dist(center1, center2) < maxDist; +}; diff --git a/collision/sat.ts b/collision/sat.ts new file mode 100644 index 0000000..84383d1 --- /dev/null +++ b/collision/sat.ts @@ -0,0 +1,105 @@ +import { Polygon } from "../geometry/polygon.ts"; +import { Point, Vector } from "../geometry/vector.ts"; + +export const satCollision = (s1: Polygon, s2: Polygon) => { + const shape1 = s1.points.map((p) => new Vector(p).add(s1.center)); + const shape2 = s2.points.map((p) => new Vector(p).add(s2.center)); + + if (shape1.length < 2 || shape2.length < 2) { + throw "Insufficient shape data in satCollision"; + } + const offset = new Vector(s1.center).sub(s2.center); + // Take one side of the polygon and find the normal + let last: Vector[] = []; + for (let i = 0; i < shape1.length; i++) { + const axis = shape1[i].normal(shape1.at(i - 1)!); + // axis.draw(); + let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); + let [p1max, p1maxDot] = [p1min, p1minDot]; + for (const point of shape1) { + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + // projected.drawDot(); + p1min = dot < p1minDot ? projected : p1min; + p1minDot = Math.min(dot, p1minDot); + p1max = dot > p1maxDot ? projected : p1max; + p1maxDot = Math.max(dot, p1maxDot); + } + let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); + let [p2max, p2maxDot] = [p2min, p2minDot]; + for (const point of shape2) { + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + // projected.drawDot(); + p2min = dot < p2minDot ? projected : p2min; + p2minDot = Math.min(dot, p2minDot); + p2max = dot > p2maxDot ? projected : p2max; + p2maxDot = Math.max(dot, p2maxDot); + } + + // const scale = axis.dot(offset); + // p1max += scale; + // p1min += scale; + + // let p2min = axis.dot(shape1[0]); + // let p2max = p2min; + // for (const point of shape2) { + // const dot = point.dot(axis); + // p2max = Math.max(p2max, dot); + // p2min = Math.min(p2min, dot); + // Vector.vectorProjection(point, axis).drawDot(); + // } + + if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + // axis.draw(); + // p1max.drawDot("blue"); + // p1min.drawDot("lime"); + // p2max.drawDot("blue"); + // p2min.drawDot("lime"); + return false; + } + last = [axis, p1max, p1min, p2max, p2min]; + } + for (let i = 0; i < shape2.length; i++) { + const axis = shape2[i].normal(shape2.at(i - 1)!); + // axis.draw(); + let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); + let [p1max, p1maxDot] = [p1min, p1minDot]; + for (const point of shape2) { + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + // projected.drawDot(); + p1min = dot < p1minDot ? projected : p1min; + p1minDot = Math.min(dot, p1minDot); + p1max = dot > p1maxDot ? projected : p1max; + p1maxDot = Math.max(dot, p1maxDot); + } + let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); + let [p2max, p2maxDot] = [p2min, p2minDot]; + for (const point of shape1) { + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + // projected.drawDot(); + p2min = dot < p2minDot ? projected : p2min; + p2minDot = Math.min(dot, p2minDot); + p2max = dot > p2maxDot ? projected : p2max; + p2maxDot = Math.max(dot, p2maxDot); + } + + if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + // axis.draw(); + // p1max.drawDot("blue"); + // p1min.drawDot("lime"); + // p2max.drawDot("blue"); + // p2min.drawDot("lime"); + return false; + } + last = [axis, p1max, p1min, p2max, p2min]; + } + + // for (const [i, l] of last.entries()) { + // if (i === 0) { + // l.draw(); + // } else { + // l.drawDot(); + // } + // } + + return true; +}; diff --git a/deno.jsonc b/deno.jsonc index c24fed6..993cb74 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -2,13 +2,13 @@ "compilerOptions": { "lib": [ "DOM", - "es2015" + "es2023" ], "types": [ "./global.d.ts" ] }, "tasks": { - "dev" : "deno bundle --watch main.ts bundle.js" + "dev": "deno bundle --watch main.ts bundle.js" } } \ No newline at end of file diff --git a/geometry/polygon.ts b/geometry/polygon.ts new file mode 100644 index 0000000..d2927a1 --- /dev/null +++ b/geometry/polygon.ts @@ -0,0 +1,97 @@ +import { axisAlignedBoundingBox } from "../collision/aa.ts"; +import { CircleLike } from "../collision/circular.ts"; +import { Vector } from "../mod.ts"; +import { Point } from "./vector.ts"; + +export class Polygon { + points: Vector[]; + center: Vector; + + constructor(points: Point[]) { + this.points = points.map((p) => new Vector(p)); + this.center = this.calcCenter(); + } + + draw(color?: string) { + for (let i = 0; i < this.points.length; i++) { + const p1 = this.points[i]; + const p2 = this.points.at(i - this.points.length + 1)!; + doodler.line(p1.copy().add(this.center), p2.copy().add(this.center), { + color, + }); + const { x, y, w, h } = this.aaHitbox(); + doodler.drawRect(new Vector(x, y), w, h, { color: "lime" }); + } + } + + calcCenter() { + if (!this.points.length) return new Vector(); + const center = new Vector(); + + for (const point of this.points) { + center.add(point); + } + center.div(this.points.length); + return center; + } + + circularHitbox(): CircleLike { + let greatestDistance = 0; + for (const p of this.points) { + greatestDistance = Math.max( + p.copy().add(this.center).dist(this.center), + greatestDistance, + ); + } + + return { + center: this.center.copy(), + radius: greatestDistance, + }; + } + + aaHitbox(): axisAlignedBoundingBox { + let smallestX, biggestX, smallestY, biggestY; + smallestX = + smallestY = + Infinity; + biggestX = + biggestY = + -Infinity; + + for (const p of this.points) { + const temp = p.copy().add(this.center); + smallestX = Math.min(temp.x, smallestX); + biggestX = Math.max(temp.x, biggestX); + smallestY = Math.min(temp.y, smallestY); + biggestY = Math.max(temp.y, biggestY); + } + + return { + x: smallestX, + y: smallestY, + w: biggestX - smallestX, + h: biggestY - smallestY, + }; + } + + static createPolygon(sides = 3, radius = 100) { + sides = Math.round(sides); + if (sides < 3) { + throw "You need at least 3 sides for a polygon"; + } + + const poly = new Polygon([]); + // figure out the angles required + const rotangle = (Math.PI * 2) / sides; + let angle = 0; + // loop through and generate each point + for (let i = 0; i < sides; i++) { + angle = (i * rotangle) + ((Math.PI - rotangle) * 0.5); + const pt = new Vector(Math.cos(angle) * radius, Math.sin(angle) * radius); + poly.points.push(pt); + } + poly.center = poly.calcCenter(); + return poly; + } +} diff --git a/geometry/vector.ts b/geometry/vector.ts index 9d15ed4..fbf0287 100644 --- a/geometry/vector.ts +++ b/geometry/vector.ts @@ -7,10 +7,19 @@ export class Vector implements Point { y: number; z: number; - constructor(x = 0, y = 0, z = 0) { - this.x = x; - this.y = y; - this.z = z; + constructor(); + constructor(p: Point); + constructor(x: number, y: number, z?: number); + constructor(x: number | Point = 0, y = 0, z = 0) { + if (typeof x === "number") { + this.x = x; + this.y = y; + this.z = z; + } else { + this.x = x.x; + this.y = x.y || y; + this.z = x.z || z; + } } set(x: number, y: number, z?: number): void; @@ -18,9 +27,11 @@ export class Vector implements Point { set(v: [number, number, number]): void; set(v: Vector | [number, number, number] | number, y?: number, z?: number) { if (arguments.length === 1 && typeof v !== "number") { - this.set((v as Vector).x || (v as Array)[0] || 0, + this.set( + (v as Vector).x || (v as Array)[0] || 0, (v as Vector).y || (v as Array)[1] || 0, - (v as Vector).z || (v as Array)[2] || 0); + (v as Vector).z || (v as Array)[2] || 0, + ); } else { this.x = v as number; this.y = y || 0; @@ -43,7 +54,7 @@ export class Vector implements Point { return (x * x + y * y + z * z); } setMag(len: number): void; - setMag(v: Vector, len: number): Vector + setMag(v: Vector, len: number): Vector; setMag(v_or_len: Vector | number, len?: number) { if (len === undefined) { len = v_or_len as number; @@ -60,7 +71,7 @@ export class Vector implements Point { add(x: number, y: number): Vector; add(v: Vector): Vector; add(v: Vector | number, y?: number, z?: number) { - if (arguments.length === 1 && typeof v !== 'number') { + if (arguments.length === 1 && typeof v !== "number") { this.x += v.x; this.y += v.y; this.z += v.z; @@ -79,7 +90,7 @@ export class Vector implements Point { sub(x: number, y: number): Vector; sub(v: Vector): Vector; sub(v: Vector | number, y?: number, z?: number) { - if (arguments.length === 1 && typeof v !== 'number') { + if (arguments.length === 1 && typeof v !== "number") { this.x -= v.x; this.y -= v.y; this.z -= v.z; @@ -95,7 +106,7 @@ export class Vector implements Point { return this; } mult(v: number | Vector) { - if (typeof v === 'number') { + if (typeof v === "number") { this.x *= v; this.y *= v; this.z *= v; @@ -107,7 +118,7 @@ export class Vector implements Point { return this; } div(v: number | Vector) { - if (typeof v === 'number') { + if (typeof v === "number") { this.x /= v; this.y /= v; this.z /= v; @@ -135,18 +146,16 @@ export class Vector implements Point { dot(x: number, y: number, z: number): number; dot(v: Vector): number; dot(v: Vector | number, y?: number, z?: number) { - if (arguments.length === 1 && typeof v !== 'number') { - return (this.x * v.x + this.y * v.y + this.z * v.z); + if (arguments.length === 1 && typeof v !== "number") { + return (this.x * v.x) + (this.y * v.y) + (this.z * v.z); } - return (this.x * (v as number) + this.y * y! + this.z * z!); + return (this.x * (v as number)) + (this.y * y!) + (this.z * z!); } cross(v: Vector) { const x = this.x, y = this.y, z = this.z; - return new Vector(y * v.z - v.y * z, - z * v.x - v.z * x, - x * v.y - v.x * y); + return new Vector(y * v.z - v.y * z, z * v.x - v.z * x, x * v.y - v.x * y); } lerp(x: number, y: number, z: number): void; lerp(v: Vector, amt: number): void; @@ -155,7 +164,7 @@ export class Vector implements Point { return start + (stop - start) * amt; }; let x, y: number; - if (arguments.length === 2 && typeof v_or_x !== 'number') { + if (arguments.length === 2 && typeof v_or_x !== "number") { // given vector and amt amt = amt_or_y; x = v_or_x.x; @@ -202,10 +211,32 @@ export class Vector implements Point { return new Vector(this.x, this.y, this.z); } - drawDot() { + drawDot(color?: string) { if (!doodler) return; - doodler.dot(this, {weight: 2, color: 'red'}); + doodler.dot(this, { weight: 2, color: color || "red" }); + } + + draw() { + if (!doodler) return; + + const startPoint = new Vector(); + doodler.dot(new Vector(), { weight: 4, color: "orange" }); + doodler.line( + startPoint, + startPoint.copy().add(this.copy().normalize().mult(700)), + ); + doodler.line( + startPoint, + startPoint.copy().sub(this.copy().normalize().mult(700)), + ); + } + + normal(v: Vector) { + const dx = v.x - this.x; + const dy = v.y - this.y; + + return new Vector(-dy, dx); } static fromAngle(angle: number, v?: Vector) { @@ -261,9 +292,9 @@ export class Vector implements Point { static lerp(v1: Vector, v2: Vector, amt: number) { // non-static lerp mutates object, but this version returns a new vector - const retval = new Vector(v1.x, v1.y, v1.z); - retval.lerp(v2, amt); - return retval; + const val = new Vector(v1.x, v1.y, v1.z); + val.lerp(v2, amt); + return val; } static vectorProjection(v1: Vector, v2: Vector) { @@ -273,9 +304,16 @@ export class Vector implements Point { v2.mult(sp); return v2; } + static vectorProjectionAndDot(v1: Vector, v2: Vector): [Vector, number] { + v2 = v2.copy(); + v2.normalize(); + const sp = v1.dot(v2); + v2.mult(sp); + return [v2, sp]; + } static hypot2(a: Vector, b: Vector) { - return Vector.dot(Vector.sub(a, b), Vector.sub(a, b)) + return Vector.dot(Vector.sub(a, b), Vector.sub(a, b)); } } @@ -284,9 +322,9 @@ export class OriginVector extends Vector { get halfwayPoint() { return { - x: (this.mag()/2 * Math.sin(this.heading())) + this.origin.x, - y: (this.mag()/2 * Math.cos(this.heading())) + this.origin.y - } + x: (this.mag() / 2 * Math.sin(this.heading())) + this.origin.x, + y: (this.mag() / 2 * Math.cos(this.heading())) + this.origin.y, + }; } constructor(origin: Point, p: Point) { diff --git a/main.ts b/main.ts index cbdefcd..400be28 100644 --- a/main.ts +++ b/main.ts @@ -1,59 +1,106 @@ /// +import { axisAlignedCollision } from "./collision/aa.ts"; +import { circularCollision } from "./collision/circular.ts"; +import { satCollision } from "./collision/sat.ts"; +import { Polygon } from "./geometry/polygon.ts"; import { initializeDoodler, Vector } from "./mod.ts"; -import { ZoomableDoodler } from "./zoomableCanvas.ts"; +// import { ZoomableDoodler } from "./zoomableCanvas.ts"; initializeDoodler( { - width: 400, - height: 400, + width: 2400, + height: 1200, + bg: "#333", }, true, (ctx) => { ctx.imageSmoothingEnabled = false; + // ctx.translate(1200, 600); }, ); -const movingVector = new Vector(100, 300); -let angleMultiplier = 0; -const v = new Vector(30, 30); -doodler.registerDraggable(v, 20); +// const movingVector = new Vector(100, 300); +// let angleMultiplier = 0; +// const v = new Vector(30, 30); +// doodler.registerDraggable(v, 20); const img = new Image(); img.src = "./skeleton.png"; img.hidden; document.body.append(img); -const p = new Vector(200, 200); +const p = new Vector(500, 500); -doodler.createLayer(() => { +const poly = new Polygon([ + { x: -50, y: -25 }, + { x: 25, y: -50 }, + { x: 25, y: 25 }, + { x: -25, y: 25 }, +]); +// poly.center = p.copy(); + +const poly2 = Polygon.createPolygon(5, 75); +poly2.center = p.copy(); + +poly.center.add(p); + +doodler.createLayer((c) => { + for (let i = 0; i < c.canvas.width; i += 50) { + for (let j = 0; j < c.canvas.height; j += 50) { + doodler.drawSquare(new Vector(i, j), 50, { color: "#00000010" }); + } + } + const color = axisAlignedCollision( + poly.aaHitbox(), + poly2.aaHitbox(), + ) + ? "red" + : "black"; + + // console.log(satCollision( + // )); + + poly.draw(color); + + poly2.draw(color); + + // poly2.center.add(Vector.random2D()); const [gamepad] = navigator.getGamepads(); - const deadzone = 0.04; + const deadzone = 0.05; if (gamepad) { const leftX = gamepad.axes[0]; const leftY = gamepad.axes[1]; - p.add( - Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), - Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), + poly.center.add( + new Vector( + Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), + Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), + ).mult(10), ); - const rigthX = gamepad.axes[2]; - const rigthY = gamepad.axes[3]; - (doodler as ZoomableDoodler).moveOrigin({ x: -rigthX * 5, y: -rigthY * 5 }); + const rightX = gamepad.axes[2]; + const rightY = gamepad.axes[3]; + poly2.center.add( + new Vector( + Math.min(Math.max(rightX - deadzone, 0), rightX + deadzone), + Math.min(Math.max(rightY - deadzone, 0), rightY + deadzone), + ).mult(10), + ); + // (doodler as ZoomableDoodler).moveOrigin({ x: -rigthX * 5, y: -rigthY * 5 }); - if (gamepad.buttons[7].value) { - (doodler as ZoomableDoodler).scaleAt( - { x: 200, y: 200 }, - 1 + (gamepad.buttons[7].value / 5), - ); - } - if (gamepad.buttons[6].value) { - (doodler as ZoomableDoodler).scaleAt( - { x: 200, y: 200 }, - 1 - (gamepad.buttons[6].value / 5), - ); - } + // if (gamepad.buttons[7].value) { + // (doodler as ZoomableDoodler).scaleAt( + // { x: 200, y: 200 }, + // 1 + (gamepad.buttons[7].value / 5), + // ); + // } + // if (gamepad.buttons[6].value) { + // (doodler as ZoomableDoodler).scaleAt( + // { x: 200, y: 200 }, + // 1 - (gamepad.buttons[6].value / 5), + // ); + // } } - doodler.drawImageWithOutline(img, p); + // doodler.drawImageWithOutline(img, p); // doodler.line(new Vector(100, 100), new Vector(200, 200)) // doodler.dot(new Vector(300, 300)) // doodler.fillCircle(movingVector, 6, { color: 'red' }); @@ -88,9 +135,9 @@ doodler.createLayer(() => { // }); }); -document.addEventListener("keyup", (e) => { - e.preventDefault(); - if (e.key === " ") { - doodler.unregisterDraggable(v); - } -}); +// document.addEventListener("keyup", (e) => { +// e.preventDefault(); +// if (e.key === " ") { +// doodler.unregisterDraggable(v); +// } +// }); diff --git a/zoomableCanvas.ts b/zoomableCanvas.ts index 35a01ce..0b88503 100644 --- a/zoomableCanvas.ts +++ b/zoomableCanvas.ts @@ -54,7 +54,7 @@ export class ZoomableDoodler extends Doodler { e.preventDefault(); this.dragging = false; }); - this._canvas.addEventListener("mouseleave", (e) => { + this._canvas.addEventListener("mouseleave", (_e) => { this.dragging = false; }); this._canvas.addEventListener("mousemove", (e) => { From 62b13e49e74229d8344470bce7790ec4d4da0206 Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 3 Nov 2023 05:12:19 -0600 Subject: [PATCH 2/4] changes hitbox getters to get properties, adds aaContains --- bundle.js | 109 ++++++++++++++++++++++++++++++++++++++------ collision/aa.ts | 10 ++++ geometry/polygon.ts | 6 +-- main.ts | 38 ++++++++++----- 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/bundle.js b/bundle.js index 95dbaac..9b401a2 100644 --- a/bundle.js +++ b/bundle.js @@ -2,8 +2,8 @@ // deno-lint-ignore-file // This code was bundled using `deno bundle` and it's not recommended to edit it manually -const axisAlignedCollision = (aa1, aa2)=>{ - return aa1.x < aa2.x + aa2.w && aa1.x + aa1.w > aa2.x && aa1.y < aa2.y + aa2.h && aa1.y + aa1.h > aa2.y; +const axisAlignedContains = (aa1, aa2)=>{ + return aa1.x < aa2.x && aa1.y < aa2.y && aa1.x + aa1.w > aa2.x + aa2.w && aa1.y + aa1.h > aa2.y + aa2.h; }; const Constants = { TWO_PI: Math.PI * 2 @@ -293,6 +293,89 @@ class OriginVector extends Vector { return new OriginVector(origin, v); } } +const satCollision = (s1, s2)=>{ + const shape1 = s1.points.map((p)=>new Vector(p).add(s1.center)); + const shape2 = s2.points.map((p)=>new Vector(p).add(s2.center)); + if (shape1.length < 2 || shape2.length < 2) { + throw "Insufficient shape data in satCollision"; + } + new Vector(s1.center).sub(s2.center); + for(let i = 0; i < shape1.length; i++){ + const axis = shape1[i].normal(shape1.at(i - 1)); + let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); + let [p1max, p1maxDot] = [ + p1min, + p1minDot + ]; + for (const point of shape1){ + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + p1min = dot < p1minDot ? projected : p1min; + p1minDot = Math.min(dot, p1minDot); + p1max = dot > p1maxDot ? projected : p1max; + p1maxDot = Math.max(dot, p1maxDot); + } + let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); + let [p2max, p2maxDot] = [ + p2min, + p2minDot + ]; + for (const point of shape2){ + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + p2min = dot < p2minDot ? projected : p2min; + p2minDot = Math.min(dot, p2minDot); + p2max = dot > p2maxDot ? projected : p2max; + p2maxDot = Math.max(dot, p2maxDot); + } + if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + return false; + } + [ + axis, + p1max, + p1min, + p2max, + p2min + ]; + } + for(let i = 0; i < shape2.length; i++){ + const axis = shape2[i].normal(shape2.at(i - 1)); + let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); + let [p1max, p1maxDot] = [ + p1min, + p1minDot + ]; + for (const point of shape2){ + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + p1min = dot < p1minDot ? projected : p1min; + p1minDot = Math.min(dot, p1minDot); + p1max = dot > p1maxDot ? projected : p1max; + p1maxDot = Math.max(dot, p1maxDot); + } + let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); + let [p2max, p2maxDot] = [ + p2min, + p2minDot + ]; + for (const point of shape1){ + const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); + p2min = dot < p2minDot ? projected : p2min; + p2minDot = Math.min(dot, p2minDot); + p2max = dot > p2maxDot ? projected : p2max; + p2maxDot = Math.max(dot, p2maxDot); + } + if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + return false; + } + [ + axis, + p1max, + p1min, + p2max, + p2min + ]; + } + return true; +}; const easeInOut = (x)=>x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; const map = (value, x1, y1, x2, y2)=>(value - x1) * (y2 - x2) / (y1 - x1) + x2; class Doodler { @@ -879,10 +962,6 @@ class Polygon { doodler.line(p1.copy().add(this.center), p2.copy().add(this.center), { color }); - const { x, y, w, h } = this.aaHitbox(); - doodler.drawRect(new Vector(x, y), w, h, { - color: "lime" - }); } } calcCenter() { @@ -894,7 +973,7 @@ class Polygon { center.div(this.points.length); return center; } - circularHitbox() { + get circularHitbox() { let greatestDistance = 0; for (const p of this.points){ greatestDistance = Math.max(p.copy().add(this.center).dist(this.center), greatestDistance); @@ -904,7 +983,7 @@ class Polygon { radius: greatestDistance }; } - aaHitbox() { + get aaHitbox() { let smallestX, biggestX, smallestY, biggestY; smallestX = smallestY = Infinity; biggestX = biggestY = -Infinity; @@ -953,12 +1032,12 @@ document.body.append(img); const p = new Vector(500, 500); const poly = new Polygon([ { - x: -50, + x: -25, y: -25 }, { x: 25, - y: -50 + y: -25 }, { x: 25, @@ -970,7 +1049,7 @@ const poly = new Polygon([ } ]); const poly2 = Polygon.createPolygon(5, 75); -poly2.center = p.copy(); +poly2.center = p.copy().add(100, 100); poly.center.add(p); doodler.createLayer((c)=>{ for(let i = 0; i < c.canvas.width; i += 50){ @@ -980,16 +1059,20 @@ doodler.createLayer((c)=>{ }); } } - const color = axisAlignedCollision(poly.aaHitbox(), poly2.aaHitbox()) ? "red" : "black"; + const color = satCollision(poly, poly2) ? "red" : "aqua"; poly.draw(color); poly2.draw(color); const [gamepad] = navigator.getGamepads(); if (gamepad) { const leftX = gamepad.axes[0]; const leftY = gamepad.axes[1]; - poly.center.add(new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)).mult(10)); const rightX = gamepad.axes[2]; const rightY = gamepad.axes[3]; + if (axisAlignedContains(poly2.aaHitbox, poly.aaHitbox)) { + poly.center.add(new Vector(Math.min(Math.max(rightX - 0.05, 0), rightX + 0.05), Math.min(Math.max(rightY - 0.05, 0), rightY + 0.05)).mult(10)); + poly2.center.add(new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)).mult(10)); + } + poly.center.add(new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)).mult(10)); poly2.center.add(new Vector(Math.min(Math.max(rightX - 0.05, 0), rightX + 0.05), Math.min(Math.max(rightY - 0.05, 0), rightY + 0.05)).mult(10)); } }); diff --git a/collision/aa.ts b/collision/aa.ts index 7387e69..b34f1ca 100644 --- a/collision/aa.ts +++ b/collision/aa.ts @@ -14,3 +14,13 @@ export const axisAlignedCollision = ( aa1.y < aa2.y + aa2.h && aa1.y + aa1.h > aa2.y; }; + +export const axisAlignedContains = ( + aa1: axisAlignedBoundingBox, + aa2: axisAlignedBoundingBox, +) => { + return aa1.x < aa2.x && + aa1.y < aa2.y && + aa1.x + aa1.w > aa2.x + aa2.w && + aa1.y + aa1.h > aa2.y + aa2.h; +}; diff --git a/geometry/polygon.ts b/geometry/polygon.ts index d2927a1..0d9c031 100644 --- a/geometry/polygon.ts +++ b/geometry/polygon.ts @@ -19,8 +19,6 @@ export class Polygon { doodler.line(p1.copy().add(this.center), p2.copy().add(this.center), { color, }); - const { x, y, w, h } = this.aaHitbox(); - doodler.drawRect(new Vector(x, y), w, h, { color: "lime" }); } } @@ -35,7 +33,7 @@ export class Polygon { return center; } - circularHitbox(): CircleLike { + get circularHitbox(): CircleLike { let greatestDistance = 0; for (const p of this.points) { greatestDistance = Math.max( @@ -50,7 +48,7 @@ export class Polygon { }; } - aaHitbox(): axisAlignedBoundingBox { + get aaHitbox(): axisAlignedBoundingBox { let smallestX, biggestX, smallestY, biggestY; smallestX = smallestY = diff --git a/main.ts b/main.ts index 400be28..90ac638 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,6 @@ /// -import { axisAlignedCollision } from "./collision/aa.ts"; +import { axisAlignedCollision, axisAlignedContains } from "./collision/aa.ts"; import { circularCollision } from "./collision/circular.ts"; import { satCollision } from "./collision/sat.ts"; import { Polygon } from "./geometry/polygon.ts"; @@ -32,15 +32,15 @@ document.body.append(img); const p = new Vector(500, 500); const poly = new Polygon([ - { x: -50, y: -25 }, - { x: 25, y: -50 }, + { x: -25, y: -25 }, + { x: 25, y: -25 }, { x: 25, y: 25 }, { x: -25, y: 25 }, ]); // poly.center = p.copy(); const poly2 = Polygon.createPolygon(5, 75); -poly2.center = p.copy(); +poly2.center = p.copy().add(100, 100); poly.center.add(p); @@ -50,12 +50,12 @@ doodler.createLayer((c) => { doodler.drawSquare(new Vector(i, j), 50, { color: "#00000010" }); } } - const color = axisAlignedCollision( - poly.aaHitbox(), - poly2.aaHitbox(), + const color = satCollision( + poly, + poly2, ) ? "red" - : "black"; + : "aqua"; // console.log(satCollision( // )); @@ -70,21 +70,37 @@ doodler.createLayer((c) => { if (gamepad) { const leftX = gamepad.axes[0]; const leftY = gamepad.axes[1]; + const rightX = gamepad.axes[2]; + const rightY = gamepad.axes[3]; + + if (axisAlignedContains(poly2.aaHitbox, poly.aaHitbox)) { + poly.center.add( + new Vector( + Math.min(Math.max(rightX - deadzone, 0), rightX + deadzone), + Math.min(Math.max(rightY - deadzone, 0), rightY + deadzone), + ).mult(10), + ); + poly2.center.add( + new Vector( + Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), + Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), + ).mult(10), + ); + } + poly.center.add( new Vector( Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), ).mult(10), ); - - const rightX = gamepad.axes[2]; - const rightY = gamepad.axes[3]; poly2.center.add( new Vector( Math.min(Math.max(rightX - deadzone, 0), rightX + deadzone), Math.min(Math.max(rightY - deadzone, 0), rightY + deadzone), ).mult(10), ); + // (doodler as ZoomableDoodler).moveOrigin({ x: -rigthX * 5, y: -rigthY * 5 }); // if (gamepad.buttons[7].value) { From 9d8a0fc7d25fa626715dd0100da389fb1a9926cd Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 3 Nov 2023 05:46:50 -0600 Subject: [PATCH 3/4] sat circle collision --- bundle.js | 127 +++++++++++++++---------------------------- collision/sat.ts | 132 ++++++++++++++++++++++++--------------------- geometry/vector.ts | 9 ++-- main.ts | 41 +++++++------- 4 files changed, 141 insertions(+), 168 deletions(-) diff --git a/bundle.js b/bundle.js index 9b401a2..cbecb5c 100644 --- a/bundle.js +++ b/bundle.js @@ -2,9 +2,6 @@ // deno-lint-ignore-file // This code was bundled using `deno bundle` and it's not recommended to edit it manually -const axisAlignedContains = (aa1, aa2)=>{ - return aa1.x < aa2.x && aa1.y < aa2.y && aa1.x + aa1.w > aa2.x + aa2.w && aa1.y + aa1.h > aa2.y + aa2.h; -}; const Constants = { TWO_PI: Math.PI * 2 }; @@ -74,7 +71,7 @@ class Vector { if (arguments.length === 1 && typeof v !== "number") { this.x -= v.x; this.y -= v.y; - this.z -= v.z; + this.z -= v.z || 0; } else if (arguments.length === 2) { this.x -= v; this.y -= y ?? 0; @@ -118,7 +115,7 @@ class Vector { return this; } dist(v) { - const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z; + const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - (v.z || 0); return Math.sqrt(dx * dx + dy * dy + dz * dz); } dot(v, y, z) { @@ -293,86 +290,45 @@ class OriginVector extends Vector { return new OriginVector(origin, v); } } -const satCollision = (s1, s2)=>{ - const shape1 = s1.points.map((p)=>new Vector(p).add(s1.center)); - const shape2 = s2.points.map((p)=>new Vector(p).add(s2.center)); - if (shape1.length < 2 || shape2.length < 2) { - throw "Insufficient shape data in satCollision"; +const satCollisionCircle = (s, c)=>{ + const shape = s.points.map((p)=>new Vector(p).add(s.center)); + if (shape.length < 2) { + throw "Insufficient shape data in satCollisionCircle"; } - new Vector(s1.center).sub(s2.center); - for(let i = 0; i < shape1.length; i++){ - const axis = shape1[i].normal(shape1.at(i - 1)); - let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); - let [p1max, p1maxDot] = [ - p1min, - p1minDot - ]; - for (const point of shape1){ - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - p1min = dot < p1minDot ? projected : p1min; + for(let i = 0; i < shape.length; i++){ + const axis = shape[i].normal(shape.at(i - 1)); + let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); + let p1maxDot = p1minDot; + for (const point of shape){ + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); p1minDot = Math.min(dot, p1minDot); - p1max = dot > p1maxDot ? projected : p1max; p1maxDot = Math.max(dot, p1maxDot); } - let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); - let [p2max, p2maxDot] = [ - p2min, - p2minDot - ]; - for (const point of shape2){ - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - p2min = dot < p2minDot ? projected : p2min; - p2minDot = Math.min(dot, p2minDot); - p2max = dot > p2maxDot ? projected : p2max; - p2maxDot = Math.max(dot, p2maxDot); - } + const [__, circleDot] = Vector.vectorProjectionAndDot(new Vector(c.center), axis); + const p2minDot = circleDot - c.radius; + const p2maxDot = circleDot + c.radius; if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { return false; } - [ - axis, - p1max, - p1min, - p2max, - p2min - ]; } - for(let i = 0; i < shape2.length; i++){ - const axis = shape2[i].normal(shape2.at(i - 1)); - let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); - let [p1max, p1maxDot] = [ - p1min, - p1minDot - ]; - for (const point of shape2){ - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - p1min = dot < p1minDot ? projected : p1min; - p1minDot = Math.min(dot, p1minDot); - p1max = dot > p1maxDot ? projected : p1max; - p1maxDot = Math.max(dot, p1maxDot); - } - let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); - let [p2max, p2maxDot] = [ - p2min, - p2minDot - ]; - for (const point of shape1){ - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - p2min = dot < p2minDot ? projected : p2min; - p2minDot = Math.min(dot, p2minDot); - p2max = dot > p2maxDot ? projected : p2max; - p2maxDot = Math.max(dot, p2maxDot); - } - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - return false; - } - [ - axis, - p1max, - p1min, - p2max, - p2min - ]; + const center = new Vector(c.center); + let nearest = shape[0]; + for (const p of shape){ + if (center.dist(p) < center.dist(nearest)) nearest = p; + } + const axis = center.sub(nearest); + let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); + let p1maxDot = p1minDot; + for (const point of shape){ + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); + p1minDot = Math.min(dot, p1minDot); + p1maxDot = Math.max(dot, p1maxDot); + } + const [__, circleDot] = Vector.vectorProjectionAndDot(new Vector(c.center), axis); + const p2minDot = circleDot - c.radius; + const p2maxDot = circleDot + c.radius; + if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + return false; } return true; }; @@ -1048,7 +1004,16 @@ const poly = new Polygon([ y: 25 } ]); -const poly2 = Polygon.createPolygon(5, 75); +const poly2 = new Polygon([ + { + x: -250, + y: -25 + }, + { + x: 25, + y: 250 + } +]); poly2.center = p.copy().add(100, 100); poly.center.add(p); doodler.createLayer((c)=>{ @@ -1059,7 +1024,7 @@ doodler.createLayer((c)=>{ }); } } - const color = satCollision(poly, poly2) ? "red" : "aqua"; + const color = satCollisionCircle(poly2, poly.circularHitbox) ? "red" : "aqua"; poly.draw(color); poly2.draw(color); const [gamepad] = navigator.getGamepads(); @@ -1068,10 +1033,6 @@ doodler.createLayer((c)=>{ const leftY = gamepad.axes[1]; const rightX = gamepad.axes[2]; const rightY = gamepad.axes[3]; - if (axisAlignedContains(poly2.aaHitbox, poly.aaHitbox)) { - poly.center.add(new Vector(Math.min(Math.max(rightX - 0.05, 0), rightX + 0.05), Math.min(Math.max(rightY - 0.05, 0), rightY + 0.05)).mult(10)); - poly2.center.add(new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)).mult(10)); - } poly.center.add(new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)).mult(10)); poly2.center.add(new Vector(Math.min(Math.max(rightX - 0.05, 0), rightX + 0.05), Math.min(Math.max(rightY - 0.05, 0), rightY + 0.05)).mult(10)); } diff --git a/collision/sat.ts b/collision/sat.ts index 84383d1..0df3ffd 100644 --- a/collision/sat.ts +++ b/collision/sat.ts @@ -1,5 +1,6 @@ import { Polygon } from "../geometry/polygon.ts"; import { Point, Vector } from "../geometry/vector.ts"; +import { CircleLike } from "./circular.ts"; export const satCollision = (s1: Polygon, s2: Polygon) => { const shape1 = s1.points.map((p) => new Vector(p).add(s1.center)); @@ -8,98 +9,105 @@ export const satCollision = (s1: Polygon, s2: Polygon) => { if (shape1.length < 2 || shape2.length < 2) { throw "Insufficient shape data in satCollision"; } - const offset = new Vector(s1.center).sub(s2.center); - // Take one side of the polygon and find the normal - let last: Vector[] = []; for (let i = 0; i < shape1.length; i++) { const axis = shape1[i].normal(shape1.at(i - 1)!); - // axis.draw(); - let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); - let [p1max, p1maxDot] = [p1min, p1minDot]; + let [_, p1minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); + let p1maxDot = p1minDot; for (const point of shape1) { - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - // projected.drawDot(); - p1min = dot < p1minDot ? projected : p1min; + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); p1minDot = Math.min(dot, p1minDot); - p1max = dot > p1maxDot ? projected : p1max; p1maxDot = Math.max(dot, p1maxDot); } - let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); - let [p2max, p2maxDot] = [p2min, p2minDot]; + let [__, p2minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); + let p2maxDot = p2minDot; for (const point of shape2) { - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - // projected.drawDot(); - p2min = dot < p2minDot ? projected : p2min; + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); p2minDot = Math.min(dot, p2minDot); - p2max = dot > p2maxDot ? projected : p2max; p2maxDot = Math.max(dot, p2maxDot); } - // const scale = axis.dot(offset); - // p1max += scale; - // p1min += scale; - - // let p2min = axis.dot(shape1[0]); - // let p2max = p2min; - // for (const point of shape2) { - // const dot = point.dot(axis); - // p2max = Math.max(p2max, dot); - // p2min = Math.min(p2min, dot); - // Vector.vectorProjection(point, axis).drawDot(); - // } - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - // axis.draw(); - // p1max.drawDot("blue"); - // p1min.drawDot("lime"); - // p2max.drawDot("blue"); - // p2min.drawDot("lime"); return false; } - last = [axis, p1max, p1min, p2max, p2min]; } for (let i = 0; i < shape2.length; i++) { const axis = shape2[i].normal(shape2.at(i - 1)!); - // axis.draw(); - let [p1min, p1minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); - let [p1max, p1maxDot] = [p1min, p1minDot]; + let [_, p1minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); + let p1maxDot = p1minDot; for (const point of shape2) { - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - // projected.drawDot(); - p1min = dot < p1minDot ? projected : p1min; + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); + // p1min = dot < p1minDot ? projected : p1min; p1minDot = Math.min(dot, p1minDot); - p1max = dot > p1maxDot ? projected : p1max; + // p1max = dot > p1maxDot ? projected : p1max; p1maxDot = Math.max(dot, p1maxDot); } - let [p2min, p2minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); - let [p2max, p2maxDot] = [p2min, p2minDot]; + let [__, p2minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); + let p2maxDot = p2minDot; for (const point of shape1) { - const [projected, dot] = Vector.vectorProjectionAndDot(point, axis); - // projected.drawDot(); - p2min = dot < p2minDot ? projected : p2min; + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); + // p2min = dot < p2minDot ? projected : p2min; p2minDot = Math.min(dot, p2minDot); - p2max = dot > p2maxDot ? projected : p2max; + // p2max = dot > p2maxDot ? projected : p2max; p2maxDot = Math.max(dot, p2maxDot); } if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - // axis.draw(); - // p1max.drawDot("blue"); - // p1min.drawDot("lime"); - // p2max.drawDot("blue"); - // p2min.drawDot("lime"); return false; } - last = [axis, p1max, p1min, p2max, p2min]; } - // for (const [i, l] of last.entries()) { - // if (i === 0) { - // l.draw(); - // } else { - // l.drawDot(); - // } - // } - + return true; +}; + +export const satCollisionCircle = (s: Polygon, c: CircleLike) => { + const shape = s.points.map((p) => new Vector(p).add(s.center)); + + if (shape.length < 2) { + throw "Insufficient shape data in satCollisionCircle"; + } + for (let i = 0; i < shape.length; i++) { + const axis = shape[i].normal(shape.at(i - 1)!); + let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); + let p1maxDot = p1minDot; + for (const point of shape) { + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); + p1minDot = Math.min(dot, p1minDot); + p1maxDot = Math.max(dot, p1maxDot); + } + const [__, circleDot] = Vector.vectorProjectionAndDot( + new Vector(c.center), + axis, + ); + const p2minDot = circleDot - c.radius; + const p2maxDot = circleDot + c.radius; + + if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + return false; + } + } + const center = new Vector(c.center); + let nearest = shape[0]; + for (const p of shape) { + if (center.dist(p) < center.dist(nearest)) nearest = p; + } + const axis = center.sub(nearest); + let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); + let p1maxDot = p1minDot; + for (const point of shape) { + const [_, dot] = Vector.vectorProjectionAndDot(point, axis); + p1minDot = Math.min(dot, p1minDot); + p1maxDot = Math.max(dot, p1maxDot); + } + const [__, circleDot] = Vector.vectorProjectionAndDot( + new Vector(c.center), + axis, + ); + const p2minDot = circleDot - c.radius; + const p2maxDot = circleDot + c.radius; + + if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + return false; + } + return true; }; diff --git a/geometry/vector.ts b/geometry/vector.ts index fbf0287..a04988b 100644 --- a/geometry/vector.ts +++ b/geometry/vector.ts @@ -89,11 +89,12 @@ export class Vector implements Point { sub(x: number, y: number, z: number): Vector; sub(x: number, y: number): Vector; sub(v: Vector): Vector; - sub(v: Vector | number, y?: number, z?: number) { + sub(v: Point): Vector; + sub(v: Vector | Point | number, y?: number, z?: number) { if (arguments.length === 1 && typeof v !== "number") { this.x -= v.x; this.y -= v.y; - this.z -= v.z; + this.z -= v.z || 0; } else if (arguments.length === 2) { // 2D Vector this.x -= v as number; @@ -137,10 +138,10 @@ export class Vector implements Point { this.y = s * prev_x + c * this.y; return this; } - dist(v: Vector) { + dist(v: Vector | Point) { const dx = this.x - v.x, dy = this.y - v.y, - dz = this.z - v.z; + dz = this.z - (v.z || 0); return Math.sqrt(dx * dx + dy * dy + dz * dz); } dot(x: number, y: number, z: number): number; diff --git a/main.ts b/main.ts index 90ac638..ccddfe2 100644 --- a/main.ts +++ b/main.ts @@ -2,7 +2,7 @@ import { axisAlignedCollision, axisAlignedContains } from "./collision/aa.ts"; import { circularCollision } from "./collision/circular.ts"; -import { satCollision } from "./collision/sat.ts"; +import { satCollision, satCollisionCircle } from "./collision/sat.ts"; import { Polygon } from "./geometry/polygon.ts"; import { initializeDoodler, Vector } from "./mod.ts"; // import { ZoomableDoodler } from "./zoomableCanvas.ts"; @@ -39,9 +39,12 @@ const poly = new Polygon([ ]); // poly.center = p.copy(); -const poly2 = Polygon.createPolygon(5, 75); -poly2.center = p.copy().add(100, 100); +const poly2 = new Polygon([ + { x: -250, y: -25 }, + { x: 25, y: 250 }, +]); +poly2.center = p.copy().add(100, 100); poly.center.add(p); doodler.createLayer((c) => { @@ -50,9 +53,9 @@ doodler.createLayer((c) => { doodler.drawSquare(new Vector(i, j), 50, { color: "#00000010" }); } } - const color = satCollision( - poly, + const color = satCollisionCircle( poly2, + poly.circularHitbox, ) ? "red" : "aqua"; @@ -73,20 +76,20 @@ doodler.createLayer((c) => { const rightX = gamepad.axes[2]; const rightY = gamepad.axes[3]; - if (axisAlignedContains(poly2.aaHitbox, poly.aaHitbox)) { - poly.center.add( - new Vector( - Math.min(Math.max(rightX - deadzone, 0), rightX + deadzone), - Math.min(Math.max(rightY - deadzone, 0), rightY + deadzone), - ).mult(10), - ); - poly2.center.add( - new Vector( - Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), - Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), - ).mult(10), - ); - } + // if (axisAlignedContains(poly2.aaHitbox, poly.aaHitbox)) { + // poly.center.add( + // new Vector( + // Math.min(Math.max(rightX - deadzone, 0), rightX + deadzone), + // Math.min(Math.max(rightY - deadzone, 0), rightY + deadzone), + // ).mult(10), + // ); + // poly2.center.add( + // new Vector( + // Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), + // Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), + // ).mult(10), + // ); + // } poly.center.add( new Vector( From c6c4b46312ff6acdb5893eb585f8f0818832a9af Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 3 Nov 2023 22:28:44 -0600 Subject: [PATCH 4/4] refactors SAT to be less wet, adds spline and spline collision using SAT --- .vscode/settings.json | 1 + bundle.js | 344 +++++++++++++++++++++++++++++++++--------- collision/sat.ts | 212 ++++++++++++++------------ geometry/polygon.ts | 30 +++- geometry/spline.ts | 246 ++++++++++++++++++++++++++++++ geometry/vector.ts | 16 +- main.ts | 74 +++++---- 7 files changed, 712 insertions(+), 211 deletions(-) create mode 100644 geometry/spline.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index be43d98..f8598ab 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,6 +23,7 @@ "deno.unstable": true, "liveServer.settings.port": 5501, "cSpell.words": [ + "aabb", "deadzone" ] } \ No newline at end of file diff --git a/bundle.js b/bundle.js index cbecb5c..1066e25 100644 --- a/bundle.js +++ b/bundle.js @@ -187,17 +187,13 @@ class Vector { color: color || "red" }); } - draw() { + draw(origin) { if (!doodler) return; - const startPoint = new Vector(); - doodler.dot(new Vector(), { - weight: 4, - color: "orange" - }); - doodler.line(startPoint, startPoint.copy().add(this.copy().normalize().mult(700))); - doodler.line(startPoint, startPoint.copy().sub(this.copy().normalize().mult(700))); + const startPoint = origin ? new Vector(origin) : new Vector(); + doodler.line(startPoint, startPoint.copy().add(this.copy().normalize().mult(100))); } normal(v) { + if (!v) return new Vector(-this.y, this.x); const dx = v.x - this.x; const dy = v.y - this.y; return new Vector(-dy, dx); @@ -290,48 +286,54 @@ class OriginVector extends Vector { return new OriginVector(origin, v); } } -const satCollisionCircle = (s, c)=>{ - const shape = s.points.map((p)=>new Vector(p).add(s.center)); - if (shape.length < 2) { - throw "Insufficient shape data in satCollisionCircle"; - } - for(let i = 0; i < shape.length; i++){ - const axis = shape[i].normal(shape.at(i - 1)); - let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); - let p1maxDot = p1minDot; - for (const point of shape){ - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - p1minDot = Math.min(dot, p1minDot); - p1maxDot = Math.max(dot, p1maxDot); +function satCollisionSpline(p, spline) { + for(let i = 0; i < 100; i++){ + const t1 = i / 100; + const t2 = (i + 1) / 100; + const segmentStart = spline.getPointAtT(t1); + const segmentEnd = spline.getPointAtT(t2); + if (segmentIntersectsPolygon(p, segmentStart, segmentEnd)) { + return true; } - const [__, circleDot] = Vector.vectorProjectionAndDot(new Vector(c.center), axis); - const p2minDot = circleDot - c.radius; - const p2maxDot = circleDot + c.radius; - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { + } + return false; +} +function segmentIntersectsPolygon(p, start, end) { + const edges = p.getEdges(); + for (const edge of edges){ + const axis = edge.copy().normal().normalize(); + const proj1 = projectPolygonOntoAxis(p, axis); + const proj2 = projectSegmentOntoAxis(start, end, axis); + if (!overlap(proj1, proj2)) { return false; } } - const center = new Vector(c.center); - let nearest = shape[0]; - for (const p of shape){ - if (center.dist(p) < center.dist(nearest)) nearest = p; - } - const axis = center.sub(nearest); - let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); - let p1maxDot = p1minDot; - for (const point of shape){ - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - p1minDot = Math.min(dot, p1minDot); - p1maxDot = Math.max(dot, p1maxDot); - } - const [__, circleDot] = Vector.vectorProjectionAndDot(new Vector(c.center), axis); - const p2minDot = circleDot - c.radius; - const p2maxDot = circleDot + c.radius; - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - return false; - } return true; -}; +} +function projectPolygonOntoAxis(p, axis) { + let min = Infinity; + let max = -Infinity; + for (const point of p.points){ + const dotProduct = point.copy().add(p.center).dot(axis); + min = Math.min(min, dotProduct); + max = Math.max(max, dotProduct); + } + return { + min, + max + }; +} +function projectSegmentOntoAxis(start, end, axis) { + const dotProductStart = start.dot(axis); + const dotProductEnd = end.dot(axis); + return { + min: Math.min(dotProductStart, dotProductEnd), + max: Math.max(dotProductStart, dotProductEnd) + }; +} +function overlap(proj1, proj2) { + return proj1.min <= proj2.max && proj1.max >= proj2.min; +} const easeInOut = (x)=>x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; const map = (value, x1, y1, x2, y2)=>(value - x1) * (y2 - x2) / (y1 - x1) + x2; class Doodler { @@ -939,7 +941,14 @@ class Polygon { radius: greatestDistance }; } - get aaHitbox() { + _aabb; + get AABB() { + if (!this._aabb) { + this._aabb = this.recalculateAABB(); + } + return this._aabb; + } + recalculateAABB() { let smallestX, biggestX, smallestY, biggestY; smallestX = smallestY = Infinity; biggestX = biggestY = -Infinity; @@ -973,6 +982,200 @@ class Polygon { poly.center = poly.calcCenter(); return poly; } + getEdges() { + const edges = []; + for(let i = 0; i < this.points.length; i++){ + const nextIndex = (i + 1) % this.points.length; + const edge = this.points[nextIndex].copy().add(this.center).sub(this.points[i].copy().add(this.center)); + edges.push(edge); + } + return edges; + } + getNearestPoint(p) { + let nearest = this.points[0]; + for (const point of this.points){ + if (p.dist(point) < p.dist(nearest)) nearest = point; + } + return nearest; + } +} +class SplineSegment { + points; + length; + constructor(points){ + this.points = points; + this.length = this.calculateApproxLength(100); + } + draw(color) { + const [a, b, c, d] = this.points; + doodler.drawBezier(a, b, c, d, { + strokeColor: color || "#ffffff50" + }); + } + getPointAtT(t) { + const [a, b, c, d] = this.points; + const res = a.copy(); + res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t)); + res.add(Vector.add(Vector.add(a.copy().mult(3), b.copy().mult(-6)), c.copy().mult(3)).mult(Math.pow(t, 2))); + res.add(Vector.add(Vector.add(a.copy().mult(-1), b.copy().mult(3)), Vector.add(c.copy().mult(-3), d.copy())).mult(Math.pow(t, 3))); + return res; + } + getClosestPoint(v) { + const resolution = 1 / 25; + let closest = this.points[0]; + let closestDistance = this.points[0].dist(v); + let closestT = 0; + for(let i = 0; i < 25; i++){ + const point = this.getPointAtT(i * resolution); + const distance = v.dist(point); + if (distance < closestDistance) { + closest = point; + closestDistance = distance; + closestT = i * resolution; + } + } + return [ + closest, + closestDistance, + closestT + ]; + } + getPointsWithinRadius(v, r) { + const points = []; + const resolution = 1 / 25; + for(let i = 0; i < 25 + 1; i++){ + const point = this.getPointAtT(i * resolution); + const distance = v.dist(point); + if (distance < r) { + points.push([ + i * resolution, + this + ]); + } + } + return points; + } + tangent(t) { + const [a, b, c, d] = this.points; + const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2)); + res.add(Vector.add(Vector.sub(c, b).mult(6 * (1 - t) * t), Vector.sub(d, c).mult(3 * Math.pow(t, 2)))); + return res; + } + doesIntersectCircle(x, y, r) { + const v = new Vector(x, y); + const resolution = 1 / 25; + let distance = Infinity; + let t; + for(let i = 0; i < 25 - 1; i++){ + const a = this.getPointAtT(i * resolution); + const b = this.getPointAtT((i + 1) * resolution); + const ac = Vector.sub(v, a); + const ab = Vector.sub(b, a); + const d = Vector.add(Vector.vectorProjection(ac, ab), a); + const ad = Vector.sub(d, a); + const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y; + let dist; + if (k <= 0.0) { + dist = Vector.hypot2(v, a); + } else if (k >= 1.0) { + dist = Vector.hypot2(v, b); + } + dist = Vector.hypot2(v, d); + if (dist < distance) { + distance = dist; + t = i * resolution; + } + } + if (distance < r) return t; + return false; + } + intersectsCircle(circleCenter, radius) { + for(let i = 0; i < 100; i++){ + const t1 = i / 100; + const t2 = (i + 1) / 100; + const segmentStart = this.getPointAtT(t1); + const segmentEnd = this.getPointAtT(t2); + const segmentLength = Math.sqrt((segmentEnd.x - segmentStart.x) ** 2 + (segmentEnd.y - segmentStart.y) ** 2); + const resolution = Math.max(10, Math.ceil(100 * (segmentLength / radius))); + for(let j = 0; j <= resolution; j++){ + const t = j / resolution; + const point = this.getPointAtT(t); + const distance = Math.sqrt((point.x - circleCenter.x) ** 2 + (point.y - circleCenter.y) ** 2); + if (distance <= radius) { + return true; + } + } + } + return false; + } + calculateApproxLength(resolution = 25) { + const stepSize = 1 / resolution; + const points = []; + for(let i = 0; i <= resolution; i++){ + const current = stepSize * i; + points.push(this.getPointAtT(current)); + } + this.length = points.reduce((acc, cur)=>{ + const prev = acc.prev; + acc.prev = cur; + if (!prev) return acc; + acc.length += cur.dist(prev); + return acc; + }, { + prev: undefined, + length: 0 + }).length; + return this.length; + } + calculateEvenlySpacedPoints(spacing, resolution = 1) { + const points = []; + points.push(this.points[0]); + let prev = points[0]; + let distSinceLastEvenPoint = 0; + let t = 0; + const div = Math.ceil(this.length * resolution * 10); + while(t < 1){ + t += 1 / div; + const point = this.getPointAtT(t); + distSinceLastEvenPoint += prev.dist(point); + if (distSinceLastEvenPoint >= spacing) { + const overshoot = distSinceLastEvenPoint - spacing; + const evenPoint = Vector.add(point, Vector.sub(point, prev).normalize().mult(overshoot)); + distSinceLastEvenPoint = overshoot; + points.push(evenPoint); + prev = evenPoint; + } + prev = point; + } + return points; + } + _aabb; + get AABB() { + if (!this._aabb) { + this._aabb = this.recalculateAABB(); + } + return this._aabb; + } + recalculateAABB() { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for(let i = 0; i < 100; i++){ + const t = i / 100; + const point = this.getPointAtT(t); + minX = Math.min(minX, point.x); + minY = Math.min(minY, point.y); + maxX = Math.max(maxX, point.x); + maxY = Math.max(maxY, point.y); + } + return { + x: minX, + y: minY, + w: maxX - minX, + h: maxY - minY + }; + } } init({ width: 2400, @@ -986,36 +1189,26 @@ img.src = "./skeleton.png"; img.hidden; document.body.append(img); const p = new Vector(500, 500); -const poly = new Polygon([ - { +const spline = new SplineSegment([ + new Vector({ x: -25, y: -25 - }, - { + }).mult(10).add(p), + new Vector({ x: 25, y: -25 - }, - { - x: 25, - y: 25 - }, - { + }).mult(10).add(p), + new Vector({ + x: -25, + y: -25 + }).mult(10).add(p), + new Vector({ x: -25, y: 25 - } -]); -const poly2 = new Polygon([ - { - x: -250, - y: -25 - }, - { - x: 25, - y: 250 - } + }).mult(10).add(p) ]); +const poly2 = Polygon.createPolygon(4); poly2.center = p.copy().add(100, 100); -poly.center.add(p); doodler.createLayer((c)=>{ for(let i = 0; i < c.canvas.width; i += 50){ for(let j = 0; j < c.canvas.height; j += 50){ @@ -1024,16 +1217,19 @@ doodler.createLayer((c)=>{ }); } } - const color = satCollisionCircle(poly2, poly.circularHitbox) ? "red" : "aqua"; - poly.draw(color); + poly2.circularHitbox; + const intersects = satCollisionSpline(poly2, spline); + const color = intersects ? "red" : "aqua"; + spline.draw(color); poly2.draw(color); const [gamepad] = navigator.getGamepads(); if (gamepad) { - const leftX = gamepad.axes[0]; - const leftY = gamepad.axes[1]; + gamepad.axes[0]; + gamepad.axes[1]; const rightX = gamepad.axes[2]; const rightY = gamepad.axes[3]; - poly.center.add(new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)).mult(10)); - poly2.center.add(new Vector(Math.min(Math.max(rightX - 0.05, 0), rightX + 0.05), Math.min(Math.max(rightY - 0.05, 0), rightY + 0.05)).mult(10)); + let mMulti = 10; + const mod = new Vector(Math.min(Math.max(rightX - 0.05, 0), rightX + 0.05), Math.min(Math.max(rightY - 0.05, 0), rightY + 0.05)); + poly2.center.add(mod.mult(mMulti)); } }); diff --git a/collision/sat.ts b/collision/sat.ts index 0df3ffd..7dc2c94 100644 --- a/collision/sat.ts +++ b/collision/sat.ts @@ -1,113 +1,125 @@ import { Polygon } from "../geometry/polygon.ts"; -import { Point, Vector } from "../geometry/vector.ts"; +import { SplineSegment } from "../geometry/spline.ts"; +import { Vector } from "../geometry/vector.ts"; import { CircleLike } from "./circular.ts"; -export const satCollision = (s1: Polygon, s2: Polygon) => { - const shape1 = s1.points.map((p) => new Vector(p).add(s1.center)); - const shape2 = s2.points.map((p) => new Vector(p).add(s2.center)); +export function satCollisionSpline(p: Polygon, spline: SplineSegment): boolean { + const numSegments = 100; // You can adjust the number of segments based on your needs - if (shape1.length < 2 || shape2.length < 2) { - throw "Insufficient shape data in satCollision"; - } - for (let i = 0; i < shape1.length; i++) { - const axis = shape1[i].normal(shape1.at(i - 1)!); - let [_, p1minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); - let p1maxDot = p1minDot; - for (const point of shape1) { - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - p1minDot = Math.min(dot, p1minDot); - p1maxDot = Math.max(dot, p1maxDot); - } - let [__, p2minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); - let p2maxDot = p2minDot; - for (const point of shape2) { - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - p2minDot = Math.min(dot, p2minDot); - p2maxDot = Math.max(dot, p2maxDot); - } + for (let i = 0; i < numSegments; i++) { + const t1 = i / numSegments; + const t2 = (i + 1) / numSegments; - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - return false; - } - } - for (let i = 0; i < shape2.length; i++) { - const axis = shape2[i].normal(shape2.at(i - 1)!); - let [_, p1minDot] = Vector.vectorProjectionAndDot(shape2[0], axis); - let p1maxDot = p1minDot; - for (const point of shape2) { - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - // p1min = dot < p1minDot ? projected : p1min; - p1minDot = Math.min(dot, p1minDot); - // p1max = dot > p1maxDot ? projected : p1max; - p1maxDot = Math.max(dot, p1maxDot); - } - let [__, p2minDot] = Vector.vectorProjectionAndDot(shape1[0], axis); - let p2maxDot = p2minDot; - for (const point of shape1) { - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - // p2min = dot < p2minDot ? projected : p2min; - p2minDot = Math.min(dot, p2minDot); - // p2max = dot > p2maxDot ? projected : p2max; - p2maxDot = Math.max(dot, p2maxDot); - } + const segmentStart = spline.getPointAtT(t1); + const segmentEnd = spline.getPointAtT(t2); - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - return false; + if (segmentIntersectsPolygon(p, segmentStart, segmentEnd)) { + return true; } } + return false; +} + +export function satCollisionPolygon(poly: Polygon, poly2: Polygon): boolean { + for (const edge of poly.getEdges()) { + const axis = edge.copy().normal().normalize(); + const proj1 = projectPolygonOntoAxis(poly, axis); + const proj2 = projectPolygonOntoAxis(poly2, axis); + + if (!overlap(proj1, proj2)) return false; + } + for (const edge of poly2.getEdges()) { + const axis = edge.copy().normal().normalize(); + const proj1 = projectPolygonOntoAxis(poly, axis); + const proj2 = projectPolygonOntoAxis(poly2, axis); + + if (!overlap(proj1, proj2)) return false; + } return true; -}; +} +export function satCollisionCircle(p: Polygon, circle: CircleLike): boolean { + for (const edge of p.getEdges()) { + const axis = edge.copy().normal().normalize(); + const proj1 = projectPolygonOntoAxis(p, axis); + const proj2 = projectCircleOntoAxis(circle, axis); -export const satCollisionCircle = (s: Polygon, c: CircleLike) => { - const shape = s.points.map((p) => new Vector(p).add(s.center)); - - if (shape.length < 2) { - throw "Insufficient shape data in satCollisionCircle"; - } - for (let i = 0; i < shape.length; i++) { - const axis = shape[i].normal(shape.at(i - 1)!); - let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); - let p1maxDot = p1minDot; - for (const point of shape) { - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - p1minDot = Math.min(dot, p1minDot); - p1maxDot = Math.max(dot, p1maxDot); - } - const [__, circleDot] = Vector.vectorProjectionAndDot( - new Vector(c.center), - axis, - ); - const p2minDot = circleDot - c.radius; - const p2maxDot = circleDot + c.radius; - - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - return false; - } - } - const center = new Vector(c.center); - let nearest = shape[0]; - for (const p of shape) { - if (center.dist(p) < center.dist(nearest)) nearest = p; - } - const axis = center.sub(nearest); - let [_, p1minDot] = Vector.vectorProjectionAndDot(shape[0], axis); - let p1maxDot = p1minDot; - for (const point of shape) { - const [_, dot] = Vector.vectorProjectionAndDot(point, axis); - p1minDot = Math.min(dot, p1minDot); - p1maxDot = Math.max(dot, p1maxDot); - } - const [__, circleDot] = Vector.vectorProjectionAndDot( - new Vector(c.center), - axis, - ); - const p2minDot = circleDot - c.radius; - const p2maxDot = circleDot + c.radius; - - if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) { - return false; - } + if (!overlap(proj1, proj2)) return false; + } + const center = new Vector(circle.center); + const nearest = p.getNearestPoint(center); + const axis = nearest.copy().normal(center).normalize(); + const proj1 = projectPolygonOntoAxis(p, axis); + const proj2 = projectCircleOntoAxis(circle, axis); + if (!overlap(proj1, proj2)) return false; return true; -}; +} + +function segmentIntersectsPolygon( + p: Polygon, + start: Vector, + end: Vector, +): boolean { + const edges = p.getEdges(); + + for (const edge of edges) { + // const axis = new Vector(-edge.y, edge.x).normalize(); + const axis = edge.copy().normal().normalize(); + + const proj1 = projectPolygonOntoAxis(p, axis); + const proj2 = projectSegmentOntoAxis(start, end, axis); + + if (!overlap(proj1, proj2)) { + return false; // No overlap, no intersection + } + } + + return true; // Overlapping on all axes, intersection detected +} + +function projectPolygonOntoAxis( + p: Polygon, + axis: Vector, +): { min: number; max: number } { + let min = Infinity; + let max = -Infinity; + + for (const point of p.points) { + const dotProduct = point.copy().add(p.center).dot(axis); + min = Math.min(min, dotProduct); + max = Math.max(max, dotProduct); + } + + return { min, max }; +} + +function projectSegmentOntoAxis( + start: Vector, + end: Vector, + axis: Vector, +): { min: number; max: number } { + const dotProductStart = start.dot(axis); + const dotProductEnd = end.dot(axis); + return { + min: Math.min(dotProductStart, dotProductEnd), + max: Math.max(dotProductStart, dotProductEnd), + }; +} + +function projectCircleOntoAxis( + c: CircleLike, + axis: Vector, +): { min: number; max: number } { + const dot = new Vector(c.center).dot(axis); + const min = dot - c.radius; + const max = dot + c.radius; + return { min, max }; +} + +function overlap( + proj1: { min: number; max: number }, + proj2: { min: number; max: number }, +): boolean { + return proj1.min <= proj2.max && proj1.max >= proj2.min; +} diff --git a/geometry/polygon.ts b/geometry/polygon.ts index 0d9c031..bdb6219 100644 --- a/geometry/polygon.ts +++ b/geometry/polygon.ts @@ -48,7 +48,15 @@ export class Polygon { }; } - get aaHitbox(): axisAlignedBoundingBox { + _aabb?: axisAlignedBoundingBox; + get AABB(): axisAlignedBoundingBox { + if (!this._aabb) { + this._aabb = this.recalculateAABB(); + } + return this._aabb; + } + + recalculateAABB(): axisAlignedBoundingBox { let smallestX, biggestX, smallestY, biggestY; smallestX = smallestY = @@ -92,4 +100,24 @@ export class Polygon { poly.center = poly.calcCenter(); return poly; } + + getEdges(): Vector[] { + const edges: Vector[] = []; + for (let i = 0; i < this.points.length; i++) { + const nextIndex = (i + 1) % this.points.length; + const edge = this.points[nextIndex].copy().add(this.center).sub( + this.points[i].copy().add(this.center), + ); + edges.push(edge); + } + return edges; + } + + getNearestPoint(p: Vector) { + let nearest = this.points[0]; + for (const point of this.points) { + if (p.dist(point) < p.dist(nearest)) nearest = point; + } + return nearest; + } } diff --git a/geometry/spline.ts b/geometry/spline.ts new file mode 100644 index 0000000..9da13fa --- /dev/null +++ b/geometry/spline.ts @@ -0,0 +1,246 @@ +import { axisAlignedBoundingBox } from "../collision/aa.ts"; +import { Point, Vector } from "./vector.ts"; + +export class SplineSegment { + points: [Vector, Vector, Vector, Vector]; + + length: number; + + constructor(points: [Vector, Vector, Vector, Vector]) { + this.points = points; + this.length = this.calculateApproxLength(100); + } + + draw(color?: string) { + const [a, b, c, d] = this.points; + doodler.drawBezier(a, b, c, d, { + strokeColor: color || "#ffffff50", + }); + } + + getPointAtT(t: number) { + const [a, b, c, d] = this.points; + const res = a.copy(); + + res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t)); + res.add( + Vector.add( + Vector.add(a.copy().mult(3), b.copy().mult(-6)), + c.copy().mult(3), + ).mult(Math.pow(t, 2)), + ); + res.add( + Vector.add( + Vector.add(a.copy().mult(-1), b.copy().mult(3)), + Vector.add(c.copy().mult(-3), d.copy()), + ).mult(Math.pow(t, 3)), + ); + + return res; + } + + getClosestPoint(v: Vector): [Vector, number, number] { + const samples = 25; + const resolution = 1 / samples; + let closest = this.points[0]; + let closestDistance = this.points[0].dist(v); + let closestT = 0; + + for (let i = 0; i < samples; i++) { + const point = this.getPointAtT(i * resolution); + const distance = v.dist(point); + if (distance < closestDistance) { + closest = point; + closestDistance = distance; + closestT = i * resolution; + } + } + + return [closest, closestDistance, closestT]; + } + + getPointsWithinRadius(v: Vector, r: number) { + const points: [number, SplineSegment][] = []; + const samples = 25; + const resolution = 1 / samples; + + for (let i = 0; i < samples + 1; i++) { + const point = this.getPointAtT(i * resolution); + const distance = v.dist(point); + if (distance < r) { + points.push([i * resolution, this]); + } + } + return points; + } + + tangent(t: number) { + // dP(t) / dt = -3(1-t)^2 * P0 + 3(1-t)^2 * P1 - 6t(1-t) * P1 - 3t^2 * P2 + 6t(1-t) * P2 + 3t^2 * P3 + const [a, b, c, d] = this.points; + + const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2)); + res.add( + Vector.add( + Vector.sub(c, b).mult(6 * (1 - t) * t), + Vector.sub(d, c).mult(3 * Math.pow(t, 2)), + ), + ); + + return res; + } + + doesIntersectCircle(x: number, y: number, r: number) { + const v = new Vector(x, y); + const samples = 25; + const resolution = 1 / samples; + + let distance = Infinity; + let t; + + for (let i = 0; i < samples - 1; i++) { + const a = this.getPointAtT(i * resolution); + const b = this.getPointAtT((i + 1) * resolution); + const ac = Vector.sub(v, a); + const ab = Vector.sub(b, a); + + const d = Vector.add(Vector.vectorProjection(ac, ab), a); + const ad = Vector.sub(d, a); + + const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y; + + let dist; + if (k <= 0.0) { + dist = Vector.hypot2(v, a); + } else if (k >= 1.0) { + dist = Vector.hypot2(v, b); + } + + dist = Vector.hypot2(v, d); + + if (dist < distance) { + distance = dist; + t = i * resolution; + } + } + + if (distance < r) return t; + + return false; + } + + intersectsCircle(circleCenter: Point, radius: number): boolean { + const numSegments = 100; // Initial number of segments + const minResolution = 10; // Minimum resolution to ensure accuracy + + for (let i = 0; i < numSegments; i++) { + const t1 = i / numSegments; + const t2 = (i + 1) / numSegments; + + const segmentStart = this.getPointAtT(t1); + const segmentEnd = this.getPointAtT(t2); + + const segmentLength = Math.sqrt( + (segmentEnd.x - segmentStart.x) ** 2 + + (segmentEnd.y - segmentStart.y) ** 2, + ); + + // Dynamically adjust resolution based on segment length + const resolution = Math.max( + minResolution, + Math.ceil(numSegments * (segmentLength / radius)), + ); + + for (let j = 0; j <= resolution; j++) { + const t = j / resolution; + const point = this.getPointAtT(t); + const distance = Math.sqrt( + (point.x - circleCenter.x) ** 2 + (point.y - circleCenter.y) ** 2, + ); + + if (distance <= radius) { + return true; // Intersection detected + } + } + } + + return false; // No intersection found + } + + calculateApproxLength(resolution = 25) { + const stepSize = 1 / resolution; + const points: Vector[] = []; + for (let i = 0; i <= resolution; i++) { + const current = stepSize * i; + points.push(this.getPointAtT(current)); + } + this.length = + points.reduce((acc: { prev?: Vector; length: number }, cur) => { + const prev = acc.prev; + acc.prev = cur; + if (!prev) return acc; + acc.length += cur.dist(prev); + return acc; + }, { prev: undefined, length: 0 }).length; + return this.length; + } + + calculateEvenlySpacedPoints(spacing: number, resolution = 1) { + const points: Vector[] = []; + + points.push(this.points[0]); + let prev = points[0]; + let distSinceLastEvenPoint = 0; + + let t = 0; + + const div = Math.ceil(this.length * resolution * 10); + while (t < 1) { + t += 1 / div; + const point = this.getPointAtT(t); + distSinceLastEvenPoint += prev.dist(point); + + if (distSinceLastEvenPoint >= spacing) { + const overshoot = distSinceLastEvenPoint - spacing; + const evenPoint = Vector.add( + point, + Vector.sub(point, prev).normalize().mult(overshoot), + ); + distSinceLastEvenPoint = overshoot; + points.push(evenPoint); + prev = evenPoint; + } + + prev = point; + } + + return points; + } + + private _aabb?: axisAlignedBoundingBox; + get AABB() { + if (!this._aabb) { + this._aabb = this.recalculateAABB(); + } + return this._aabb; + } + recalculateAABB(): axisAlignedBoundingBox { + const numPoints = 100; // You can adjust the number of points based on your needs + + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + for (let i = 0; i < numPoints; i++) { + const t = i / numPoints; + const point = this.getPointAtT(t); + + minX = Math.min(minX, point.x); + minY = Math.min(minY, point.y); + maxX = Math.max(maxX, point.x); + maxY = Math.max(maxY, point.y); + } + + return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; + } +} diff --git a/geometry/vector.ts b/geometry/vector.ts index a04988b..f88a8d2 100644 --- a/geometry/vector.ts +++ b/geometry/vector.ts @@ -218,22 +218,20 @@ export class Vector implements Point { doodler.dot(this, { weight: 2, color: color || "red" }); } - draw() { + draw(origin?: Point) { if (!doodler) return; - const startPoint = new Vector(); - doodler.dot(new Vector(), { weight: 4, color: "orange" }); + const startPoint = origin ? new Vector(origin) : new Vector(); doodler.line( startPoint, - startPoint.copy().add(this.copy().normalize().mult(700)), - ); - doodler.line( - startPoint, - startPoint.copy().sub(this.copy().normalize().mult(700)), + startPoint.copy().add(this.copy().normalize().mult(100)), ); } - normal(v: Vector) { + normal(): Vector; + normal(v: Vector): Vector; + normal(v?: Vector) { + if (!v) return new Vector(-this.y, this.x); const dx = v.x - this.x; const dy = v.y - this.y; diff --git a/main.ts b/main.ts index ccddfe2..bda097a 100644 --- a/main.ts +++ b/main.ts @@ -2,8 +2,9 @@ import { axisAlignedCollision, axisAlignedContains } from "./collision/aa.ts"; import { circularCollision } from "./collision/circular.ts"; -import { satCollision, satCollisionCircle } from "./collision/sat.ts"; +import { satCollisionSpline } from "./collision/sat.ts"; import { Polygon } from "./geometry/polygon.ts"; +import { SplineSegment } from "./geometry/spline.ts"; import { initializeDoodler, Vector } from "./mod.ts"; // import { ZoomableDoodler } from "./zoomableCanvas.ts"; @@ -31,39 +32,48 @@ document.body.append(img); const p = new Vector(500, 500); -const poly = new Polygon([ - { x: -25, y: -25 }, - { x: 25, y: -25 }, - { x: 25, y: 25 }, - { x: -25, y: 25 }, +const spline = new SplineSegment([ + new Vector({ x: -25, y: -25 }).mult(10).add(p), + new Vector({ x: 25, y: -25 }).mult(10).add(p), + new Vector({ x: -25, y: -25 }).mult(10).add(p), + new Vector({ x: -25, y: 25 }).mult(10).add(p), ]); // poly.center = p.copy(); -const poly2 = new Polygon([ - { x: -250, y: -25 }, - { x: 25, y: 250 }, -]); +const poly2 = Polygon.createPolygon(4); poly2.center = p.copy().add(100, 100); -poly.center.add(p); +// poly.center.add(p); doodler.createLayer((c) => { + // c.translate(1200, 600); for (let i = 0; i < c.canvas.width; i += 50) { for (let j = 0; j < c.canvas.height; j += 50) { doodler.drawSquare(new Vector(i, j), 50, { color: "#00000010" }); } } - const color = satCollisionCircle( - poly2, - poly.circularHitbox, - ) - ? "red" - : "aqua"; + const cir = poly2.circularHitbox; + // const t = spline.getPointsWithinRadius( + // new Vector(cir.center), + // cir.radius, + // ).map((t) => t[0]); + const intersects = satCollisionSpline(poly2, spline); + const color = intersects ? "red" : "aqua"; + + // const point = spline.getPointAtT(t || 0); + // point.drawDot("pink"); // console.log(satCollision( // )); - poly.draw(color); + // for (let i = 0; i < 10; i++) { + // for (const i of t) { + // // const tan = spline.tangent(i / 10); + // const point = spline.getPointAtT(i); + // point.drawDot(); + // } + + spline.draw(color); poly2.draw(color); @@ -91,17 +101,27 @@ doodler.createLayer((c) => { // ); // } - poly.center.add( - new Vector( - Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), - Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), - ).mult(10), + // poly.center.add( + // new Vector( + // Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone), + // Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone), + // ).mult(10), + // ); + let mMulti = 10; + const mod = new Vector( + Math.min(Math.max(rightX - deadzone, 0), rightX + deadzone), + Math.min(Math.max(rightY - deadzone, 0), rightY + deadzone), ); + // let future = new Vector(cir.center).add(mod.copy().mult(mMulti--)); + // while (spline.intersectsCircle(future, cir.radius)) { + // // if (mMulti === 0) { + // // mMulti = 1; + // // break; + // // } + // future = new Vector(cir.center).add(mod.copy().mult(mMulti--)); + // } poly2.center.add( - new Vector( - Math.min(Math.max(rightX - deadzone, 0), rightX + deadzone), - Math.min(Math.max(rightY - deadzone, 0), rightY + deadzone), - ).mult(10), + mod.mult(mMulti), ); // (doodler as ZoomableDoodler).moveOrigin({ x: -rigthX * 5, y: -rigthY * 5 });