collissions example
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #000000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
border-radius: 16px;
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="canvas-wrapper">
|
||||
<canvas id="canvas" tabindex="0"></canvas>
|
||||
</div>
|
||||
</body>
|
||||
<script src="collisions.js" type="module"></script>
|
||||
</html>
|
||||
+142
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user