one/frontend/src/components/PhysicsSimulation.tsx
Ameya Shenoy 406177bf01 chore: stuff
Signed-off-by: Ameya Shenoy <shenoy.ameya@gmail.com>
2025-06-07 16:33:42 +05:30

227 lines
6 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { useTheme } from "next-themes";
import Matter from "matter-js";
import { twj } from "tw-to-css";
export function World() {
const containerRef = useRef<HTMLDivElement>(null);
const { resolvedTheme } = useTheme();
const getRandomXY = (width: number, height: number, radius: number) => {
const randomX = Math.floor(Math.random() * (width - 2 * radius)) + radius;
const randomY = Math.floor(Math.random() * (height - 2 * radius)) + radius;
return [randomX, randomY];
};
useEffect(() => {
const pillTexts: HTMLDivElement[] = [];
let container = containerRef.current;
if (!container) {
return;
}
const width = container.offsetWidth;
const height = container.offsetHeight;
// TODO: ask Shubh about using variables in useEffect not working as expected
// const bgColor = getComputedStyle(document.documentElement)
// .getPropertyValue("--color-background")
// .trim();
// const fgColor = getComputedStyle(document.documentElement)
// .getPropertyValue("--color-foreground")
// .trim();
const bgColor =
resolvedTheme === "dark" ? "oklch(0.145 0 0)" : "oklch(1 0 0)";
const fgColor =
resolvedTheme === "dark" ? "oklch(0.985 0 0)" : "oklch(0.145 0 0)";
const {
Engine,
Events,
Render,
Bodies,
Composite,
Runner,
Mouse,
MouseConstraint,
} = Matter;
const engine = Engine.create();
engine.gravity.y = 0.3;
engine.enableSleeping = true;
const render = Render.create({
element: container,
engine,
options: {
width,
height,
wireframes: false,
background: bgColor,
},
});
// Bottom wall (ground)
const ground = Bodies.rectangle(width / 2, height - 10, width, 20, {
isStatic: true,
render: { fillStyle: bgColor },
});
// Left wall
const leftWall = Bodies.rectangle(-10, height / 2, 20, height + 40, {
isStatic: true,
render: { fillStyle: bgColor },
});
// Right wall
const rightWall = Bodies.rectangle(
width + 10,
height / 2,
20,
height + 40,
{
isStatic: true,
render: { fillStyle: bgColor },
},
);
// Top wall
const topWall = Bodies.rectangle(width / 2, -10, width, 20, {
isStatic: true,
render: { fillStyle: bgColor },
});
// Random ball position
const ballRadius = 15;
let [randomX, randomY] = getRandomXY(width, height, ballRadius);
const ballRed = Bodies.circle(randomX, randomY, ballRadius, {
restitution: 0.8,
render: { fillStyle: "red" },
});
[randomX, randomY] = getRandomXY(width, height, ballRadius);
const ballBlue = Bodies.circle(randomX, randomY, ballRadius, {
restitution: 0.8,
render: { fillStyle: "blue" },
});
[randomX, randomY] = getRandomXY(width, height, ballRadius);
const ballGreen = Bodies.circle(randomX, randomY, ballRadius, {
restitution: 0.8,
render: { fillStyle: "green" },
});
// Mouse interaction
const mouse = Mouse.create(render.canvas);
const mouseConstraint = MouseConstraint.create(engine, {
mouse,
constraint: {
stiffness: 1,
angularStiffness: 0,
render: { visible: false },
},
});
const pills: Matter.Body[] = [];
const pillsToRenderInfo = [
{ text: "ENGINEERING MANAGER" },
{ text: "PRINCIPAL ENGINEER" },
{ text: "NIX", chamferRadius: 20 },
{ text: "AI", chamferRadius: 20 },
];
pillsToRenderInfo.forEach(function (pillInfo) {
const fontSize = 32;
const pillText = pillInfo.text;
const ctx = render.canvas.getContext("2d");
ctx.font = `${fontSize}px Mono`;
const textWidth = ctx.measureText(pillText).width;
const pillHeight = fontSize * 2;
const pillWidth = textWidth + pillHeight;
[randomX, randomY] = getRandomXY(width, height, pillWidth / 2);
const chamferRadius = pillInfo.chamferRadius || pillHeight / 2;
const pill = Matter.Bodies.rectangle(
randomX,
randomY,
pillWidth,
pillHeight,
{
chamfer: { radius: chamferRadius },
isStatic: false,
density: 0.001,
friction: 0.05,
frictionAir: 0.01,
render: {
fillStyle: "rgba(255, 255, 255, 0)", // transparent fill
strokeStyle: fgColor, // black border
lineWidth: 1,
},
},
);
pills.push(pill);
const text: HTMLDivElement = document.createElement("div");
Object.assign(text.style, {
...twj("absolute flex justify-center items-center"),
...{
width: `${pillWidth}px`,
height: `${pillHeight}px`,
fontFamily: "Space Mono",
fontSize: `${fontSize}px`,
color: fgColor,
pointerEvents: "none",
},
});
text.innerText = pillText;
pillTexts.push(text);
render.canvas.getContext("2d").measure;
document.body.appendChild(text);
Events.on(engine, "afterUpdate", () => {
text.style.left = `${pill.position.x - pillWidth / 2 + 60}px`;
text.style.top = `${pill.position.y + pillHeight / 2 - 4}px`;
text.style.transform = `rotate(${pill.angle}rad)`;
});
});
// Add all elements to the world
Composite.add(engine.world, [
ground,
leftWall,
rightWall,
topWall,
ballGreen,
ballBlue,
ballRed,
...pills,
mouseConstraint,
]);
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
return () => {
Render.stop(render);
Runner.stop(runner);
Composite.clear(engine.world);
Engine.clear(engine);
render.canvas.remove();
pillTexts.forEach((pillText) => {
pillText.remove();
});
};
}, [resolvedTheme]);
return (
<div
ref={containerRef}
className="w-[95vw] h-[40vh] mx-auto relative overflow-hidden"
/>
);
}