aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/PhysicsSimulationWeight.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/PhysicsSimulationWeight.tsx')
-rw-r--r--src/client/views/nodes/PhysicsSimulationWeight.tsx913
1 files changed, 913 insertions, 0 deletions
diff --git a/src/client/views/nodes/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsSimulationWeight.tsx
new file mode 100644
index 000000000..227f20901
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationWeight.tsx
@@ -0,0 +1,913 @@
+import { InputAdornment, TextField } from "@mui/material";
+import { useEffect, useState } from "react";
+import { IWallProps } from "./PhysicsSimulationWall";
+import { Wedge } from "./PhysicsSimulationWedge";
+
+export interface IForce {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWeightProps {
+ adjustPendulumAngle: { angle: number; length: number };
+ color: string;
+ displayXPosition: number;
+ displayYPosition: number;
+ displayXVelocity: number;
+ displayYVelocity: number;
+ elasticCollisions: boolean;
+ startForces: IForce[];
+ incrementTime: number;
+ mass: number;
+ paused: boolean;
+ pendulum: boolean;
+ pendulumLength: number;
+ wedge: boolean;
+ radius: number;
+ reset: boolean;
+ setDisplayXAcceleration: (val: number) => any;
+ setDisplayXPosition: (val: number) => any;
+ setDisplayXVelocity: (val: number) => any;
+ setDisplayYAcceleration: (val: number) => any;
+ setDisplayYPosition: (val: number) => any;
+ setDisplayYVelocity: (val: number) => any;
+ setPaused: (bool: boolean) => any;
+ setPendulumAngle: (val: number) => any;
+ setPendulumLength: (val: number) => any;
+ setStartPendulumAngle: (val: number) => any;
+ showAcceleration: boolean;
+ mode: string;
+ noMovement: boolean;
+ pendulumAngle: number;
+ setSketching: (val: boolean) => any;
+ showForces: boolean;
+ showForceMagnitudes: boolean;
+ showVelocity: boolean;
+ startPosX: number;
+ startPosY: number;
+ startVelX?: number;
+ startVelY?: number;
+ timestepSize: number;
+ updateDisplay: { xDisplay: number; yDisplay: number };
+ updatedForces: IForce[];
+ setUpdatedForces: (val: IForce[]) => any;
+ walls: IWallProps[];
+ coefficientOfKineticFriction: number;
+ wedgeWidth: number;
+ wedgeHeight: number;
+}
+
+export const Weight = (props: IWeightProps) => {
+ const {
+ adjustPendulumAngle,
+ color,
+ displayXPosition,
+ displayYPosition,
+ displayXVelocity,
+ displayYVelocity,
+ elasticCollisions,
+ startForces,
+ incrementTime,
+ mass,
+ paused,
+ pendulum,
+ pendulumLength,
+ wedge,
+ radius,
+ mode,
+ noMovement,
+ pendulumAngle,
+ reset,
+ setSketching,
+ setDisplayXAcceleration,
+ setDisplayXPosition,
+ setDisplayXVelocity,
+ setDisplayYAcceleration,
+ setDisplayYPosition,
+ setDisplayYVelocity,
+ setPaused,
+ setPendulumAngle,
+ setPendulumLength,
+ setStartPendulumAngle,
+ showAcceleration,
+ showForces,
+ showForceMagnitudes,
+ showVelocity,
+ startPosX,
+ startPosY,
+ startVelX,
+ startVelY,
+ timestepSize,
+ updateDisplay,
+ updatedForces,
+ setUpdatedForces,
+ walls,
+ coefficientOfKineticFriction,
+ wedgeWidth,
+ wedgeHeight,
+ } = props;
+
+ // Constants
+ const draggable = !wedge && mode == "Freeform";
+ const epsilon = 0.0001;
+
+ const forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: mass * 9.81,
+ directionInDegrees: 270,
+ };
+ const xMax = window.innerWidth * 0.7;
+ const xMin = 0;
+ const yMax = window.innerHeight * 0.8;
+ const yMin = 0;
+
+ // State hooks
+ const [dragging, setDragging] = useState(false);
+ const [kineticFriction, setKineticFriction] = useState(false);
+ const [updatedStartPosX, setUpdatedStartPosX] = useState(startPosX);
+ const [updatedStartPosY, setUpdatedStartPosY] = useState(startPosY);
+ const [xPosition, setXPosition] = useState(startPosX);
+ const [xVelocity, setXVelocity] = useState(startVelX ?? 0);
+ const [yPosition, setYPosition] = useState(startPosY);
+ const [yVelocity, setYVelocity] = useState(startVelY ?? 0);
+
+ // Helper function to go between display and real values
+ const getDisplayYPos = (yPos: number) => {
+ return yMax - yPos - 2 * radius + 5;
+ };
+ const getYPosFromDisplay = (yDisplay: number) => {
+ return yMax - yDisplay - 2 * radius + 5;
+ };
+
+ // Set display values based on real values
+ const setYPosDisplay = (yPos: number) => {
+ const displayPos = getDisplayYPos(yPos);
+ setDisplayYPosition(Math.round(displayPos * 100) / 100);
+ };
+ const setXPosDisplay = (xPos: number) => {
+ setDisplayXPosition(Math.round(xPos * 100) / 100);
+ };
+ const setYVelDisplay = (yVel: number) => {
+ setDisplayYVelocity((-1 * Math.round(yVel * 100)) / 100);
+ };
+ const setXVelDisplay = (xVel: number) => {
+ setDisplayXVelocity(Math.round(xVel * 100) / 100);
+ };
+
+ const setDisplayValues = (
+ xPos: number = xPosition,
+ yPos: number = yPosition,
+ xVel: number = xVelocity,
+ yVel: number = yVelocity
+ ) => {
+ setYPosDisplay(yPos);
+ setXPosDisplay(xPos);
+ setYVelDisplay(yVel);
+ setXVelDisplay(xVel);
+ setDisplayYAcceleration(
+ (-1 * Math.round(getNewAccelerationY(updatedForces) * 100)) / 100
+ );
+ setDisplayXAcceleration(
+ Math.round(getNewAccelerationX(updatedForces) * 100) / 100
+ );
+ };
+
+ // When display values updated by user, update real values
+ useEffect(() => {
+ if (updateDisplay.xDisplay != xPosition) {
+ let x = updateDisplay.xDisplay;
+ x = Math.max(0, x);
+ x = Math.min(x, xMax - 2 * radius);
+ setUpdatedStartPosX(x);
+ setXPosition(x);
+ setDisplayXPosition(x);
+ }
+
+ if (updateDisplay.yDisplay != getDisplayYPos(yPosition)) {
+ let y = updateDisplay.yDisplay;
+ y = Math.max(0, y);
+ y = Math.min(y, yMax - 2 * radius);
+ setDisplayYPosition(y);
+ let coordinatePosition = getYPosFromDisplay(y);
+ setUpdatedStartPosY(coordinatePosition);
+ setYPosition(coordinatePosition);
+ }
+
+ if (displayXVelocity != xVelocity) {
+ let x = displayXVelocity;
+ setXVelocity(x);
+ setDisplayXVelocity(x);
+ }
+
+ if (displayYVelocity != -yVelocity) {
+ let y = displayYVelocity;
+ setYVelocity(-y);
+ setDisplayYVelocity(y);
+ }
+ }, [updateDisplay]);
+
+ // Check for collisions and update
+ useEffect(() => {
+ if (!paused && !noMovement) {
+ let collisions = false;
+ if (!pendulum) {
+ const collisionsWithGround = checkForCollisionsWithGround();
+ const collisionsWithWalls = checkForCollisionsWithWall();
+ collisions = collisionsWithGround || collisionsWithWalls;
+ }
+ if (!collisions) {
+ update();
+ }
+ setDisplayValues();
+ }
+ }, [incrementTime]);
+
+ useEffect(() => {
+ resetEverything();
+ }, [reset]);
+
+ useEffect(() => {
+ setXVelocity(startVelX ?? 0);
+ setYVelocity(startVelY ?? 0);
+ setDisplayValues();
+ }, [startForces]);
+
+ const resetEverything = () => {
+ setKineticFriction(false);
+ setXPosition(updatedStartPosX);
+ setYPosition(updatedStartPosY);
+ setXVelocity(startVelX ?? 0);
+ setYVelocity(startVelY ?? 0);
+ setUpdatedForces(startForces);
+ setDisplayValues();
+ };
+
+ // Change pendulum angle based on input field
+ useEffect(() => {
+ let length = adjustPendulumAngle.length;
+ const x =
+ length * Math.cos(((90 - adjustPendulumAngle.angle) * Math.PI) / 180);
+ const y =
+ length * Math.sin(((90 - adjustPendulumAngle.angle) * Math.PI) / 180);
+ const xPos = xMax / 2 - x - radius;
+ const yPos = y - radius - 5;
+ setXPosition(xPos);
+ setYPosition(yPos);
+ setUpdatedStartPosX(xPos);
+ setUpdatedStartPosY(yPos);
+ setPendulumAngle(adjustPendulumAngle.angle);
+ setPendulumLength(adjustPendulumAngle.length);
+ }, [adjustPendulumAngle]);
+
+ const getNewAccelerationX = (forceList: IForce[]) => {
+ let newXAcc = 0;
+ forceList.forEach((force) => {
+ newXAcc +=
+ (force.magnitude *
+ Math.cos((force.directionInDegrees * Math.PI) / 180)) /
+ mass;
+ });
+ return newXAcc;
+ };
+
+ const getNewAccelerationY = (forceList: IForce[]) => {
+ let newYAcc = 0;
+ forceList.forEach((force) => {
+ newYAcc +=
+ (-1 *
+ (force.magnitude *
+ Math.sin((force.directionInDegrees * Math.PI) / 180))) /
+ mass;
+ });
+ return newYAcc;
+ };
+
+ const getNewForces = (
+ xPos: number,
+ yPos: number,
+ xVel: number,
+ yVel: number
+ ) => {
+ if (!pendulum) {
+ return updatedForces;
+ }
+ const x = xMax / 2 - xPos - radius;
+ const y = yPos + radius + 5;
+ let angle = (Math.atan(y / x) * 180) / Math.PI;
+ if (angle < 0) {
+ angle += 180;
+ }
+ let oppositeAngle = 90 - angle;
+ if (oppositeAngle < 0) {
+ oppositeAngle = 90 - (180 - angle);
+ }
+
+ const pendulumLength = Math.sqrt(x * x + y * y);
+ setPendulumAngle(oppositeAngle);
+ setPendulumLength(Math.sqrt(x * x + y * y));
+
+ const mag =
+ mass * 9.81 * Math.cos((oppositeAngle * Math.PI) / 180) +
+ (mass * (xVel * xVel + yVel * yVel)) / pendulumLength;
+
+ const forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: angle,
+ };
+
+ return [forceOfGravity, forceOfTension];
+ };
+
+ const getNewPosition = (pos: number, vel: number) => {
+ return pos + vel * timestepSize;
+ };
+
+ const getNewVelocity = (vel: number, acc: number) => {
+ return vel + acc * timestepSize;
+ };
+
+ const checkForCollisionsWithWall = () => {
+ let collision = false;
+ const minX = xPosition;
+ const maxX = xPosition + 2 * radius;
+ const containerWidth = window.innerWidth;
+ if (xVelocity != 0) {
+ walls.forEach((wall) => {
+ if (wall.angleInDegrees == 90) {
+ const wallX = (wall.xPos / 100) * window.innerWidth;
+ if (wall.xPos < 0.35) {
+ if (minX <= wallX) {
+ if (elasticCollisions) {
+ setXVelocity(-xVelocity);
+ } else {
+ setXVelocity(0);
+ setXPosition(wallX + 5);
+ }
+ collision = true;
+ }
+ } else {
+ if (maxX >= wallX) {
+ if (elasticCollisions) {
+ setXVelocity(-xVelocity);
+ } else {
+ setXVelocity(0);
+ setXPosition(wallX - 2 * radius + 5);
+ }
+ collision = true;
+ }
+ }
+ }
+ });
+ }
+ return collision;
+ };
+
+ const checkForCollisionsWithGround = () => {
+ let collision = false;
+ const maxY = yPosition + 2 * radius;
+ if (yVelocity > 0) {
+ walls.forEach((wall) => {
+ if (wall.angleInDegrees == 0) {
+ const groundY = (wall.yPos / 100) * window.innerHeight;
+ if (maxY >= groundY) {
+ if (elasticCollisions) {
+ setYVelocity(-yVelocity);
+ } else {
+ setYVelocity(0);
+ setYPosition(groundY - 2 * radius + 5);
+ const forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: 9.81 * mass,
+ directionInDegrees: 270,
+ };
+ const normalForce: IForce = {
+ description: "Normal force",
+ magnitude: 9.81 * mass,
+ directionInDegrees: wall.angleInDegrees + 90,
+ };
+ setUpdatedForces([forceOfGravity, normalForce]);
+ }
+ collision = true;
+ }
+ }
+ });
+ }
+ return collision;
+ };
+
+ useEffect(() => {
+ if (wedge && xVelocity != 0 && mode != "Review" && !kineticFriction) {
+ setKineticFriction(true);
+ //switch from static to kinetic friction
+ const normalForce: IForce = {
+ description: "Normal Force",
+ magnitude:
+ forceOfGravity.magnitude *
+ Math.cos(Math.atan(wedgeHeight / wedgeWidth)),
+ directionInDegrees:
+ 180 - 90 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI,
+ };
+ let frictionForce: IForce = {
+ description: "Kinetic Friction Force",
+ magnitude:
+ coefficientOfKineticFriction *
+ forceOfGravity.magnitude *
+ Math.cos(Math.atan(wedgeHeight / wedgeWidth)),
+ directionInDegrees:
+ 180 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI,
+ };
+ // reduce magnitude of friction force if necessary such that block cannot slide up plane
+ let yForce = -forceOfGravity.magnitude;
+ yForce +=
+ normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+ yForce +=
+ frictionForce.magnitude *
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude =
+ (-normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
+ forceOfGravity.magnitude) /
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+ if (coefficientOfKineticFriction != 0) {
+ setUpdatedForces([forceOfGravity, normalForce, frictionForce]);
+ } else {
+ setUpdatedForces([forceOfGravity, normalForce]);
+ }
+ }
+ }, [xVelocity]);
+
+ const update = () => {
+ // RK4 update
+ let xPos = xPosition;
+ let yPos = yPosition;
+ let xVel = xVelocity;
+ let yVel = yVelocity;
+ for (let i = 0; i < 60; i++) {
+ let forces1 = getNewForces(xPos, yPos, xVel, yVel);
+ const xAcc1 = getNewAccelerationX(forces1);
+ const yAcc1 = getNewAccelerationY(forces1);
+ const xVel1 = getNewVelocity(xVel, xAcc1);
+ const yVel1 = getNewVelocity(yVel, yAcc1);
+
+ let xVel2 = getNewVelocity(xVel, xAcc1 / 2);
+ let yVel2 = getNewVelocity(yVel, yAcc1 / 2);
+ let xPos2 = getNewPosition(xPos, xVel1 / 2);
+ let yPos2 = getNewPosition(yPos, yVel1 / 2);
+ const forces2 = getNewForces(xPos2, yPos2, xVel2, yVel2);
+ const xAcc2 = getNewAccelerationX(forces2);
+ const yAcc2 = getNewAccelerationY(forces2);
+ xVel2 = getNewVelocity(xVel2, xAcc2);
+ yVel2 = getNewVelocity(yVel2, yAcc2);
+ xPos2 = getNewPosition(xPos2, xVel2);
+ yPos2 = getNewPosition(yPos2, yVel2);
+
+ let xVel3 = getNewVelocity(xVel, xAcc2 / 2);
+ let yVel3 = getNewVelocity(yVel, yAcc2 / 2);
+ let xPos3 = getNewPosition(xPos, xVel2 / 2);
+ let yPos3 = getNewPosition(yPos, yVel2 / 2);
+ const forces3 = getNewForces(xPos3, yPos3, xVel3, yVel3);
+ const xAcc3 = getNewAccelerationX(forces3);
+ const yAcc3 = getNewAccelerationY(forces3);
+ xVel3 = getNewVelocity(xVel3, xAcc3);
+ yVel3 = getNewVelocity(yVel3, yAcc3);
+ xPos3 = getNewPosition(xPos3, xVel3);
+ yPos3 = getNewPosition(yPos3, yVel3);
+
+ let xVel4 = getNewVelocity(xVel, xAcc3);
+ let yVel4 = getNewVelocity(yVel, yAcc3);
+ let xPos4 = getNewPosition(xPos, xVel3);
+ let yPos4 = getNewPosition(yPos, yVel3);
+ const forces4 = getNewForces(xPos4, yPos4, xVel4, yVel4);
+ const xAcc4 = getNewAccelerationX(forces4);
+ const yAcc4 = getNewAccelerationY(forces4);
+ xVel4 = getNewVelocity(xVel4, xAcc4);
+ yVel4 = getNewVelocity(yVel4, yAcc4);
+ xPos4 = getNewPosition(xPos4, xVel4);
+ yPos4 = getNewPosition(yPos4, yVel4);
+
+ xVel +=
+ timestepSize * (xAcc1 / 6.0 + xAcc2 / 3.0 + xAcc3 / 3.0 + xAcc4 / 6.0);
+ yVel +=
+ timestepSize * (yAcc1 / 6.0 + yAcc2 / 3.0 + yAcc3 / 3.0 + yAcc4 / 6.0);
+ xPos +=
+ timestepSize * (xVel1 / 6.0 + xVel2 / 3.0 + xVel3 / 3.0 + xVel4 / 6.0);
+ yPos +=
+ timestepSize * (yVel1 / 6.0 + yVel2 / 3.0 + yVel3 / 3.0 + yVel4 / 6.0);
+ }
+
+ setXVelocity(xVel);
+ setYVelocity(yVel);
+ setXPosition(xPos);
+ setYPosition(yPos);
+ setUpdatedForces(getNewForces(xPos, yPos, xVel, yVel));
+ };
+
+ let weightStyle = {
+ backgroundColor: color,
+ borderStyle: "solid",
+ borderColor: "black",
+ position: "absolute" as "absolute",
+ left: xPosition + "px",
+ top: yPosition + "px",
+ width: 2 * radius + "px",
+ height: 2 * radius + "px",
+ borderRadius: 50 + "%",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ touchAction: "none",
+ };
+ if (dragging) {
+ weightStyle.borderColor = "lightblue";
+ }
+
+ const [clickPositionX, setClickPositionX] = useState(0);
+ const [clickPositionY, setClickPositionY] = useState(0);
+ const labelBackgroundColor = `rgba(255,255,255,0.5)`;
+
+ // Update x start position
+ useEffect(() => {
+ setUpdatedStartPosX(startPosX);
+ setXPosition(startPosX);
+ setXPosDisplay(startPosX);
+ }, [startPosX]);
+
+ // Update y start position
+ useEffect(() => {
+ setUpdatedStartPosY(startPosY);
+ setYPosition(startPosY);
+ setYPosDisplay(startPosY);
+ }, [startPosY]);
+
+ return (
+ <div style={{ zIndex: -1000 }}>
+ <div
+ className="weightContainer"
+ onPointerDown={(e) => {
+ if (draggable) {
+ e.preventDefault();
+ setPaused(true);
+ setDragging(true);
+ setClickPositionX(e.clientX);
+ setClickPositionY(e.clientY);
+ } else if (mode == "Review") {
+ setSketching(true);
+ }
+ }}
+ onPointerMove={(e) => {
+ e.preventDefault();
+ if (dragging) {
+ let newY = yPosition + e.clientY - clickPositionY;
+ if (newY > yMax - 2 * radius) {
+ newY = yMax - 2 * radius;
+ }
+
+ let newX = xPosition + e.clientX - clickPositionX;
+ if (newX > xMax - 2 * radius) {
+ newX = xMax - 2 * radius;
+ } else if (newX < 0) {
+ newX = 0;
+ }
+
+ setXPosition(newX);
+ setYPosition(newY);
+ setUpdatedStartPosX(newX);
+ setUpdatedStartPosY(newY);
+ setDisplayYPosition(
+ Math.round((yMax - 2 * radius - newY + 5) * 100) / 100
+ );
+ setClickPositionX(e.clientX);
+ setClickPositionY(e.clientY);
+ setDisplayValues();
+ }
+ }}
+ onPointerUp={(e) => {
+ if (dragging) {
+ e.preventDefault();
+ if (!pendulum) {
+ resetEverything();
+ }
+ setDragging(false);
+ let newY = yPosition + e.clientY - clickPositionY;
+ if (newY > yMax - 2 * radius) {
+ newY = yMax - 2 * radius;
+ }
+
+ let newX = xPosition + e.clientX - clickPositionX;
+ if (newX > xMax - 2 * radius) {
+ newX = xMax - 2 * radius;
+ } else if (newX < 0) {
+ newX = 0;
+ }
+ if (pendulum) {
+ const x = xMax / 2 - newX - radius;
+ const y = newY + radius + 5;
+ let angle = (Math.atan(y / x) * 180) / Math.PI;
+ if (angle < 0) {
+ angle += 180;
+ }
+ let oppositeAngle = 90 - angle;
+ if (oppositeAngle < 0) {
+ oppositeAngle = 90 - (180 - angle);
+ }
+
+ const pendulumLength = Math.sqrt(x * x + y * y);
+ setPendulumAngle(oppositeAngle);
+ setPendulumLength(Math.sqrt(x * x + y * y));
+ const mag = 9.81 * Math.cos((oppositeAngle * Math.PI) / 180);
+ const forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: angle,
+ };
+
+ setKineticFriction(false);
+ setXVelocity(startVelX ?? 0);
+ setYVelocity(startVelY ?? 0);
+ setDisplayValues();
+ setUpdatedForces([forceOfGravity, forceOfTension]);
+ }
+ }
+ }}
+ >
+ <div className="weight" style={weightStyle}>
+ <p className="weightLabel">{mass} kg</p>
+ </div>
+ </div>
+ {pendulum && (
+ <div
+ className="rod"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ zIndex: -2,
+ }}
+ >
+ <svg width={xMax + "px"} height={window.innerHeight + "px"}>
+ <line
+ x1={xPosition + radius}
+ y1={yPosition + radius}
+ x2={xMax / 2}
+ y2={-5}
+ stroke={"#deb887"}
+ strokeWidth="10"
+ />
+ </svg>
+ {!dragging && (
+ <div>
+ <p
+ style={{
+ position: "absolute",
+ zIndex: 5,
+ left: xPosition + "px",
+ top: yPosition - 70 + "px",
+ backgroundColor: labelBackgroundColor,
+ }}
+ >
+ {Math.round(pendulumLength)} m
+ </p>
+ <p
+ style={{
+ position: "absolute",
+ zIndex: -1,
+ left: xMax / 2 + "px",
+ top: 30 + "px",
+ backgroundColor: labelBackgroundColor,
+ }}
+ >
+ {Math.round(pendulumAngle * 100) / 100}°
+ </p>
+ </div>
+ )}
+ </div>
+ )}
+ {!dragging && showAcceleration && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ zIndex: -1,
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={xMax + "px"} height={window.innerHeight + "px"}>
+ <defs>
+ <marker
+ id="accArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="3"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,6 L9,3 z" fill="green" />
+ </marker>
+ </defs>
+ <line
+ x1={xPosition + radius}
+ y1={yPosition + radius}
+ x2={xPosition + radius + getNewAccelerationX(updatedForces) * 5}
+ y2={yPosition + radius + getNewAccelerationY(updatedForces) * 5}
+ stroke={"green"}
+ strokeWidth="5"
+ markerEnd="url(#accArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left:
+ xPosition +
+ radius +
+ getNewAccelerationX(updatedForces) * 5 +
+ 25 +
+ "px",
+ top:
+ yPosition +
+ radius +
+ getNewAccelerationY(updatedForces) * 5 +
+ 25 +
+ "px",
+ zIndex: -1,
+ lineHeight: 0.5,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 *
+ Math.sqrt(
+ Math.pow(getNewAccelerationX(updatedForces) * 3, 2) +
+ Math.pow(getNewAccelerationY(updatedForces) * 3, 2)
+ )
+ ) / 100}{" "}
+ m/s<sup>2</sup>
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!dragging && showVelocity && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ zIndex: -1,
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={xMax + "px"} height={window.innerHeight + "px"}>
+ <defs>
+ <marker
+ id="velArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="3"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,6 L9,3 z" fill="blue" />
+ </marker>
+ </defs>
+ <line
+ x1={xPosition + radius}
+ y1={yPosition + radius}
+ x2={xPosition + radius + xVelocity * 3}
+ y2={yPosition + radius + yVelocity * 3}
+ stroke={"blue"}
+ strokeWidth="5"
+ markerEnd="url(#velArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: xPosition + radius + xVelocity * 3 + 25 + "px",
+ top: yPosition + radius + yVelocity * 3 + "px",
+ zIndex: -1,
+ lineHeight: 0.5,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 * Math.sqrt(xVelocity * xVelocity + yVelocity * yVelocity)
+ ) / 100}{" "}
+ m/s
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!dragging &&
+ showForces &&
+ updatedForces.map((force, index) => {
+ if (force.magnitude < epsilon) {
+ return;
+ }
+ let arrowStartY: number = yPosition + radius;
+ const arrowStartX: number = xPosition + radius;
+ let arrowEndY: number =
+ arrowStartY -
+ Math.abs(force.magnitude) *
+ 20 *
+ Math.sin((force.directionInDegrees * Math.PI) / 180);
+ const arrowEndX: number =
+ arrowStartX +
+ Math.abs(force.magnitude) *
+ 20 *
+ Math.cos((force.directionInDegrees * Math.PI) / 180);
+
+ let color = "#0d0d0d";
+
+ let labelTop = arrowEndY;
+ let labelLeft = arrowEndX;
+ if (force.directionInDegrees > 90 && force.directionInDegrees < 270) {
+ labelLeft -= 120;
+ } else {
+ labelLeft += 30;
+ }
+ if (force.directionInDegrees >= 0 && force.directionInDegrees < 180) {
+ labelTop += 40;
+ } else {
+ labelTop -= 40;
+ }
+ labelTop = Math.min(labelTop, yMax + 50);
+ labelTop = Math.max(labelTop, yMin);
+ labelLeft = Math.min(labelLeft, xMax - 60);
+ labelLeft = Math.max(labelLeft, xMin);
+
+ return (
+ <div key={index}>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ zIndex: -1,
+ left: xMin,
+ top: yMin,
+ }}
+ >
+ <svg
+ width={xMax - xMin + "px"}
+ height={window.innerHeight + "px"}
+ >
+ <defs>
+ <marker
+ id="forceArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="3"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,6 L9,3 z" fill={color} />
+ </marker>
+ </defs>
+ <line
+ x1={arrowStartX}
+ y1={arrowStartY}
+ x2={arrowEndX}
+ y2={arrowEndY}
+ stroke={color}
+ strokeWidth="5"
+ markerEnd="url(#forceArrow)"
+ />
+ </svg>
+ </div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: labelLeft + "px",
+ top: labelTop + "px",
+ // zIndex: -1,
+ lineHeight: 0.5,
+ backgroundColor: labelBackgroundColor,
+ }}
+ >
+ {force.description && <p>{force.description}</p>}
+ {!force.description && <p>Force</p>}
+ {showForceMagnitudes && (
+ <p>{Math.round(100 * force.magnitude) / 100} N</p>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+};