summaryrefslogtreecommitdiff
path: root/script.js
diff options
context:
space:
mode:
Diffstat (limited to 'script.js')
-rw-r--r--script.js210
1 files changed, 210 insertions, 0 deletions
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)