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 (
{mass} kg
{Math.round(pendulumLength)} m
{Math.round(pendulumAngle * 100) / 100}°
{Math.round( 100 * Math.sqrt( Math.pow(getNewAccelerationX(updatedForces) * 3, 2) + Math.pow(getNewAccelerationY(updatedForces) * 3, 2) ) ) / 100}{" "} m/s2
{Math.round( 100 * Math.sqrt(xVelocity * xVelocity + yVelocity * yVelocity) ) / 100}{" "} m/s
{force.description}
} {!force.description &&Force
} {showForceMagnitudes && ({Math.round(100 * force.magnitude) / 100} N
)}