import { fillCircle, resizeCanvas } from "./common.js"; import Vec2d from "./vec.js"; interface Ball { pos: Vec2d; vel: Vec2d; } const GRAVITY = new Vec2d(0, 1000); const BALL_RADIUS = 25; 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 outer: 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++) { // subdivide 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 outer; } 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 = resolveBallSegmentCollision(ball, [a, b], { friction, restitution}); ball = resolveBallSegmentCollision(ball, [b, c], { friction, restitution}); ball = resolveBallSegmentCollision(ball, [c, d], { friction, restitution}); ball = resolveBallSegmentCollision(ball, [d, a], { friction, restitution}); } balls[i] = ball; } // drawing for (const p of balls) { fillCircle(ctx, p.pos, BALL_RADIUS, 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 resolveBallSegmentCollision(ball: Ball, segment: [Vec2d, Vec2d], { restitution = 1, friction = 0 }: { restitution?: number, friction?: number } = {}) { const [a, b] = segment; 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() < BALL_RADIUS) { 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); };