refactors SAT to be less wet, adds spline and spline collision using SAT
This commit is contained in:
parent
9d8a0fc7d2
commit
c6c4b46312
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -23,6 +23,7 @@
|
||||
"deno.unstable": true,
|
||||
"liveServer.settings.port": 5501,
|
||||
"cSpell.words": [
|
||||
"aabb",
|
||||
"deadzone"
|
||||
]
|
||||
}
|
338
bundle.js
338
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";
|
||||
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;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
212
collision/sat.ts
212
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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
246
geometry/spline.ts
Normal 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 };
|
||||
}
|
||||
}
|
@ -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
74
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),
|
||||
);
|
||||
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 });
|
||||
|
Loading…
x
Reference in New Issue
Block a user