diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .prettierrc | 1 | ||||
-rw-r--r-- | Makefile | 15 | ||||
-rw-r--r-- | package-lock.json | 37 | ||||
-rw-r--r-- | package.json | 8 | ||||
-rw-r--r-- | src/index.html | 25 | ||||
-rw-r--r-- | src/index.js | 94 |
7 files changed, 182 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0a5c34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +/dist/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b205e04 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.POSIX: + +all: build + +run: build + npx http-server + +build: + mkdir -p dist + cp src/index.html dist/index.html + cp src/index.js dist/index.js + cp node_modules/@dimforge/rapier2d-compat/rapier.es.js dist/rapier.es.js + +format: + npx prettier . --write diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..257fc9a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "throw_simulation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@dimforge/rapier2d-compat": "^0.14.0" + }, + "devDependencies": { + "prettier": "3.3.3" + } + }, + "node_modules/@dimforge/rapier2d-compat": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier2d-compat/-/rapier2d-compat-0.14.0.tgz", + "integrity": "sha512-sljQVPstRS63hVLnVNphsZUjH51TZoptVM0XlglKAdZ8CT+kWnmA6olwjkF7omPWYrlKMd/nHORxOUdJDOSoAQ==", + "license": "Apache-2.0" + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9caf6a --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@dimforge/rapier2d-compat": "^0.14.0" + }, + "devDependencies": { + "prettier": "3.3.3" + } +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..aa49141 --- /dev/null +++ b/src/index.html @@ -0,0 +1,25 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Document</title> + <script type="module" src="index.js"></script> + <style> + html, + body { + margin: 0; + } + + div#throwable { + position: fixed; + transform: translate(-50%, -50%); + background-color: red; + border-radius: 50%; + } + </style> + </head> + <body> + <div id="throwable"></div> + </body> +</html> diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1275360 --- /dev/null +++ b/src/index.js @@ -0,0 +1,94 @@ +import RAPIER from "./rapier.es.js"; + +RAPIER.init().then(() => { + const CLICK_IMPULSE_MULTIPLIER = 300_000; + const GRAVITY = { x: 0.0, y: 2000 }; + const LINEAR_DAMPING = 0.5; + const OBJ_BASE_RADIUS = 30; + const OBJ_BASE_SIZE = { x: OBJ_BASE_RADIUS * 2, y: OBJ_BASE_RADIUS * 2 }; + const OBJ_MASS = 100; + const STEP_MS = 33; + const TARGET_WIDTH_FACTOR = 0.05; + const VIEWPORT_SIZE = { + x: document.documentElement.clientWidth, + y: document.documentElement.clientHeight, + }; + const WIDTH_JUMP_FACTOR = 5; + + let world = new RAPIER.World(GRAVITY); + + const wallColliderDescs = { + x: RAPIER.ColliderDesc.cuboid(VIEWPORT_SIZE.x, 1).setRestitution(1), + y: RAPIER.ColliderDesc.cuboid(1, VIEWPORT_SIZE.y).setRestitution(1), + }; + world.createCollider(wallColliderDescs.x); + world.createCollider(wallColliderDescs.x.setTranslation(0, VIEWPORT_SIZE.y)); + world.createCollider(wallColliderDescs.y); + world.createCollider(wallColliderDescs.y.setTranslation(VIEWPORT_SIZE.x, 0)); + + let objRigidBodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(VIEWPORT_SIZE.x / 2, VIEWPORT_SIZE.y / 3) + .setLinearDamping(LINEAR_DAMPING) + .setCcdEnabled(true); + let obj = world.createRigidBody(objRigidBodyDesc); + world.createCollider( + RAPIER.ColliderDesc.ball(OBJ_BASE_RADIUS).setMass(OBJ_MASS), + obj, + ); + + const objElement = document.querySelector("div#throwable"); + const objSize = { + x: OBJ_BASE_SIZE.x, + y: OBJ_BASE_SIZE.y, + }; + let objVelocityLastFrame = null; + + let mainLoop = () => { + world.step(); + + const objPosition = obj.translation(); + objElement.style.left = `${objPosition.x}px`; + objElement.style.top = `${objPosition.y}px`; + + const objVelocity = obj.linvel(); + if (objVelocityLastFrame !== null) { + // Just delta velocity across a frame, technically + const objAcceleration = { + x: objVelocity.x - objVelocityLastFrame.x, + y: objVelocity.y - objVelocityLastFrame.y, + }; + let targetWidth = + OBJ_BASE_SIZE.x + TARGET_WIDTH_FACTOR * Math.abs(objAcceleration.y); + let targetHeight = + OBJ_BASE_SIZE.y + TARGET_WIDTH_FACTOR * Math.abs(objAcceleration.x); + + objSize.x = objSize.x + (targetWidth - objSize.x) / WIDTH_JUMP_FACTOR; + objSize.y = objSize.y + (targetHeight - objSize.y) / WIDTH_JUMP_FACTOR; + } + objVelocityLastFrame = objVelocity; + + objElement.style.width = `${objSize.x}px`; + objElement.style.height = `${objSize.y}px`; + + setTimeout(mainLoop, STEP_MS); + }; + + document.addEventListener("click", (e) => { + const clickPosition = { x: e.clientX, y: e.clientY }; + const objPosition = obj.translation(); + const direction = { + x: clickPosition.x - objPosition.x, + y: clickPosition.y - objPosition.y, + }; + const distance = Math.sqrt(direction.x ** 2 + direction.y ** 2); + obj.setLinvel({x: 0, y: 0}); + if (distance > 0) { + obj.applyImpulse({ + x: (direction.x * CLICK_IMPULSE_MULTIPLIER) / distance, + y: (direction.y * CLICK_IMPULSE_MULTIPLIER) / distance, + }); + } + }); + + mainLoop(); +}); |