summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.prettierrc1
-rw-r--r--Makefile15
-rw-r--r--package-lock.json37
-rw-r--r--package.json8
-rw-r--r--src/index.html25
-rw-r--r--src/index.js94
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();
+});