// Inject SVG into DOM synchronously because we can't access the DOM of SVGs inside img tags document.querySelectorAll("svg").forEach(function(svg) { let req = new XMLHttpRequest() req.open("GET", svg.id + ".svg", false) req.send() svg.outerHTML = req.responseText }) let rad = 10 // Stroke radius let A = [] // Objects let idcnt = 0 let ix = 0 let iy = 0 let ny = 0 document.querySelectorAll("svg").forEach(function(svg) { // Position objects so they aren't overlapping if (ix + svg.width.baseVal.value > window.innerWidth) { ix = 0 iy += ny ny = 0 } svg.style.left = ix + "px" svg.style.top = iy + "px" ix += svg.width.baseVal.value ny = Math.max(svg.height.baseVal.value, ny) svg.id = idcnt++ const rect = svg.getBoundingClientRect() let a = { id: svg.id, // Unique ID p: [], // Collision circles cm: svg.createSVGPoint(), // Position of center of mass relative to top left corner x: rect.x, // x position of center of mass y: rect.y, // y position of center of mass vx: Math.random(), // x velocity vy: Math.random(), // y velocity th: 0, // Angular position w: Math.random() / 100, // Angular velocity m: 0, // Mass mi: 0, // Moment of inertia r: 0 // Distance to farthest point from center of mass } svg.querySelectorAll("path").forEach(function(path) { // Get circles on path for collision checking let num = Math.floor(path.getTotalLength() / 5) for (let i = 0; i <= num; i++) { const p = path.getPointAtLength(i / num * path.getTotalLength()) a.cm.x += p.x a.cm.y += p.y a.p.push(p) // Show circles for debugging /* let circle = document.createElementNS("http://www.w3.org/2000/svg", "circle") circle.setAttribute("cx", p.x) circle.setAttribute("cy", p.y) circle.setAttribute("r", 10) circle.setAttribute("fill", "red") svg.appendChild(circle) */ } }) a.cm.x /= a.p.length a.cm.y /= a.p.length // Change origin to center of mass a.x += a.cm.x a.y += a.cm.y for (const p of a.p) { p.x -= a.cm.x p.y -= a.cm.y } svg.style.transformOrigin = a.cm.x + "px " + a.cm.y + "px" a.m = a.p.length for (const p of a.p) a.mi += p.x ** 2 + p.y ** 2 for (const p of a.p) a.r = Math.max(Math.sqrt(p.x ** 2 + p.y ** 2), a.r) A.push(a) }) // Actual position of p in object a function rot(a, p) { const c = Math.cos(a.th) const s = Math.sin(a.th) return {x: a.x + p.x * c - p.y * s, y: a.y + p.x * s + p.y * c} } // Distance squared between a and b function ds(a, b) { return (a.x - b.x) ** 2 + (a.y - b.y) ** 2 } // Cross product function cr(a, b) { return a.x * b.y - a.y * b.x } // Collision of object a with b at point c with normal n function collide(a, b, c, n) { // https://physics.stackexchange.com/questions/783524/angular-motion-in-collisions/783565#783565 // https://physics.stackexchange.com/questions/786641/collision-calculation-in-2d/786969#786969 // https://physics.stackexchange.com/questions/686640/resolving-angular-components-in-2d-circular-rigid-body-collision-response // I still don't know how to derive this magic but I'm convinced it works // No idea if there's a sign error // It looks fine though const ca = {x: a.x - c.x, y: a.y - c.y} const cb = {x: b.x - c.x, y: b.y - c.y} const v = n.x * (a.vx - b.vx) + n.y * (a.vy - b.vy) - a.w * cr(ca, n) + b.w * cr(cb, n) const m = 1 / (1 / a.m + 1 / b.m + cr(ca, n) ** 2 / a.mi + cr(cb, n) ** 2 / b.mi) const j = 2 * m * v a.vx += -n.x * j / a.m a.vy += -n.y * j / a.m a.w += cr(ca, n) * j / a.mi b.vx += n.x * j / b.m b.vy += n.y * j / b.m b.w += -cr(cb, n) * j / b.mi console.log('boop') } // Collision of object a with wall at position k and direction d function wallCollide(a, k, d) { if ((d == 0 && Math.abs(a.x - k) < a.r) || (d == 1 && Math.abs(a.y - k) < a.r)) { let c = {x: 0, y: 0, cnt: 0} for (const p of a.p.map(x => rot(a, x))) { if ((d == 0 && Math.abs(p.x - k) < rad) || (d == 1 && Math.abs(p.y - k) < rad)) { c.x += p.x c.y += p.y c.cnt++ } } if (c.cnt > 0) { c.x /= c.cnt c.y /= c.cnt let b = c b.vx = b.vy = b.w = 0 b.m = b.mi = 1e9 collide(a, b, c, {x: 1 - d, y: d}) } } } // Collision of object a with object b function objectsCollide(a, b) { if (ds(a, b) < (a.r + b.r + 2 * rad) ** 2) { // Objects are close let c = {x: 0, y: 0, cnt: 0} let n = {x: 0, y: 0} // Slight performance optimization? // Only consider points close to other object let aa = [] let bb = [] for (const p of a.p.map(x => rot(a, x))) { if (ds(p, b) < (a.r + b.r + 2 * rad) ** 2) aa.push(p) } for (const p of b.p.map(x => rot(b, x))) { if (ds(p, a) < (a.r + b.r + 2 * rad) ** 2) bb.push(p) } for (const p of aa) { for (const q of bb) { const d = ds(p, q) if (d < (2 * rad) ** 2) { // Collision! // These calculations are a bit sketchy but I guess they work? c.x += p.x + q.x c.y += p.y + q.y c.cnt++ n.x += (p.x - q.x) / d n.y += (p.y - q.y) / d } } } if (c.cnt > 0) { c.x /= 2 * c.cnt c.y /= 2 * c.cnt // Normalize n let norm = Math.sqrt(n.x ** 2 + n.y ** 2) n.x /= norm n.y /= norm collide(a, b, c, n) } } } // Move stuff, check collisions, and render function tick() { // Move each object one step for (let a of A) { a.x += a.vx a.y += a.vy a.th += a.w if (Math.abs(a.vx) > 0.001) a.vx -= 0.001 * Math.sign(a.vx) if (Math.abs(a.vy) > 0.001) a.vy -= 0.001 * Math.sign(a.vy) if (Math.abs(a.w) > 0.00001) a.w -= 0.00001 * Math.sign(a.w) } // Check wall collisions for (let a of A) { wallCollide(a, 0, 0) wallCollide(a, window.innerWidth, 0) wallCollide(a, 0, 1) wallCollide(a, window.innerHeight, 1) } // Check collisions between objects for (let i = 0; i < A.length; i++) { for (let j = i + 1; j < A.length; j++) { objectsCollide(A[i], A[j]) } } // Render every 10ms tickcnt++ if (tickcnt == 10) { tickcnt = 0 for (a of A) { let e = document.getElementById(a.id) e.style.left = a.x - a.cm.x + "px" e.style.top = a.y - a.cm.y + "px" e.style.rotate = a.th + "rad" } } } // Use click to update velocities function updatev(event) { for (a of A) { let d = Math.max(ds(a, {x: event.clientX, y: event.clientY}), 100) a.vx += 100 * (a.x - event.clientX) / d a.vy += 100 * (a.y - event.clientY) / d } // Display spreading out circles let circle = document.createElement("div") circle.style.width = circle.style.height = "10px" circle.style.left = event.clientX - 5 + "px" circle.style.top = event.clientY - 5 + "px" document.body.appendChild(circle) circle.offsetWidth circle.style.transform = "scale(500)" circle.style.opacity = "0" setTimeout(function () { document.body.removeChild(circle) }, 1000) } let tickcnt = 0 setInterval(tick, 1) document.addEventListener("click", updatev)