From da77aa10bb168c5a4f9bb0fa97013010d902452b Mon Sep 17 00:00:00 2001 From: Emma Date: Sun, 5 Nov 2023 01:41:21 -0600 Subject: [PATCH] Fixes circular SAT --- bundle.js | 152 +++++++++++++++++++++++--------------------- canvas.ts | 27 ++++++-- collision/sat.ts | 15 ++++- geometry/polygon.ts | 21 +++--- index.html | 2 +- main.ts | 51 +++++++++++---- zoomableCanvas.ts | 10 ++- 7 files changed, 174 insertions(+), 104 deletions(-) diff --git a/bundle.js b/bundle.js index 4489c01..ab9d6db 100644 --- a/bundle.js +++ b/bundle.js @@ -707,7 +707,7 @@ class Doodler { document.body.append(canvas); } this.bg = bg || "white"; - this.framerate = framerate || 60; + this.framerate = framerate; canvas.width = fillScreen ? document.body.clientWidth : width; canvas.height = fillScreen ? document.body.clientHeight : height; if (fillScreen) { @@ -735,10 +735,17 @@ class Doodler { lastFrameAt = 0; startDrawLoop() { this.lastFrameAt = Date.now(); - this.timer = setInterval(()=>this.draw(), 1000 / this.framerate); + if (this.framerate) { + this.timer = setInterval(()=>this.draw(Date.now()), 1000 / this.framerate); + } else { + const cb = (t)=>{ + this.draw(t); + requestAnimationFrame(cb); + }; + requestAnimationFrame(cb); + } } - draw() { - const time = Date.now(); + draw(time) { const frameTime = time - this.lastFrameAt; this.ctx.clearRect(0, 0, this.width, this.height); this.ctx.fillStyle = this.bg; @@ -1033,6 +1040,7 @@ class ZoomableDoodler extends Doodler { y: 0 }; maxScale = 4; + minScale = 1; constructor(options, postInit){ super(options, postInit); this._canvas.addEventListener("wheel", (e)=>{ @@ -1180,7 +1188,7 @@ class ZoomableDoodler extends Doodler { }, scaleBy); } scaleAt(p, scaleBy) { - this.scale = Math.min(Math.max(this.scale * scaleBy, 1), this.maxScale); + this.scale = Math.min(Math.max(this.scale * scaleBy, this.minScale), this.maxScale); this.origin.x = p.x - (p.x - this.origin.x) * scaleBy; this.origin.y = p.y - (p.y - this.origin.y) * scaleBy; this.constrainOrigin(); @@ -1205,12 +1213,12 @@ class ZoomableDoodler extends Doodler { this.origin.x = Math.min(Math.max(this.origin.x, -this._canvas.width * this.scale + this._canvas.width), 0); this.origin.y = Math.min(Math.max(this.origin.y, -this._canvas.height * this.scale + this._canvas.height), 0); } - draw() { + draw(time) { this.ctx.setTransform(this.scale, 0, 0, this.scale, this.origin.x, this.origin.y); this.animateZoom(); this.ctx.fillStyle = this.bg; this.ctx.fillRect(0, 0, this.width / this.scale, this.height / this.scale); - super.draw(); + super.draw(time); } getTouchOffset(p) { const { x, y } = this._canvas.getBoundingClientRect(); @@ -1269,54 +1277,6 @@ const init = (opt, zoomable, postInit)=>{ window.doodler = zoomable ? new ZoomableDoodler(opt, postInit) : new Doodler(opt, postInit); window.doodler.init(); }; -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; - } - } - 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; - } - } - 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; -} class Polygon { points; center; @@ -1342,7 +1302,12 @@ class Polygon { center.div(this.points.length); return center; } - get circularHitbox() { + _circularBoundingBox; + get circularBoundingBox() { + this._circularBoundingBox = this.calculateCircularBoundingBox(); + return this._circularBoundingBox; + } + calculateCircularBoundingBox() { let greatestDistance = 0; for (const p of this.points){ greatestDistance = Math.max(p.copy().add(this.center).dist(this.center), greatestDistance); @@ -1354,9 +1319,7 @@ class Polygon { } _aabb; get AABB() { - if (!this._aabb) { - this._aabb = this.recalculateAABB(); - } + this._aabb = this.recalculateAABB(); return this._aabb; } recalculateAABB() { @@ -1371,8 +1334,8 @@ class Polygon { biggestY = Math.max(temp.y, biggestY); } return { - x: smallestX, - y: smallestY, + x: smallestX + this.center.x, + y: smallestY + this.center.y, w: biggestX - smallestX, h: biggestY - smallestY }; @@ -1407,9 +1370,49 @@ class Polygon { for (const point of this.points){ if (p.dist(point) < p.dist(nearest)) nearest = point; } - return nearest; + return nearest.copy().add(this.center); } } +function satCollisionCircle(p, circle) { + for (const edge of p.getEdges()){ + const axis = edge.copy().normal().normalize(); + const proj1 = projectPolygonOntoAxis(p, axis); + const proj2 = projectCircleOntoAxis(circle, axis); + if (!overlap(proj1, proj2)) return false; + } + const center = new Vector(circle.center); + const nearest = p.getNearestPoint(center); + const axis = nearest.copy().sub(center).normalize(); + const proj1 = projectPolygonOntoAxis(p, axis); + const proj2 = projectCircleOntoAxis(circle, axis); + if (!overlap(proj1, proj2)) 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 projectCircleOntoAxis(c, axis) { + const dot = new Vector(c.center).dot(axis); + const min = dot - c.radius; + const max = dot + c.radius; + return { + min, + max + }; +} +function overlap(proj1, proj2) { + return proj1.min <= proj2.max && proj1.max >= proj2.min; +} class SplineSegment { points; length; @@ -1594,10 +1597,11 @@ init({ }, true, (ctx)=>{ ctx.imageSmoothingEnabled = false; }); +doodler.minScale = .1; const img = new Image(); img.src = "./pixel fire.gif"; -const p = new Vector(); -const gif = new GIFAnimation("./fire-joypixels.gif", p, .5); +const p = new Vector(500, 500); +new GIFAnimation("./fire-joypixels.gif", p, .5); const spline = new SplineSegment([ new Vector({ x: -25, @@ -1616,10 +1620,11 @@ const spline = new SplineSegment([ y: 25 }).mult(10).add(p) ]); +const poly = Polygon.createPolygon(4); const poly2 = Polygon.createPolygon(4); poly2.center = p.copy().add(100, 100); doodler.createLayer((c, i, t)=>{ - gif.draw(t); + c.translate(500, 500); 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, { @@ -1627,19 +1632,22 @@ doodler.createLayer((c, i, t)=>{ }); } } - poly2.circularHitbox; - const intersects = satCollisionSpline(poly2, spline); + const intersects = satCollisionCircle(poly, poly2.circularBoundingBox); const color = intersects ? "red" : "aqua"; spline.draw(color); + poly.draw(color); poly2.draw(color); const [gamepad] = navigator.getGamepads(); if (gamepad) { - gamepad.axes[0]; - gamepad.axes[1]; + const leftX = gamepad.axes[0]; + const leftY = gamepad.axes[1]; const rightX = gamepad.axes[2]; const rightY = gamepad.axes[3]; - 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)); + let lMulti = 10; + const lMod = new Vector(Math.min(Math.max(leftX - 0.05, 0), leftX + 0.05), Math.min(Math.max(leftY - 0.05, 0), leftY + 0.05)); + poly.center.add(lMod.mult(lMulti)); + let rMulti = 10; + const rMod = 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(rMod.mult(rMulti)); } }); diff --git a/canvas.ts b/canvas.ts index 8205014..d490f26 100644 --- a/canvas.ts +++ b/canvas.ts @@ -49,7 +49,7 @@ export class Doodler { private layers: layer[] = []; protected bg: string; - private framerate: number; + private framerate?: number; get width() { return this.ctx.canvas.width; @@ -77,7 +77,7 @@ export class Doodler { } this.bg = bg || "white"; - this.framerate = framerate || 60; + this.framerate = framerate; canvas.width = fillScreen ? document.body.clientWidth : width; canvas.height = fillScreen ? document.body.clientHeight : height; @@ -113,11 +113,21 @@ export class Doodler { private lastFrameAt = 0; private startDrawLoop() { this.lastFrameAt = Date.now(); - this.timer = setInterval(() => this.draw(), 1000 / this.framerate); + if (this.framerate) { + this.timer = setInterval( + () => this.draw(Date.now()), + 1000 / this.framerate, + ); + } else { + const cb = (t: number) => { + this.draw(t); + requestAnimationFrame(cb); + }; + requestAnimationFrame(cb); + } } - protected draw() { - const time = Date.now(); + protected draw(time: number) { const frameTime = time - this.lastFrameAt; this.ctx.clearRect(0, 0, this.width, this.height); this.ctx.fillStyle = this.bg; @@ -254,6 +264,13 @@ export class Doodler { : this.ctx.drawImage(img, at.x, at.y); } + /** + * @description This method is VERY expensive and should be used sparingly - O(n^2) where n is weight. Beyond that, it doesn't work with transparency correctly since the image is overlaid multiple times in drawing and the resulting transparency is dependent on the weight provided + * + * @param img + * @param at + * @param style + */ drawImageWithOutline(img: HTMLImageElement, at: Vector, style?: IStyle): void; drawImageWithOutline( img: HTMLImageElement, diff --git a/collision/sat.ts b/collision/sat.ts index 7dc2c94..5c16d9b 100644 --- a/collision/sat.ts +++ b/collision/sat.ts @@ -1,6 +1,7 @@ import { Polygon } from "../geometry/polygon.ts"; import { SplineSegment } from "../geometry/spline.ts"; import { Vector } from "../geometry/vector.ts"; +import { axisAlignedBoundingBox } from "./aa.ts"; import { CircleLike } from "./circular.ts"; export function satCollisionSpline(p: Polygon, spline: SplineSegment): boolean { @@ -48,13 +49,25 @@ export function satCollisionCircle(p: Polygon, circle: CircleLike): boolean { } const center = new Vector(circle.center); const nearest = p.getNearestPoint(center); - const axis = nearest.copy().normal(center).normalize(); + const axis = nearest.copy().sub(center).normalize(); const proj1 = projectPolygonOntoAxis(p, axis); const proj2 = projectCircleOntoAxis(circle, axis); if (!overlap(proj1, proj2)) return false; return true; } +export function satCollisionAABBCircle( + aabb: axisAlignedBoundingBox, + circle: CircleLike, +): boolean { + const p = new Polygon([ + { x: aabb.x, y: aabb.y }, + { x: aabb.x + aabb.w, y: aabb.y }, + { x: aabb.x + aabb.w, y: aabb.y + aabb.h }, + { x: aabb.x, y: aabb.y + aabb.h }, + ]); + return satCollisionCircle(p, circle); +} function segmentIntersectsPolygon( p: Polygon, diff --git a/geometry/polygon.ts b/geometry/polygon.ts index bdb6219..a1c138f 100644 --- a/geometry/polygon.ts +++ b/geometry/polygon.ts @@ -33,7 +33,14 @@ export class Polygon { return center; } - get circularHitbox(): CircleLike { + _circularBoundingBox?: CircleLike; + + get circularBoundingBox(): CircleLike { + this._circularBoundingBox = this.calculateCircularBoundingBox(); + return this._circularBoundingBox; + } + + private calculateCircularBoundingBox() { let greatestDistance = 0; for (const p of this.points) { greatestDistance = Math.max( @@ -50,13 +57,11 @@ export class Polygon { _aabb?: axisAlignedBoundingBox; get AABB(): axisAlignedBoundingBox { - if (!this._aabb) { - this._aabb = this.recalculateAABB(); - } + this._aabb = this.recalculateAABB(); return this._aabb; } - recalculateAABB(): axisAlignedBoundingBox { + private recalculateAABB(): axisAlignedBoundingBox { let smallestX, biggestX, smallestY, biggestY; smallestX = smallestY = @@ -74,8 +79,8 @@ export class Polygon { } return { - x: smallestX, - y: smallestY, + x: smallestX + this.center.x, + y: smallestY + this.center.y, w: biggestX - smallestX, h: biggestY - smallestY, }; @@ -118,6 +123,6 @@ export class Polygon { for (const point of this.points) { if (p.dist(point) < p.dist(nearest)) nearest = point; } - return nearest; + return nearest.copy().add(this.center); } } diff --git a/index.html b/index.html index 2e39be8..1f66c28 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ Doodler