From c6c4b46312ff6acdb5893eb585f8f0818832a9af Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 3 Nov 2023 22:28:44 -0600 Subject: [PATCH] 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 });