commit bf7131f7b2b529ab591235d738e98cf1f90ab864 Author: Anthony Wang Date: Mon Jan 29 02:34:01 2024 -0500 PHYSICS diff --git a/hiragana-a.svg b/hiragana-a.svg new file mode 100644 index 0000000..8086c81 --- /dev/null +++ b/hiragana-a.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/hiragana-ni.svg b/hiragana-ni.svg new file mode 100644 index 0000000..0bddfde --- /dev/null +++ b/hiragana-ni.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/hiragana-small-ya.svg b/hiragana-small-ya.svg new file mode 100644 index 0000000..a994ef5 --- /dev/null +++ b/hiragana-small-ya.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..822899d --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + dumb physics engine + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..00140c1 --- /dev/null +++ b/script.js @@ -0,0 +1,210 @@ +// Inject SVG into DOM synchronously because we can't access the DOM of SVGs inside img tags +function injectSVG(svg) { + let req = new XMLHttpRequest() + req.open("GET", svg, false) + req.send() + document.body.innerHTML += req.responseText +} + +// Draw 200x200 SVGs in Inkscape with the pencil tool and a stroke width of 20 +// Make sure the scale is set to 1 and that the id is set to a unique value +// Also, change the stroke to currentColor so we can style the color with CSS +injectSVG("hiragana-ni.svg") +injectSVG("hiragana-small-ya.svg") +injectSVG("hiragana-a.svg") + +let rad = 10 +let size = 200 +let A = [] +let cnt = 0 +document.querySelectorAll("svg").forEach(function(svg) { + svg.style.left = 1.5 * size * cnt++ + "px" + svg.style.top = "50px" + let a = { + id: svg.id, // Unique ID + p: [], // Collision circles of Array instances creates a new array populated with the results of calling a provided function on every element in the calling array. Try it Syntax js map(callbackFn) map(callbackFn, thisArg) Parameters callbackFn A function to execute for each element in the array. + + cm: svg.createSVGPoint(), // Center of mass + vx: Math.random(), // x velocity + vy: Math.random(), // y velocity + th: 0, // Angular position + w: Math.random() / 100 // Angular velocity + } + svg.querySelectorAll("path").forEach(function(path) { + // Get circles on path for collision checking + for (let i = 0; i < path.getTotalLength(); i += rad) { + const p = path.getPointAtLength(i) + 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", rad); + 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 + const rect = svg.getBoundingClientRect() + a.x = rect.x + a.cm.x // Position of center of mass + a.y = rect.y + a.cm.y // Position of center of mass + 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 // Mass + a.mi = 0 // Moment of inertia + for (const p of a.p) a.mi += p.x ** 2 + p.y ** 2 + 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 + 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('hi') +} + +// 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) < size) || (d == 1 && Math.abs(a.y - k) < size)) { + 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}) + } + } +} + +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++) { + let a = A[i] + let b = A[j] + if (ds(a, b) < size * size) { + // Objects are close + let c = {x: 0, y: 0, cnt: 0} + let n = {x: 0, y: 0} + for (const p of a.p.map(x => rot(a, x))) { + // p is close to object b + if (ds(p, b) < size * size) { + for (const q of b.p.map(x => rot(b, x))) { + const d = ds(p, q) + if (d < 4 * rad * rad) { + // Collision! + 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 + let norm = Math.sqrt(n.x ** 2 + n.y ** 2) + n.x /= norm + n.y /= norm + console.log(c.cnt, c, n) + collide(a, b, c, n) + } + } + } + } + + // Render every 10ms + cnt++ + if (cnt == 10) { + cnt = 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 = ds(a, {x: event.clientX, y: event.clientY}) + a.vx += 100 * (a.x - event.clientX) / d + a.vy += 100 * (a.y - event.clientY) / d + } + + // TODO: Display spreading out circles + +} + +cnt = 0 +setInterval(tick, 1) +document.addEventListener("click", updatev) diff --git a/style.css b/style.css new file mode 100644 index 0000000..fe2652d --- /dev/null +++ b/style.css @@ -0,0 +1,45 @@ +body { +/* background-image: url("catgirl.png"); */ + background-size: auto 100vh; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + touch-action: manipulation; +} + +@keyframes color { + 10% { + color: #a43535; + } + 20% { + color: #ff7b00; + } + 30% { + color: #ffff51; + } + 40% { + color: #a7ff4e; + } + 50% { + color: #7addfe; + } + 60% { + color: #6b6bfd; + } + 70% { + color: #ca61ff; + } + 80% { + color: #ff54af; + } + 90% { + color: #f3bbff; + } +} + +svg { + color: #f3bbff; + font-size: 240px; + animation: color 10s infinite linear; + position: absolute; +}