diff --git a/projects/Physics/BallCollisionOptimization/index.html b/projects/Physics/BallCollisionOptimization/index.html index da77ba8..fb5d671 100644 --- a/projects/Physics/BallCollisionOptimization/index.html +++ b/projects/Physics/BallCollisionOptimization/index.html @@ -78,6 +78,22 @@

Pros and Cons

simulate fluids and particle interactions.

+

+ See the Pen Untitled by WaffleCake (@WaffleCake-the-decoder) + on CodePen. +

+ Click to open in new tab + +

Recursive Spatial Partitioning

The term "recursive spatial partitioning" can be attributed to a number of different types of spatial partitioning. Some of these include binary partitioning, octal and quad trees, and an diff --git a/projects/Physics/massBall/index.html b/projects/Physics/BallCollisionOptimization/massBall/index.html similarity index 90% rename from projects/Physics/massBall/index.html rename to projects/Physics/BallCollisionOptimization/massBall/index.html index 5fd2a1a..e12a5f9 100644 --- a/projects/Physics/massBall/index.html +++ b/projects/Physics/BallCollisionOptimization/massBall/index.html @@ -18,6 +18,7 @@ height: 100%; width: 100%; } + body { display: flex; background-color: black; @@ -31,13 +32,6 @@ color: white; } - a { - position: absolute; - bottom: 15px; - left: 15px; - font-size: x-large; - } - #container { display: flex; justify-content: center; @@ -48,21 +42,21 @@ } form { - width: 10rem; padding: 1rem; } #source { - width: 50%; - /* margin-left: auto; */ - /* margin-right: auto; */ + aspect-ratio: 1; + height: 100%; + box-sizing: border-box; align-self: center; - background-color: white; border: 3px solid white; flex-shrink: 1; } options { + color: white; + max-width: 30rem; position: absolute; top: 1rem; left: 1rem; @@ -70,16 +64,14 @@ #instructions { display: none; - color: white; padding: 1rem; margin: 0; - width: 10rem; + max-width: 50%; } #settings { display: none; - color: white; } @@ -87,6 +79,7 @@ +

FPS:

@@ -104,12 +97,13 @@



