diff --git a/collisions.html b/collisions.html
new file mode 100644
index 0000000..5230615
--- /dev/null
+++ b/collisions.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ Test
+
+
+
+
+
+
+
+
+
diff --git a/collisions.ts b/collisions.ts
new file mode 100644
index 0000000..42db8e2
--- /dev/null
+++ b/collisions.ts
@@ -0,0 +1,142 @@
+import { fillCircle, resizeCanvas } from "./common.js";
+import Vec2d from "./vec.js";
+
+interface Ball {
+ pos: Vec2d;
+ vel: Vec2d;
+}
+
+const gravity = new Vec2d(0, 1000); // px/s^2
+let lastTime: number | undefined = undefined;
+
+const box = [
+ new Vec2d(300, 17),
+ new Vec2d(582, 300),
+ new Vec2d(300, 582),
+ new Vec2d(17, 300),
+];
+
+const balls: Ball[] = [];
+
+function update(ctx: CanvasRenderingContext2D, timestamp: number) {
+ if (lastTime == null) lastTime = timestamp;
+ const dt = (timestamp - lastTime) / 1000; // s
+ lastTime = timestamp;
+
+ const [a, b, c, d] = box;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+ // update
+ for (let i = balls.length - 1; i >= 0; i--) {
+ let ball = balls[i];
+
+ const steps = 10;
+ const stepDt = dt / steps;
+ for (let step = 0; step < steps; step++) { // subdive the time step to improve accuracy
+ balls[i].pos = ball.pos.add(ball.vel.scale(stepDt)); // update pos
+ if (balls[i].pos.y > ctx.canvas.height) {
+ balls.splice(i, 1); // remove if out of bounds
+ continue;
+ }
+ ball.vel = ball.vel.add(gravity.scale(stepDt)); // apply gravity
+
+ // check collisions with each segment of the box
+ const friction = 0.5;
+ const restitution = 0.8;
+ ball = resolve_collision(ball, a, b, { friction, restitution});
+ ball = resolve_collision(ball, b, c, { friction, restitution});
+ ball = resolve_collision(ball, c, d, { friction, restitution});
+ ball = resolve_collision(ball, d, a, { friction, restitution});
+ }
+
+ balls[i] = ball;
+ }
+
+ // drawing
+ for (const p of balls) {
+ fillCircle(ctx, p.pos, 5, 0xff00ff);
+ }
+
+ // draw box
+ ctx.beginPath();
+ ctx.moveTo(box[0].x, box[0].y);
+ ctx.lineTo(box[1].x, box[1].y);
+ ctx.lineTo(box[2].x, box[2].y);
+ ctx.lineTo(box[3].x, box[3].y);
+ ctx.closePath();
+ ctx.strokeStyle = "#00ff00";
+ ctx.stroke();
+
+ window.requestAnimationFrame((t) => update(ctx, t));
+}
+
+function resolve_collision(ball: Ball, a: Vec2d, b: Vec2d, { restitution = 1, friction = 0 }: { restitution?: number, friction?: number } = {}) {
+ const ab = b.sub(a);
+ const ab_sqlen = ab.dot(ab);
+ const point = ball.pos.sub(a); // pos of the ball relative to a
+
+ // t reprents the position of the ball along the segment (0 = a, 1 = b)
+ const t = clamp(point.dot(ab) / ab_sqlen, 0, 1); // normalize dot product, then clamp if outside of the segment
+ const closest_point = a.add(ab.scale(t));
+ const offset = ball.pos.sub(closest_point); // vector from the point to the segment
+
+ if (offset.length() < 5) {
+ console.log("collide")
+ const normal = offset.normalize();
+ // Decompose velocity into normal and tangent components
+ const norm_speed = ball.vel.dot(normal);
+ const norm_velocity = normal.scale(norm_speed);
+ const tangeant_velocity = ball.vel.sub(norm_velocity);
+ // Only modify velocity if moving into the segment
+ if (norm_speed < 0) {
+ const new_normal_velocity = normal.scale(-norm_speed * restitution);
+ const new_tangeant_velocity = tangeant_velocity.scale(1 - friction);
+ ball.vel = new_normal_velocity.add(new_tangeant_velocity);
+ return ball;
+ }
+ }
+
+ return ball;
+}
+
+function clamp(n: number, min: number, max: number) {
+ return Math.max(Math.min(n, max), min)
+}
+
+function addBall(x: number, y: number) {
+ balls.push({
+ pos: Vec2d.fromPoint({ x, y }),
+ vel: new Vec2d(0, 0) // initial velocity is 0, but the ball will be accelerated by gravity
+ });
+}
+
+window.onload = () => {
+ const canvas = document.getElementById("canvas") as HTMLCanvasElement;
+ if (canvas == null) throw new Error("Canvas not found")
+ const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
+ if (ctx == null) throw new Error("Canvas context not found")
+ resizeCanvas(ctx);
+ canvas.onclick = (evt: MouseEvent) => {
+ const rect = canvas.getBoundingClientRect();
+ const x = evt.clientX - rect.left;
+ const y = evt.clientY - rect.top;
+ addBall(x, y);
+ };
+ canvas.ontouchstart = (evt: TouchEvent) => {
+ const rect = canvas.getBoundingClientRect();
+ console.assert(evt.touches.length == 1, "mutiple touches are not supported");
+ const x = evt.touches[0].clientX - rect.left;
+ const y = evt.touches[0].clientY - rect.top;
+ addBall(x, y);
+ };
+
+ window.requestAnimationFrame((t) => update(ctx, t));
+};
+
+window.onresize = () => {
+ const canvas = document.getElementById("canvas") as HTMLCanvasElement;
+ if (canvas == null) throw new Error("Canvas not found")
+ const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
+ if (ctx == null) throw new Error("Canvas context not found")
+ resizeCanvas(ctx);
+};