refactors SAT to be less wet, adds spline and spline collision using SAT

This commit is contained in:
Emmaline Autumn 2023-11-03 22:28:44 -06:00
parent 9d8a0fc7d2
commit c6c4b46312
7 changed files with 712 additions and 211 deletions

View File

@ -23,6 +23,7 @@
"deno.unstable": true,
"liveServer.settings.port": 5501,
"cSpell.words": [
"aabb",
"deadzone"
]
}

342
bundle.js
View File

@ -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";
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;
}
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;
}
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));
}
});

View File

@ -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);
}
if (p1minDot - p2maxDot > 0 || p2minDot - p1maxDot > 0) {
return false;
}
}
const segmentStart = spline.getPointAtT(t1);
const segmentEnd = spline.getPointAtT(t2);
if (segmentIntersectsPolygon(p, segmentStart, segmentEnd)) {
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;
}
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);
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;
}

View File

@ -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;
}
}

246
geometry/spline.ts Normal file
View File

@ -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 };
}
}

View File

@ -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;

74
main.ts
View File

@ -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),
);
poly2.center.add(
new Vector(
// 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),
).mult(10),
);
// 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(
mod.mult(mMulti),
);
// (doodler as ZoomableDoodler).moveOrigin({ x: -rigthX * 5, y: -rigthY * 5 });