+ diff --git a/projects/Physics/BallCollisionOptimization/massBall/script.js b/projects/Physics/BallCollisionOptimization/massBall/script.js new file mode 100644 index 0000000..374a12b --- /dev/null +++ b/projects/Physics/BallCollisionOptimization/massBall/script.js @@ -0,0 +1,411 @@ +class Ball { + constructor(x = ctx.canvas.width / 2, y = ctx.canvas.width / 2, dx = 0, dy = 0, color = "rgb(255, 0, 0)") { + this.x = x; + this.y = y; + this.dx = dx; + this.dy = dy; + this.color = color; + this.radius = ballRadius; + } + + render(ctx) { + ctx.moveTo(this.x, this.y); + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc(this.x, this.y, ballRadius, 0, 2 * Math.PI); + ctx.fill(); + } + + testBounds() { + if (this.x + this.radius > canvas.width || this.x - this.radius < 0.0) { + if (this.x < this.radius) { + this.x = this.radius; + } else { + this.x = canvas.width - this.radius; + } + this.dx = -this.dx * 0.9; + } + if (this.y + this.radius > canvas.height || this.y - this.radius < 0.0) { + if (this.y - this.radius < 0) { + this.y = this.radius; + } else { + this.y = canvas.height - this.radius; + } + this.dy = -this.dy * 0.9; + } + } +} + +function renderGrid() { + for (const [y, row] of grid.entries()) { + for (const [x, cell] of row.entries()) { + if (cell.length > 0) { + ctx.fillStyle = "rgb(255,255,255)"; + } else { + ctx.fillStyle = "rgb(0, " + String((255 * y) / gridResolution - 1) + ", " + String((255 * x) / gridResolution - 1) + ")"; + } + ctx.fillRect((x - 1) * ballRadius * 2, (y - 1) * ballRadius * 2, ballRadius * 2, ballRadius * 2); + } + } +} + +function createGrid() { + for (let x = 0; x < gridResolution + 2; x++) { + grid.push([]); + for (let y = 0; y < gridResolution + 2; y++) { + grid[x].push([]); + } + } +} + +function updateGrid() { + for (const [y, row] of grid.entries()) { + for (const [x, cell] of row.entries()) { + row[x] = []; + } + } + + for (ball of balls) { + let gridx = Math.floor(ball.x / (ballRadius * 2)) + 1; + let gridy = Math.floor(ball.y / (ballRadius * 2)) + 1; + let cell = grid[gridy][gridx]; + cell.push(ball); + } +} + +function collide(ball1, ball2) { + let distance = Math.sqrt((ball1.x - ball2.x) ** 2 + (ball1.y - ball2.y) ** 2); + return ball1.radius + ball2.radius > distance; +} + +function resolveCollision(ball1, ball2) { + let distance = Math.sqrt((ball1.x - ball2.x) ** 2 + (ball1.y - ball2.y) ** 2); + let normX = (ball2.x - ball1.x) / distance; + let normY = (ball2.y - ball1.y) / distance; + + let overlap = ball1.radius + ball2.radius - distance; + + ball1.x -= (overlap / 2) * normX; + ball1.y -= (overlap / 2) * normY; + + ball2.x += (overlap / 2) * normX; + ball2.y += (overlap / 2) * normY; + + let relVelx = ball2.dx - ball1.dx; + let relVely = ball2.dy - ball1.dy; + + let velNorm = normX * relVelx + normY * relVely; + if (velNorm > 0) return; + + var j = -(1 + restitution) * velNorm; + let inverseMassSum = 1 / ball1.radius + 1 / ball2.radius; + j = j / inverseMassSum; + + ball1.dx -= (normX * j) / ball1.radius; + ball1.dy -= (normY * j) / ball1.radius; + ball2.dx += (normX * j) / ball2.radius; + ball2.dy += (normY * j) / ball2.radius; +} + +function checkCellCollision(cell1, cell2) { + for (ball1 of cell1) { + for (ball2 of cell2) { + if (ball1 === ball2) { + continue; + } + if (collide(ball1, ball2)) { + resolveCollision(ball1, ball2); + } + } + } +} + +function collideGrid() { + for (let x = 1; x < gridResolution + 1; x++) { + for (let y = 1; y < gridResolution + 1; y++) { + let currentCell = grid[y][x]; + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -1; dy <= 1; dy++) { + let otherCell = grid[y + dy][x + dx]; + checkCellCollision(currentCell, otherCell); + } + } + } + } +} + +function updatePhysics(dt) { + for (ball of balls) { + ball.x = ball.x + ball.dx * dt; + ball.y = ball.y + ball.dy * dt; + } + + collideGrid(); + + if (gridOn) { + renderGrid(); + } + + for (ball of balls) { + ball.testBounds(); + ball.dy += gravity * dt; + ball.dx += whatFactor * dt; + } +} + +function updatePhysicsSubSteps(dt) { + for (let i = 0; i < substeps; i++) { + updatePhysics(dt / substeps); + } + + for (ball of balls) { + ball.render(ctx); + } +} + +const fpsCounter = document.getElementById("simulation-fps"); +const canvas = document.getElementById("source"); +const ctx = canvas.getContext("2d"); + +function clearCanvas() { + ctx.fillStyle = "rgb(0, 0, 0)"; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); +} + +var reseting = false; + +var constantTime = true; +var gravity = 5000; +var whatFactor = 0; +var restitution = 0.5; +var substeps = 8; +var ballRadius = 50; +var gridResolution = 35; +var gridOn = false; + +let balls = []; +let grid = []; + +var time = null; + +function loop(t) { + if (reseting) { + reseting = false; + return; + } + let framedt = Math.max(0, t / 1000 - time); + + fpsCounter.textContent = `FPS: ${Math.round(1 / framedt)}`; + + if (mouse1Down && mouse1InCanvas) { + var randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16); + balls.push(new Ball(mouse1x, mouse1y, Math.random() * 1000 - 500, Math.random() * 1000 - 500, randomColor)); + } + + clearCanvas(); + + let dt; + if (constantTime) { + dt = 1 / 60; + } else { + dt = framedt; + } + + updateGrid(); + updatePhysicsSubSteps(dt); + + time = t / 1000; + + window.requestAnimationFrame(loop); +} + +function cleanup() { + reseting = true; + balls = []; + grid = []; +} + +function startup() { + console.log("Starting"); + + ctx.canvas.width = ballRadius * 2 * gridResolution; + ctx.canvas.height = ballRadius * 2 * gridResolution; + + createGrid(); + + for (let i = 0; i < 100; i++) { + var randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16); + balls.push( + new Ball( + ballRadius + Math.random() * (ctx.canvas.width - ballRadius), + ballRadius + Math.random() * (ctx.canvas.height - ballRadius), + Math.random() * 1000 - 500, + Math.random() * 1000 - 500, + randomColor + ) + ); + } + window.requestAnimationFrame(loop); +} + +const gravityInput = document.getElementById("simulation-gravity"); +gravityInput.addEventListener( + "input", + (event) => { + let inputNum = parseInt(gravityInput.value); + if (isNaN(inputNum)) { + gravity = 0; + } else { + gravity = inputNum; + } + }, + false +); + +const whatFactorInput = document.getElementById("simulation-what-factor"); +whatFactorInput.addEventListener( + "input", + (event) => { + let inputNum = parseInt(whatFactorInput.value); + if (isNaN(inputNum)) { + whatFactor = 0; + } else { + whatFactor = inputNum; + } + }, + false +); + +const restitutionInput = document.getElementById("simulation-restitution"); +restitutionInput.addEventListener( + "input", + (event) => { + let inputNum = parseInt(restitutionInput.value); + if (isNaN(inputNum)) { + restitution = 0; + } else { + restitution = inputNum; + } + }, + false +); + +const simulationSubSteps = document.getElementById("simulation-substeps"); +simulationSubSteps.addEventListener( + "input", + (event) => { + let inputNum = parseInt(simulationSubSteps.value); + if (isNaN(inputNum)) { + substeps = 0; + } else { + substeps = inputNum; + } + }, + false +); + +renderGridButton = document.getElementById("simulation-render-grid"); +renderGridButton.addEventListener( + "click", + (event) => { + gridOn = renderGridButton.checked; + }, + false +); + +constantTimeButton = document.getElementById("simulation-constant-time"); +constantTimeButton.addEventListener( + "click", + (event) => { + constantTime = constantTimeButton.checked; + }, + false +); + +const clearButton = document.getElementById("simulation-clear"); +clearButton.addEventListener( + "click", + (event) => { + console.log("reset clicked"); + balls = []; + }, + false +); + +const resetButton = document.getElementById("simulation-reset"); +resetButton.addEventListener( + "click", + (event) => { + console.log("reset clicked"); + cleanup(); + startup(); + }, + false +); + +var mouse1Down = false; +var mouse1x = 0.0; +var mouse1y = 0.0; +var mouse1InCanvas = false; + +document.addEventListener( + "mouseup", + (event) => { + if (event.buttons == 0) { + mouse1Down = false; + } + }, + false +); + +document.addEventListener( + "mousedown", + (event) => { + if (event.buttons == 1) { + mouse1Down = true; + } + }, + false +); + +document.addEventListener( + "mousemove", + (evt) => { + var rect = canvas.getBoundingClientRect(), // abs. size of element + scaleX = canvas.width / rect.width, // relationship bitmap vs. element for x + scaleY = canvas.height / rect.height; // relationship bitmap vs. element for y + + if (evt.clientX - rect.left < 0 || evt.clientX - rect.left > rect.width || evt.clientY - rect.top < 0 || evt.clientY - rect.top > rect.height) { + mouse1InCanvas = false; + } else { + mouse1InCanvas = true; + } + + (mouse1x = (evt.clientX - rect.left) * scaleX), // scale mouse coordinates after they have + (mouse1y = (evt.clientY - rect.top) * scaleY); // been adjusted to be relative to element + }, + false +); + +document.addEventListener( + "keydown", + (event) => { + var name = event.key; + var code = event.code; + // Alert the key name and key code on keydown + if (code == "Space") { + event.preventDefault(); + console.log("space pressed"); + + for (ball of balls) { + ball.dx = ball.dx + Math.random() * 5000 - 2500; + ball.dy = ball.dy + Math.random() * -5000; + } + } + }, + false +); + +window.addEventListener("load", function () { + startup(); +}); diff --git a/projects/Physics/massBall/script.js b/projects/Physics/massBall/script.js deleted file mode 100644 index ddb31a2..0000000 --- a/projects/Physics/massBall/script.js +++ /dev/null @@ -1,354 +0,0 @@ -class Ball { - constructor(x = ctx.canvas.width / 2, y = ctx.canvas.width / 2, dx = 0, dy = 0, color = "rgb(255, 0, 0)") { - this.x = x - this.y = y - this.dx = dx - this.dy = dy - this.color = color - this.radius = ballRadius - } - - render(ctx) { - ctx.moveTo(this.x, this.y) - ctx.fillStyle = this.color; - ctx.beginPath(); - ctx.arc(this.x, this.y, ballRadius, 0, 2 * Math.PI); - ctx.fill(); - } - - testBounds() { - if (this.x + this.radius > canvas.width || this.x - this.radius < 0.0) { - if (this.x < this.radius) { - this.x = this.radius - } else { - this.x = canvas.width - this.radius - } - this.dx = -this.dx * .9 - } - if (this.y + this.radius > canvas.height || this.y - this.radius < 0.0) { - if (this.y - this.radius < 0) { - this.y = this.radius - } else { - this.y = canvas.height - this.radius - } - this.dy = -this.dy * .9 - } - } -} - -function renderGrid() { - for (const [y, row] of grid.entries()) { - for (const [x, cell] of row.entries()) { - if (cell.length > 0) { - ctx.fillStyle = "rgb(255,255,255)" - } else { - ctx.fillStyle = "rgb(0, " + String(255 * y / gridResolution - 1) + ", " + String(255 * x / gridResolution - 1) + ")" - } - ctx.fillRect((x-1) * ballRadius * 2, (y-1) * ballRadius * 2, ballRadius * 2, ballRadius * 2) - } - } -} - -function createGrid() { - for (let x = 0; x < gridResolution + 2; x++) { - grid.push([]) - for (let y = 0; y < gridResolution + 2; y++) { - grid[x].push([]) - } - } -} - -function updateGrid() { - for (const [y, row] of grid.entries()) { - for (const [x, cell] of row.entries()) { - row[x] = [] - } - } - - for (ball of balls) { - let gridx = Math.floor(ball.x / (ballRadius * 2)) + 1 - let gridy = Math.floor(ball.y / (ballRadius * 2)) + 1 - let cell = grid[gridy][gridx] - cell.push(ball) - } -} - -function collide(ball1, ball2) { - let distance = Math.sqrt((ball1.x - ball2.x) ** 2 + (ball1.y - ball2.y) ** 2) - return (ball1.radius + ball2.radius) > distance -} - -function resolveCollision(ball1, ball2) { - let distance = Math.sqrt((ball1.x - ball2.x) ** 2 + (ball1.y - ball2.y) ** 2) - let normX = (ball2.x - ball1.x) / distance - let normY = (ball2.y - ball1.y) / distance - - let overlap = (ball1.radius + ball2.radius) - distance - - ball1.x -= overlap / 2 * normX - ball1.y -= overlap / 2 * normY - - ball2.x += overlap / 2 * normX - ball2.y += overlap / 2 * normY - - let relVelx = ball2.dx - ball1.dx - let relVely = ball2.dy - ball1.dy - - let velNorm = normX * relVelx + normY * relVely - if (velNorm > 0) - return; - - var j = -(1 + restitution) * velNorm - let inverseMassSum = (1 / ball1.radius + 1 / ball2.radius) - j = j / inverseMassSum - - ball1.dx -= normX * j / ball1.radius - ball1.dy -= normY * j / ball1.radius - ball2.dx += normX * j / ball2.radius - ball2.dy += normY * j / ball2.radius -} - -function checkCellCollision(cell1, cell2) { - for (ball1 of cell1) { - for (ball2 of cell2) { - if (ball1 === ball2) { continue } - if (collide(ball1, ball2)) { - resolveCollision(ball1, ball2) - } - } - } -} - -function collideGrid() { - for (let x = 1; x < gridResolution + 1; x++) { - for (let y = 1; y < gridResolution + 1; y++) { - let currentCell = grid[y][x] - for (let dx = -1; dx <= 1; dx++) { - for (let dy = -1; dy <= 1; dy++) { - let otherCell = grid[y + dy][x + dx] - checkCellCollision(currentCell, otherCell) - } - } - } - } -} - -function updatePhysics(dt) { - for (ball of balls) { - ball.x = ball.x + ball.dx * dt - ball.y = ball.y + ball.dy * dt - } - - collideGrid() - - if (gridOn) { - renderGrid() - } - - for (ball of balls) { - ball.testBounds() - ball.dy += gravity * dt - ball.dx += whatFactor * dt - } -} - -function updatePhysicsSubSteps(dt) { - for (let i = 0; i < substeps; i++) { - updatePhysics(dt / substeps) - } - - for (ball of balls) { - ball.render(ctx) - } -} - -const fpsCounter = document.getElementById("simulation-fps") -const canvas = document.getElementById("source"); -const ctx = canvas.getContext("2d"); - -function clearCanvas() { - ctx.fillStyle = "rgb(0, 0, 0)" - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); -} - -var reseting = false - -var constantTime = true -var gravity = 5000 -var whatFactor = 0 -var restitution = .5 -var substeps = 8 -var ballRadius = 50 -var gridResolution = 35 -var gridOn = false - -let balls = [] -let grid = [] - -var time = null - -function loop(t) { - if (reseting) { - reseting = false - return - } - let framedt = Math.max(0, (t / 1000) - time) - - fpsCounter.textContent = `FPS: ${Math.round(1 / framedt)}` - - if (mouse1Down && mouse1InCanvas) { - var randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16); - balls.push(new Ball( - mouse1x, - mouse1y, - Math.random() * 1000 - 500, - Math.random() * 1000 - 500, - randomColor - ) - ) - } - - clearCanvas() - - let dt - if (constantTime) { - dt = 1 / 60 - } else { - dt = framedt - } - - updateGrid() - updatePhysicsSubSteps(dt) - - time = t / 1000 - - window.requestAnimationFrame(loop) -} - -function cleanup() { - reseting = true - balls = [] - grid = [] -} - -function startup() { - console.log("Starting") - - ctx.canvas.width = ballRadius * 2 * gridResolution - ctx.canvas.height = ballRadius * 2 * gridResolution - - createGrid() - - for (let i = 0; i < 100; i++) { - var randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16); - balls.push(new Ball( - ballRadius + Math.random() * (ctx.canvas.width - ballRadius), - ballRadius + Math.random() * (ctx.canvas.height - ballRadius), - Math.random() * 1000 - 500, - Math.random() * 1000 - 500, - randomColor - ) - ) - } - window.requestAnimationFrame(loop) -} - -const gravityInput = document.getElementById("simulation-gravity") -gravityInput.addEventListener('input', (event) => { - let inputNum = parseInt(gravityInput.value) - if (isNaN(inputNum)) { gravity = 0 } else { gravity = inputNum } -}, false) - -const whatFactorInput = document.getElementById("simulation-what-factor") -whatFactorInput.addEventListener('input', (event) => { - let inputNum = parseInt(whatFactorInput.value) - if (isNaN(inputNum)) { whatFactor = 0 } else { whatFactor = inputNum } -}, false) - -const restitutionInput = document.getElementById("simulation-restitution") -restitutionInput.addEventListener('input', (event) => { - let inputNum = parseInt(restitutionInput.value) - if (isNaN(inputNum)) { restitution = 0 } else { restitution = inputNum } -}, false) - -const simulationSubSteps = document.getElementById("simulation-substeps") -simulationSubSteps.addEventListener('input', (event) => { - let inputNum = parseInt(simulationSubSteps.value) - if (isNaN(inputNum)) { substeps = 0 } else { substeps = inputNum } -}, false) - -renderGridButton = document.getElementById("simulation-render-grid") -renderGridButton.addEventListener('click', (event) => { - gridOn = renderGridButton.checked -}, false) - -constantTimeButton = document.getElementById("simulation-constant-time") -constantTimeButton.addEventListener('click', (event) => { - constantTime = constantTimeButton.checked -}, false) - -const clearButton = document.getElementById("simulation-clear") -clearButton.addEventListener('click', (event) => { - console.log("reset clicked") - balls = [] -}, false) - -const resetButton = document.getElementById("simulation-reset") -resetButton.addEventListener('click', (event) => { - console.log("reset clicked") - cleanup() - startup() -}, false) - -var mouse1Down = false -var mouse1x = 0.0 -var mouse1y = 0.0 -var mouse1InCanvas = false - -document.addEventListener('mouseup', (event) => { - if (event.buttons == 0) { - mouse1Down = false - } -}, false); - -document.addEventListener('mousedown', (event) => { - if (event.buttons == 1) { - mouse1Down = true - } -}, false); - -document.addEventListener('mousemove', (evt) => { - var rect = canvas.getBoundingClientRect(), // abs. size of element - scaleX = canvas.width / rect.width, // relationship bitmap vs. element for x - scaleY = canvas.height / rect.height; // relationship bitmap vs. element for y - - if (evt.clientX - rect.left < 0 || evt.clientX - rect.left > rect.width || evt.clientY - rect.top < 0 || evt.clientY - rect.top > rect.height) { - mouse1InCanvas = false - } else { - mouse1InCanvas = true - } - - mouse1x = (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have - mouse1y = (evt.clientY - rect.top) * scaleY // been adjusted to be relative to element -}, false); - -document.addEventListener('keydown', (event) => { - var name = event.key; - var code = event.code; - // Alert the key name and key code on keydown - if (code == "Space") { - event.preventDefault() - console.log("space pressed") - - for (ball of balls) { - ball.dx = ball.dx + Math.random() * 5000 - 2500 - ball.dy = ball.dy + Math.random() * -5000 - } - } -}, false); - -window.addEventListener('load', function () { - startup() -}) - - diff --git a/projects/Physics/polygonCollision/index.html b/projects/Physics/polygonCollision/index.html index ff29286..824cb88 100644 --- a/projects/Physics/polygonCollision/index.html +++ b/projects/Physics/polygonCollision/index.html @@ -1,46 +1,59 @@ - - - - + + + - - - - + + + + - - - - Back to Portfolio + p { + margin: 0; + position: absolute; + color: white; + top: 1rem; + left: 1rem; + } + - + +

+ Drag mouse to move boxes
+ Press space to spawn new boxes +

- +
+ +
- \ No newline at end of file + + + diff --git a/projects/Physics/polygonCollision/script.js b/projects/Physics/polygonCollision/script.js index edd72ac..403af9e 100644 --- a/projects/Physics/polygonCollision/script.js +++ b/projects/Physics/polygonCollision/script.js @@ -1,582 +1,521 @@ class Box { - constructor(x = width / 2, y = height / 2, w = 50, h = 50, r = 0, dx = 0, dy = 0, dr = 0) { - this.x = x //box center x - this.y = y //box center y - this.dx = dx - this.dy = dy - - this.r = r % 360 * Math.PI / 180 //rotation in radians - this.dr = dr % 360 * Math.PI / 180 //rotation in radians - - this.w = w //box width - this.h = h //box height - this.m = w * h //mass - - if (this.m <= 0) { - this.invm = 0 - } else { - this.invm = 1 / this.m - } + constructor(x = width / 2, y = height / 2, w = 50, h = 50, r = 0, dx = 0, dy = 0, dr = 0) { + this.x = x; //box center x + this.y = y; //box center y + this.dx = dx; + this.dy = dy; - this.color = "rgb(255,255,255)" - this.e = .5 + this.r = ((r % 360) * Math.PI) / 180; //rotation in radians + this.dr = ((dr % 360) * Math.PI) / 180; //rotation in radians - this.i = 1 / 12 * this.m * (w * w + h * h) - if (this.i <= 0) { - this.invi = 0 - } else { - this.invi = 1 / this.i - } - this.anchored = false - } + this.w = w; //box width + this.h = h; //box height + this.m = w * h; //mass - setRotation(degrees) { - this.r = degrees % 360 * Math.PI / 180 + if (this.m <= 0) { + this.invm = 0; + } else { + this.invm = 1 / this.m; } - setRotationDelta(degrees) { - this.dr = degrees % 360 * Math.PI / 180 - } + this.color = "rgb(255,255,255)"; + this.e = 0.5; - projectPoint(x, y, ax, ay) { - let divResult = (x * ax + y * ay) / (ax * ax + ay + ay) - let cornerProj = { x: divResult * ax, y: divResult * ay } - return ax * cornerProj.x + ay * cornerProj.y + this.i = (1 / 12) * this.m * (w * w + h * h); + if (this.i <= 0) { + this.invi = 0; + } else { + this.invi = 1 / this.i; } - - getClosestPoint(x, y) { - let points = this.getWorldCoordinates() - let closestPoint = points[0] - let dis = Math.sqrt((x - closestPoint.x) ** 2 + (y - closestPoint.y) ** 2) - - for (let i = 1; i < points.length; i++) { - let nextPoint = points[i] - let newDis = Math.sqrt((x - nextPoint.x) ** 2 + (y - nextPoint.y) ** 2) - if (newDis < dis) { - closestPoint = nextPoint - dis = newDis - } - } - return { point: closestPoint, distance: dis } + this.anchored = false; + } + + setRotation(degrees) { + this.r = ((degrees % 360) * Math.PI) / 180; + } + + setRotationDelta(degrees) { + this.dr = ((degrees % 360) * Math.PI) / 180; + } + + projectPoint(x, y, ax, ay) { + let divResult = (x * ax + y * ay) / (ax * ax + ay + ay); + let cornerProj = { x: divResult * ax, y: divResult * ay }; + return ax * cornerProj.x + ay * cornerProj.y; + } + + getClosestPoint(x, y) { + let points = this.getWorldCoordinates(); + let closestPoint = points[0]; + let dis = Math.sqrt((x - closestPoint.x) ** 2 + (y - closestPoint.y) ** 2); + + for (let i = 1; i < points.length; i++) { + let nextPoint = points[i]; + let newDis = Math.sqrt((x - nextPoint.x) ** 2 + (y - nextPoint.y) ** 2); + if (newDis < dis) { + closestPoint = nextPoint; + dis = newDis; + } } - - getCoordinates() { - let hw = this.w / 2 - let hh = this.h / 2 - - let tl = { x: -hw, y: -hh } - let tr = { x: +hw, y: -hh } - let br = { x: +hw, y: +hh } - let bl = { x: -hw, y: +hh } - - return [tl, tr, br, bl] + return { point: closestPoint, distance: dis }; + } + + getCoordinates() { + let hw = this.w / 2; + let hh = this.h / 2; + + let tl = { x: -hw, y: -hh }; + let tr = { x: +hw, y: -hh }; + let br = { x: +hw, y: +hh }; + let bl = { x: -hw, y: +hh }; + + return [tl, tr, br, bl]; + } + + getProjection(lx, ly) { + let points = this.getWorldCoordinates(); + let minPoint = points[0]; + let min = this.projectPoint(points[0].x, points[0].y, lx, ly); + let maxPoint = points[0]; + let max = this.projectPoint(points[0].x, points[0].y, lx, ly); + + for (let i = 1; i < points.length; i++) { + let projection = this.projectPoint(points[i].x, points[i].y, lx, ly); + if (projection < min) { + minPoint = points[i]; + min = projection; + } else if (projection > max) { + maxPoint = points[i]; + max = projection; + } } - getProjection(lx, ly) { - let points = this.getWorldCoordinates() - let minPoint = points[0] - let min = this.projectPoint(points[0].x, points[0].y, lx, ly) - let maxPoint = points[0] - let max = this.projectPoint(points[0].x, points[0].y, lx, ly) - - for (let i = 1; i < points.length; i++) { - let projection = this.projectPoint(points[i].x, points[i].y, lx, ly) - if (projection < min) { - minPoint = points[i] - min = projection - } else if (projection > max) { - maxPoint = points[i] - max = projection - } - } - - return { min: min, max: max, minPoint: minPoint, maxPoint: maxPoint } - } - - testBounds() { - let widthProj = this.getProjection(width, 0) - let heightProj = this.getProjection(0, height) - - if (widthProj.maxPoint.x > width || widthProj.minPoint.x < 0) { - if (widthProj.minPoint.x < 0) { - this.x -= widthProj.minPoint.x - } else { - this.x -= widthProj.maxPoint.x - width - } - this.dx = -this.dx * restitution - } + return { min: min, max: max, minPoint: minPoint, maxPoint: maxPoint }; + } - if (heightProj.maxPoint.y > height || heightProj.minPoint.y < 0) { - if (heightProj.minPoint.y < 0) { - this.y -= heightProj.minPoint.y - } else { - this.y -= heightProj.maxPoint.y - height - } - this.dy = -this.dy * restitution - } - } + testBounds() { + let widthProj = this.getProjection(width, 0); + let heightProj = this.getProjection(0, height); - getCollisionAxis() { - let points = this.getWorldCoordinates() - let axis1 = { x: points[1].x - points[0].x, y: points[1].y - points[0].y } - let axis1Mag = Math.sqrt(axis1.x * axis1.x + axis1.y * axis1.y) - axis1.x = axis1.x / axis1Mag - axis1.y = axis1.y / axis1Mag - - let axis2 = { x: points[2].x - points[1].x, y: points[2].y - points[1].y } - let axis2Mag = Math.sqrt(axis2.x * axis2.x + axis2.y * axis2.y) - axis2.x = axis2.x / axis2Mag - axis2.y = axis2.y / axis2Mag - return [axis1, axis2] - } - - toWorldSpace(x, y) { - return { - x: this.x + x * Math.cos(this.r) - y * Math.sin(this.r), - y: this.y + x * Math.sin(this.r) + y * Math.cos(this.r), - } + if (widthProj.maxPoint.x > width || widthProj.minPoint.x < 0) { + if (widthProj.minPoint.x < 0) { + this.x -= widthProj.minPoint.x; + } else { + this.x -= widthProj.maxPoint.x - width; + } + this.dx = -this.dx * restitution; } - getWorldCoordinates() { - let hw = this.w / 2 - let hh = this.h / 2 - - let tl = this.toWorldSpace(-hw, -hh)// { x: -hw, y: -hh } - let tr = this.toWorldSpace(hw, -hh) - let bl = this.toWorldSpace(-hw, hh) - let br = this.toWorldSpace(hw, hh) - - return [tl, tr, br, bl] + if (heightProj.maxPoint.y > height || heightProj.minPoint.y < 0) { + if (heightProj.minPoint.y < 0) { + this.y -= heightProj.minPoint.y; + } else { + this.y -= heightProj.maxPoint.y - height; + } + this.dy = -this.dy * restitution; } - - // getFaces() { - // let faces = [] - // let coords = this.getWorldCoordinates() - // coords.array.forEach(element, index, array => { - // if (index < array.length - 1) { - // faces.push({element - array[index + 1], 1}) - // } else { - // faces.push() - // } - // }); - // } - - render(ctx) { - ctx.fillStyle = this.color - ctx.fillRect(this.x - 1.5, this.y - 1.5, 3, 3) - let coords = this.getWorldCoordinates() - - ctx.strokeStyle = this.color - ctx.beginPath(); - ctx.moveTo(coords[0].x, coords[0].y) - for (let i = 0; i < coords.length; i++) { - ctx.lineTo(coords[i].x, coords[i].y) - } - ctx.lineTo(coords[0].x, coords[0].y) - ctx.stroke() - - ctx.beginPath() - ctx.strokeStyle = "rgb(0, 255, 255)" - ctx.moveTo(this.x, this.y) - let foward = this.toWorldSpace(this.w / 2, 0) - ctx.lineTo(foward.x, foward.y) - ctx.stroke() - + } + + getCollisionAxis() { + let points = this.getWorldCoordinates(); + let axis1 = { x: points[1].x - points[0].x, y: points[1].y - points[0].y }; + let axis1Mag = Math.sqrt(axis1.x * axis1.x + axis1.y * axis1.y); + axis1.x = axis1.x / axis1Mag; + axis1.y = axis1.y / axis1Mag; + + let axis2 = { x: points[2].x - points[1].x, y: points[2].y - points[1].y }; + let axis2Mag = Math.sqrt(axis2.x * axis2.x + axis2.y * axis2.y); + axis2.x = axis2.x / axis2Mag; + axis2.y = axis2.y / axis2Mag; + return [axis1, axis2]; + } + + toWorldSpace(x, y) { + return { + x: this.x + x * Math.cos(this.r) - y * Math.sin(this.r), + y: this.y + x * Math.sin(this.r) + y * Math.cos(this.r), + }; + } + + getWorldCoordinates() { + let hw = this.w / 2; + let hh = this.h / 2; + + let tl = this.toWorldSpace(-hw, -hh); // { x: -hw, y: -hh } + let tr = this.toWorldSpace(hw, -hh); + let bl = this.toWorldSpace(-hw, hh); + let br = this.toWorldSpace(hw, hh); + + return [tl, tr, br, bl]; + } + + render(ctx) { + ctx.fillStyle = this.color; + ctx.fillRect(this.x - 1.5, this.y - 1.5, 3, 3); + let coords = this.getWorldCoordinates(); + + ctx.strokeStyle = this.color; + ctx.beginPath(); + ctx.moveTo(coords[0].x, coords[0].y); + for (let i = 0; i < coords.length; i++) { + ctx.lineTo(coords[i].x, coords[i].y); } + ctx.lineTo(coords[0].x, coords[0].y); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = "rgb(0, 255, 255)"; + ctx.moveTo(this.x, this.y); + let foward = this.toWorldSpace(this.w / 2, 0); + ctx.lineTo(foward.x, foward.y); + ctx.stroke(); + } } function drawLineTo(x, y, tox, toy, color = "rgb(255,255,255)") { - ctx.beginPath() - ctx.strokeStyle = color - ctx.moveTo(x, y) - ctx.lineTo(tox, toy) - ctx.stroke() + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.moveTo(x, y); + ctx.lineTo(tox, toy); + ctx.stroke(); } function round(value, decimals) { - return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals); + return Number(Math.round(value + "e" + decimals) + "e-" + decimals); } -function getPointsBelow() { - -} +function getPointsBelow() {} function getCollidingPoint(box1, box2, normal) { - let mag = Math.sqrt((box2.x - box1.x) ** 2 + (box2.y - box1.y) ** 2) - let delta = { x: (box2.x - box1.x) / mag, y: (box2.y - box1.y) / mag } - - // drawLineTo(ctx, box1.x, box1.y, box1.x + delta.x / 2 * mag, box1.y + delta.y / 2 * mag) + let mag = Math.sqrt((box2.x - box1.x) ** 2 + (box2.y - box1.y) ** 2); + let delta = { x: (box2.x - box1.x) / mag, y: (box2.y - box1.y) / mag }; - let box1Coords = box1.getCoordinates() - let box2Coords = box2.getCoordinates() + let box1Coords = box1.getCoordinates(); + let box2Coords = box2.getCoordinates(); - let maxPoint = box1.toWorldSpace(box1Coords[0].x, box1Coords[0].y) - let pointDelta = { x: maxPoint.x - box1.x, y: maxPoint.y - box1.y } - let pointMag = Math.sqrt(pointDelta.x * pointDelta.x + pointDelta.y * pointDelta.y) - let maxDot = delta.x * pointDelta.x / pointMag + delta.y * pointDelta.y / pointMag + let maxPoint = box1.toWorldSpace(box1Coords[0].x, box1Coords[0].y); + let pointDelta = { x: maxPoint.x - box1.x, y: maxPoint.y - box1.y }; + let pointMag = Math.sqrt(pointDelta.x * pointDelta.x + pointDelta.y * pointDelta.y); + let maxDot = (delta.x * pointDelta.x) / pointMag + (delta.y * pointDelta.y) / pointMag; - // ctx.font = "14pt Arial" - // ctx.fillText(String(round(maxDot, 3)), maxPoint.x, maxPoint.y) + for (let i = 1; i < box1Coords.length; i++) { + let point = box1Coords[i]; + let worldPoint = box1.toWorldSpace(point.x, point.y); - for (let i = 1; i < box1Coords.length; i++) { - let point = box1Coords[i] - let worldPoint = box1.toWorldSpace(point.x, point.y) + let pointDelta = { x: worldPoint.x - box1.x, y: worldPoint.y - box1.y }; + let mag = Math.sqrt(pointDelta.x * pointDelta.x + pointDelta.y * pointDelta.y); + pointDelta = { x: pointDelta.x / mag, y: pointDelta.y / mag }; - let pointDelta = { x: worldPoint.x - box1.x, y: worldPoint.y - box1.y } - let mag = Math.sqrt(pointDelta.x * pointDelta.x + pointDelta.y * pointDelta.y) - pointDelta = { x: pointDelta.x / mag, y: pointDelta.y / mag } + let dot = delta.x * pointDelta.x + delta.y * pointDelta.y; - let dot = delta.x * pointDelta.x + delta.y * pointDelta.y - - if (dot > maxDot) { - maxDot = dot - maxPoint = worldPoint - } - - // ctx.fillText(String(round(dot, 3)), worldPoint.x, worldPoint.y) + if (dot > maxDot) { + maxDot = dot; + maxPoint = worldPoint; } + } - for (point of box2Coords) { - let worldPoint = box2.toWorldSpace(point.x, point.y) + for (point of box2Coords) { + let worldPoint = box2.toWorldSpace(point.x, point.y); - let pointDelta = { x: worldPoint.x - box2.x, y: worldPoint.y - box2.y } - let mag = Math.sqrt(pointDelta.x * pointDelta.x + pointDelta.y * pointDelta.y) - pointDelta = { x: pointDelta.x / mag, y: pointDelta.y / mag } + let pointDelta = { x: worldPoint.x - box2.x, y: worldPoint.y - box2.y }; + let mag = Math.sqrt(pointDelta.x * pointDelta.x + pointDelta.y * pointDelta.y); + pointDelta = { x: pointDelta.x / mag, y: pointDelta.y / mag }; - let dot = -delta.x * pointDelta.x + -delta.y * pointDelta.y - - if (dot > maxDot) { - maxDot = dot - maxPoint = worldPoint - } + let dot = -delta.x * pointDelta.x + -delta.y * pointDelta.y; - // ctx.fillText(String(round(dot, 3)), worldPoint.x, worldPoint.y) + if (dot > maxDot) { + maxDot = dot; + maxPoint = worldPoint; } - return maxPoint + } + return maxPoint; } function testProjectionOverlap(proj1, proj2) { - let overlap = Math.max(0, Math.min(proj1.max, proj2.max) - Math.max(proj1.min, proj2.min)) + let overlap = Math.max(0, Math.min(proj1.max, proj2.max) - Math.max(proj1.min, proj2.min)); - if (overlap > 0) { - return overlap - } - return false + if (overlap > 0) { + return overlap; + } + return false; } function projectVectorToLine(x, y, lx, ly) { - let dotxl = x * lx + y * ly //dot input vector and plane vector - let dotll = lx * lx + ly * ly //dot plane vector on itself + let dotxl = x * lx + y * ly; //dot input vector and plane vector + let dotll = lx * lx + ly * ly; //dot plane vector on itself - let projX = dotxl / dotll * lx - let projY = dotxl / dotll * ly - return { x: projX, y: projY } + let projX = (dotxl / dotll) * lx; + let projY = (dotxl / dotll) * ly; + return { x: projX, y: projY }; } function testBoxCollision(box1, box2) { - let satAxisBox1 = box1.getCollisionAxis() - let satAxisBox2 = box2.getCollisionAxis() - - var colliding = true - let mvt = Number.MAX_VALUE - let normal = undefined - let satShape = undefined // true: box1, false: box2 - - for (axis of satAxisBox1) { - let box1Proj = box1.getProjection(axis.x, axis.y) - let box2Proj = box2.getProjection(axis.x, axis.y) - - let overlap = testProjectionOverlap(box1Proj, box2Proj) - - if (!overlap) { - colliding = false - } else if (overlap < mvt) { - mvt = overlap - normal = axis - satShape = true - } + let satAxisBox1 = box1.getCollisionAxis(); + let satAxisBox2 = box2.getCollisionAxis(); + + var colliding = true; + let mvt = Number.MAX_VALUE; + let normal = undefined; + let satShape = undefined; // true: box1, false: box2 + + for (axis of satAxisBox1) { + let box1Proj = box1.getProjection(axis.x, axis.y); + let box2Proj = box2.getProjection(axis.x, axis.y); + + let overlap = testProjectionOverlap(box1Proj, box2Proj); + + if (!overlap) { + colliding = false; + } else if (overlap < mvt) { + mvt = overlap; + normal = axis; + satShape = true; } + } - for (axis of satAxisBox2) { - let box1Proj = box1.getProjection(axis.x, axis.y) - let box2Proj = box2.getProjection(axis.x, axis.y) + for (axis of satAxisBox2) { + let box1Proj = box1.getProjection(axis.x, axis.y); + let box2Proj = box2.getProjection(axis.x, axis.y); - let overlap = testProjectionOverlap(box1Proj, box2Proj) + let overlap = testProjectionOverlap(box1Proj, box2Proj); - if (!overlap) { - colliding = false - } else if (overlap < mvt) { - mvt = overlap - normal = axis - satShape = false - } + if (!overlap) { + colliding = false; + } else if (overlap < mvt) { + mvt = overlap; + normal = axis; + satShape = false; } + } - if (colliding) { - let dot = normal.x * (box2.x - box1.x) + normal.y * (box2.y - box1.y) - - if (dot < 0) { - normal.x = -normal.x - normal.y = -normal.y - } + if (colliding) { + let dot = normal.x * (box2.x - box1.x) + normal.y * (box2.y - box1.y); - // if (satShape) { //axis is from box1 - // for - // } else { //axis is from box2 - - // } - - ctx.beginPath() - ctx.strokeStyle = "rgb(255,0,0)" - ctx.moveTo(box1.x, box1.y) - ctx.lineTo(box1.x + mvt * normal.x, box1.y + mvt * normal.y) - ctx.stroke() - // ctx.font = "24px Arial"; - // ctx.fillText(`${dot}`, box1.x + 15, box1.y - 15); - - // ctx.beginPath() - // ctx.strokeStyle = "rgb(255,0,255)" - // ctx.moveTo(width / 2, height / 2) - // ctx.lineTo(width / 2 + 50 * normal.x, height / 2 + 50 * normal.y) - - // ctx.moveTo(box1.x, box1.y) - // ctx.lineTo(box1.x + normal.x * mvt, box1.y + normal.y * mvt) - - // ctx.stroke() - - return { colliding: true, mtx: mvt, normal: normal, incidentFace: {} } - } else { - box2.color = "rgb(255,255,255)" - return { colliding: false } + if (dot < 0) { + normal.x = -normal.x; + normal.y = -normal.y; } + + ctx.beginPath(); + ctx.strokeStyle = "rgb(255,0,0)"; + ctx.moveTo(box1.x, box1.y); + ctx.lineTo(box1.x + mvt * normal.x, box1.y + mvt * normal.y); + ctx.stroke(); + + return { colliding: true, mtx: mvt, normal: normal, incidentFace: {} }; + } else { + box2.color = "rgb(255,255,255)"; + return { colliding: false }; + } } function cross(x1, y1, x2, y2) { - return x1 * y2 - y1 * x2 + return x1 * y2 - y1 * x2; } function resolveCollision(box1, box2, mvt, normal, collisionPoint) { - if (box1.anchored && box2.anchored) { return } - - if (box1.anchored) { - box2.x += mvt * normal.x - box2.y += mvt * normal.y - } else if (box2.anchored) { - box1.x -= mvt * normal.x - box1.y -= mvt * normal.y - } else { - box1.x -= mvt / 2 * normal.x - box1.y -= mvt / 2 * normal.y - box2.x += mvt / 2 * normal.x - box2.y += mvt / 2 * normal.y - } - - let collArm1 = { x: collisionPoint.x - box1.x, y: collisionPoint.y - box1.y } - let rotVel1 = { x: -box1.dr * collArm1.y, y: box1.dr * collArm1.x } - let closVel1 = { x: box1.dx + rotVel1.x, y: box1.dy + rotVel1.y } - - let collArm2 = { x: collisionPoint.x - box2.x, y: collisionPoint.y - box2.y } - let rotVel2 = { x: -box2.dr * collArm2.y, y: box2.dr * collArm2.x } - let closVel2 = { x: box2.dx + rotVel2.x, y: box2.dy + rotVel2.y } - - // console.log(collArm1, rotVel1, closVel1) - // console.log(collArm2, rotVel2, closVel2) - - let impAug1 = cross(collArm1.x, collArm1.y, normal.x, normal.y) - impAug1 = impAug1 * impAug1 * box1.invi - let impAug2 = cross(collArm2.x, collArm2.y, normal.x, normal.y) - impAug2 = impAug2 * impAug2 * box2.invi - - // console.log(impAug1, impAug2) - - let relVel = { x: closVel1.x - closVel2.x, y: closVel1.y - closVel2.y } - let sepVel = relVel.x * normal.x + relVel.y * normal.y - let newsepVel = -sepVel * Math.min(box1.e, box2.e) - let vsepDiff = newsepVel - sepVel - - // console.log(relVel, sepVel, newsepVel, vsepDiff) - - let impulse = vsepDiff / (box1.invm + box2.invm + impAug1 + impAug2) - let impulseVec = { x: impulse * normal.x, y: impulse * normal.y } - - // console.log(impulse, impulseVec) - - // console.log(impulseVec.x * box1.invm, impulseVec.y * box1.invm) - - box1.dx += impulseVec.x * box1.invm - box1.dy += impulseVec.y * box1.invm - box2.dx += impulseVec.x * -box2.invm - box2.dy += impulseVec.y * -box2.invm - - box1.dr += box1.invi * cross(collArm1.x, collArm1.y, impulseVec.x, impulseVec.y) - box2.dr -= box2.invi * cross(collArm2.x, collArm2.y, impulseVec.x, impulseVec.y) - - - // console.log(box1, box2) - - // drawLineTo(box1.x, box1.y, box1.x + collArm1.x, box1.y + collArm1.y) - // drawLineTo(box2.x, box2.y, box2.x + collArm2.x, box2.y + collArm2.y) - - // let relVelx = box2.dx - box1.dx - // let relVely = box2.dy - box1.dy - - // let velNorm = normal.x * relVelx + normal.y * relVely - // if (velNorm > 0) - // return; - - // var j = -(1 + restitution) * velNorm - // let inverseMassSum = (1 / (box1.m) + 1 / (box2.m)) - // j = j / inverseMassSum - - // box1.dx -= normal.x * j / (box1.m) - // box1.dy -= normal.y * j / (box1.m) - // box2.dx += normal.x * j / (box2.m) - // box2.dy += normal.y * j / (box2.m) + if (box1.anchored && box2.anchored) { + return; + } + + if (box1.anchored) { + box2.x += mvt * normal.x; + box2.y += mvt * normal.y; + } else if (box2.anchored) { + box1.x -= mvt * normal.x; + box1.y -= mvt * normal.y; + } else { + box1.x -= (mvt / 2) * normal.x; + box1.y -= (mvt / 2) * normal.y; + box2.x += (mvt / 2) * normal.x; + box2.y += (mvt / 2) * normal.y; + } + + let collArm1 = { x: collisionPoint.x - box1.x, y: collisionPoint.y - box1.y }; + let rotVel1 = { x: -box1.dr * collArm1.y, y: box1.dr * collArm1.x }; + let closVel1 = { x: box1.dx + rotVel1.x, y: box1.dy + rotVel1.y }; + + let collArm2 = { x: collisionPoint.x - box2.x, y: collisionPoint.y - box2.y }; + let rotVel2 = { x: -box2.dr * collArm2.y, y: box2.dr * collArm2.x }; + let closVel2 = { x: box2.dx + rotVel2.x, y: box2.dy + rotVel2.y }; + + let impAug1 = cross(collArm1.x, collArm1.y, normal.x, normal.y); + impAug1 = impAug1 * impAug1 * box1.invi; + let impAug2 = cross(collArm2.x, collArm2.y, normal.x, normal.y); + impAug2 = impAug2 * impAug2 * box2.invi; + + let relVel = { x: closVel1.x - closVel2.x, y: closVel1.y - closVel2.y }; + let sepVel = relVel.x * normal.x + relVel.y * normal.y; + let newsepVel = -sepVel * Math.min(box1.e, box2.e); + let vsepDiff = newsepVel - sepVel; + + let impulse = vsepDiff / (box1.invm + box2.invm + impAug1 + impAug2); + let impulseVec = { x: impulse * normal.x, y: impulse * normal.y }; + + box1.dx += impulseVec.x * box1.invm; + box1.dy += impulseVec.y * box1.invm; + box2.dx += impulseVec.x * -box2.invm; + box2.dy += impulseVec.y * -box2.invm; + + box1.dr += box1.invi * cross(collArm1.x, collArm1.y, impulseVec.x, impulseVec.y); + box2.dr -= box2.invi * cross(collArm2.x, collArm2.y, impulseVec.x, impulseVec.y); } function clearCanvas() { - ctx.clearRect(0, 0, width, height) + ctx.clearRect(0, 0, width, height); } function calculatePhysics(dt) { - for (box of boxes) { - if (box.anchored) { - box.dx = 0 - box.dy = 0 - box.dr = 0 - continue - } - box.x += box.dx * dt - box.y += box.dy * dt - box.r += box.dr * dt - box.testBounds() - box.dy += gravity * dt + for (box of boxes) { + if (box.anchored) { + box.dx = 0; + box.dy = 0; + box.dr = 0; + continue; } + box.x += box.dx * dt; + box.y += box.dy * dt; + box.r += box.dr * dt; + box.testBounds(); + box.dy += gravity * dt; + } } -const width = 1000 -const height = 1000 +const width = 1000; +const height = 1000; const canvas = document.getElementById("source"); const ctx = canvas.getContext("2d"); -ctx.canvas.width = width -ctx.canvas.height = height +ctx.canvas.width = width; +ctx.canvas.height = height; -let boxes = [] +let boxes = []; -const gravity = 0 -const restitution = 1 +const gravity = 0; +const restitution = 1; +var substeps = 1; -let time = 0.0 +let time = 0.0; function loop(t) { - dt = (t / 1000) - time - - clearCanvas() - if (mouse1Down) { - // for (box of boxes) { - // let point = getMousePos(canvas, mousePos) - // let delta = { x: point.x - box.x, y: point.y - box.y } - // box.dx += delta.x * dt * 5 - // box.dy += delta.y * dt * 5 - // } - - let point = getMousePos(canvas, mousePos) - let closestBox = boxes[0] - let distance = Math.sqrt((point.x - closestBox.x) ** 2 + (point.y - closestBox.y) ** 2) - for (let i = 1; i < boxes.length; i++) { - let nextBox = boxes[i] - let nextDistance = Math.sqrt((point.x - nextBox.x) ** 2 + (point.y - nextBox.y) ** 2) - if (nextDistance < distance) { - closestBox = nextBox - distance = nextDistance - } - } - closestBox.x = point.x - closestBox.y = point.y - - closestBox.dx = mouseDelta.x * 50 - closestBox.dy = mouseDelta.y * 50 + dt = t / 1000 - time; + + clearCanvas(); + if (mouse1Down) { + let point = getMousePos(canvas, mousePos); + let closestBox = boxes[0]; + let distance = Math.sqrt((point.x - closestBox.x) ** 2 + (point.y - closestBox.y) ** 2); + for (let i = 1; i < boxes.length; i++) { + let nextBox = boxes[i]; + let nextDistance = Math.sqrt((point.x - nextBox.x) ** 2 + (point.y - nextBox.y) ** 2); + if (nextDistance < distance) { + closestBox = nextBox; + distance = nextDistance; + } } - - for (let c = 0; c < 1; c++) { - let cdt = dt / 1 - calculatePhysics(cdt) - - for (let i = 0; i < boxes.length; i++) { - let box1 = boxes[i] - for (let n = i + 1; n < boxes.length; n++) { - let box2 = boxes[n] - - let result = testBoxCollision(box1, box2) - if (result.colliding) { - // console.log("collided") - - let collisionPoint = getCollidingPoint(box1, box2) - ctx.fillStyle = "rgb(255, 0, 0)" - ctx.fillRect(collisionPoint.x - 2.5, collisionPoint.y - 2.5, 5, 5) - resolveCollision(box1, box2, result.mtx, result.normal, collisionPoint) - } - } + closestBox.x = point.x; + closestBox.y = point.y; + + closestBox.dx = mouseDelta.x * 50; + closestBox.dy = mouseDelta.y * 50; + } + + for (let c = 0; c < substeps; c++) { + let cdt = dt / substeps; + calculatePhysics(cdt); + + for (let i = 0; i < boxes.length; i++) { + let box1 = boxes[i]; + for (let n = i + 1; n < boxes.length; n++) { + let box2 = boxes[n]; + + let result = testBoxCollision(box1, box2); + if (result.colliding) { + let collisionPoint = getCollidingPoint(box1, box2); + ctx.fillStyle = "rgb(255, 0, 0)"; + ctx.fillRect(collisionPoint.x - 2.5, collisionPoint.y - 2.5, 5, 5); + resolveCollision(box1, box2, result.mtx, result.normal, collisionPoint); } - + } } + } - for (box of boxes) { - box.render(ctx) - } + for (box of boxes) { + box.render(ctx); + } - time = t / 1000 - window.requestAnimationFrame(loop) + time = t / 1000; + window.requestAnimationFrame(loop); } function startup() { - console.log("Starting simulation") - - for (let i = 0; i < 35; i++) { - boxes.push(new Box( - Math.random() * width, - Math.random() * height, - 25 + Math.random() * 55, - 25 + Math.random() * 55, - 360 * Math.random(), - 150 - Math.random() * 300, - 150 - Math.random() * 300, - 150 - Math.random() * 300)) - } - - window.requestAnimationFrame(loop) + console.log("Starting simulation"); + + for (let i = 0; i < 25; i++) { + boxes.push( + new Box( + Math.random() * width, + Math.random() * height, + 50 + Math.random() * 55, + 50 + Math.random() * 55, + 360 * Math.random(), + 150 - Math.random() * 300, + 150 - Math.random() * 300, + 150 - Math.random() * 300 + ) + ); + } + + window.requestAnimationFrame(loop); } -window.addEventListener('load', (event) => { - startup() -}) +window.addEventListener("load", (event) => { + startup(); +}); function getMousePos(canvas, point) { - var rect = canvas.getBoundingClientRect(); - return { - x: (point.x - rect.left) / rect.width * width, - y: (point.y - rect.top) / rect.height * height - }; + var rect = canvas.getBoundingClientRect(); + return { + x: ((point.x - rect.left) / rect.width) * width, + y: ((point.y - rect.top) / rect.height) * height, + }; } -let mouse1Down = false -let mousePos = { x: 0, y: 0 } -let mouseDelta = { x: 0, y: 0 } - -document.addEventListener('mousedown', (event) => { - console.log("mousedown") - if (event.buttons == 1) { - mouse1Down = true - } -}) -document.addEventListener('mouseup', (event) => { - if (event.buttons == 0) { - mouse1Down = false - } -}) - -canvas.addEventListener('mousemove', (event) => { - mousePos.x = event.clientX - mousePos.y = event.clientY - mouseDelta.x = event.movementX - mouseDelta.y = event.movementY -}) +let mouse1Down = false; +let mousePos = { x: 0, y: 0 }; +let mouseDelta = { x: 0, y: 0 }; + +document.addEventListener("mousedown", (event) => { + console.log("mousedown"); + if (event.buttons == 1) { + mouse1Down = true; + } +}); +document.addEventListener("mouseup", (event) => { + if (event.buttons == 0) { + mouse1Down = false; + } +}); + +canvas.addEventListener("mousemove", (event) => { + mousePos.x = event.clientX; + mousePos.y = event.clientY; + mouseDelta.x = event.movementX; + mouseDelta.y = event.movementY; +}); + +document.onkeydown = function (event) { + if (event.key == " ") { + console.log("spacePressed"); + boxes.push( + new Box( + Math.random() * width, + Math.random() * height, + 50 + Math.random() * 55, + 50 + Math.random() * 55, + 360 * Math.random(), + 150 - Math.random() * 300, + 150 - Math.random() * 300, + 150 - Math.random() * 300 + ) + ); + } +};