import React = require('react'); import { IWallProps } from "./PhysicsSimulationWall"; 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; 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; xMax: number; yMax: number; xMin: number; yMin: number; } interface IState { clickPositionX: number, clickPositionY: number, dragging: boolean, kineticFriction: boolean, updatedStartPosX: number, updatedStartPosY: number, xPosition: number, yPosition: number, xVelocity: number, yVelocity: number, } export default class Weight extends React.Component { constructor(props: any) { super(props) this.state = { clickPositionX: 0, clickPositionY: 0, dragging: false, kineticFriction: false, updatedStartPosX: this.props.startPosX, updatedStartPosY: this.props.startPosY, xPosition: this.props.startPosX, yPosition: this.props.startPosY, xVelocity: this.props.startVelX ? this.props.startVelX: 0, yVelocity: this.props.startVelY ? this.props.startVelY: 0, } } // Constants draggable = !this.props.wedge; epsilon = 0.0001; forceOfGravity: IForce = { description: "Gravity", magnitude: this.props.mass * 9.81, directionInDegrees: 270, }; // Var weightStyle = { backgroundColor: this.props.color, borderStyle: "solid", borderColor: "black", position: "absolute" as "absolute", left: this.props.startPosX + "px", top: this.props.startPosY + "px", width: 2 * this.props.radius + "px", height: 2 * this.props.radius + "px", zIndex: 5, borderRadius: 50 + "%", display: "flex", justifyContent: "center", alignItems: "center", touchAction: "none", }; // Helper function to go between display and real values getDisplayYPos = (yPos: number) => { return this.props.yMax - yPos - 2 * this.props.radius + 5; }; getYPosFromDisplay = (yDisplay: number) => { return this.props.yMax - yDisplay - 2 * this.props.radius + 5; }; // Set display values based on real values setYPosDisplay = (yPos: number) => { const displayPos = this.getDisplayYPos(yPos); this.props.setDisplayYPosition(Math.round(displayPos * 100) / 100) }; setXPosDisplay = (xPos: number) => { this.props.setDisplayXPosition(Math.round(xPos * 100) / 100); }; setYVelDisplay = (yVel: number) => { this.props.setDisplayYVelocity((-1 * Math.round(yVel * 100)) / 100); }; setXVelDisplay = (xVel: number) => { this.props.setDisplayXVelocity(Math.round(xVel * 100) / 100); }; setDisplayValues = ( xPos: number = this.state.xPosition, yPos: number = this.state.yPosition, xVel: number = this.state.xVelocity, yVel: number = this.state.yVelocity ) => { this.setYPosDisplay(yPos); this.setXPosDisplay(xPos); this.setYVelDisplay(yVel); this.setXVelDisplay(xVel); this.props.setDisplayYAcceleration( (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100 ); this.props.setDisplayXAcceleration( Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100 ); }; componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { // When display values updated by user, update real values if (this.props.updateDisplay != prevProps.updateDisplay) { if (this.props.updateDisplay.xDisplay != this.state.xPosition) { let x = this.props.updateDisplay.xDisplay; x = Math.max(0, x); x = Math.min(x, this.props.xMax - 2 * this.props.radius); this.setState({updatedStartPosX: x}) this.setState({xPosition: x}) this.props.setDisplayXPosition(x); } if (this.props.updateDisplay.yDisplay != this.getDisplayYPos(this.state.yPosition)) { let y = this.props.updateDisplay.yDisplay; y = Math.max(0, y); y = Math.min(y, this.props.yMax - 2 * this.props.radius); this.props.setDisplayYPosition(y); let coordinatePosition = this.getYPosFromDisplay(y); this.setState({updatedStartPosY: coordinatePosition}) this.setState({yPosition: coordinatePosition}) } if (this.props.displayXVelocity != this.state.xVelocity) { let x = this.props.displayXVelocity; this.setState({xVelocity: x}) this.props.setDisplayXVelocity(x); } if (this.props.displayYVelocity != this.state.yVelocity) { let y = this.props.displayYVelocity; this.setState({yVelocity: -y}) this.props.setDisplayXVelocity(y); } } // Update sim if (this.props.incrementTime != prevProps.incrementTime) { if (!this.props.paused) { let collisions = false; if (!this.props.pendulum) { const collisionsWithGround = this.checkForCollisionsWithGround(); const collisionsWithWalls = this.checkForCollisionsWithWall(); collisions = collisionsWithGround || collisionsWithWalls; } if (!collisions) { this.update(); } this.setDisplayValues(); } } this.weightStyle = { backgroundColor: this.props.color, borderStyle: "solid", borderColor: this.state.dragging ? "lightblue" : "black", position: "absolute" as "absolute", left: this.state.xPosition + "px", top: this.state.yPosition + "px", width: 2 * this.props.radius + "px", height: 2 * this.props.radius + "px", borderRadius: 50 + "%", display: "flex", zIndex: 5, justifyContent: "center", alignItems: "center", touchAction: "none", }; if (this.props.reset != prevProps.reset) { this.resetEverything(); } if (this.props.startForces != prevProps.startForces) { this.setState({xVelocity: this.props.startVelX ?? 0}) this.setState({yVelocity: this.props.startVelY ?? 0}) this.setDisplayValues(); } if (this.props.adjustPendulumAngle != prevProps.adjustPendulumAngle) { // Change pendulum angle based on input field let length = this.props.adjustPendulumAngle.length; const x = length * Math.cos(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180); const y = length * Math.sin(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180); const xPos = this.props.xMax / 2 - x - this.props.radius; const yPos = y - this.props.radius - 5; this.setState({xPosition: xPos}) this.setState({yPosition: yPos}) this.setState({updatedStartPosX: xPos}) this.setState({updatedStartPosY: yPos}) this.props.setPendulumAngle(this.props.adjustPendulumAngle.angle); this.props.setPendulumLength(this.props.adjustPendulumAngle.length); } // Update x start position if (this.props.startPosX != prevProps.startPosX) { this.setState({updatedStartPosX: this.props.startPosX}) this.setState({xPosition: this.props.startPosX}) this.setXPosDisplay(this.props.startPosX); } // Update y start position if (this.props.startPosY != prevProps.startPosY) { this.setState({updatedStartPosY: this.props.startPosY}) this.setState({yPosition: this.props.startPosY}) this.setYPosDisplay(this.props.startPosY); } if (this.state.xVelocity != prevState.xVelocity) { if (this.props.wedge && this.state.xVelocity != 0 && !this.state.kineticFriction) { this.setState({kineticFriction: true}); //switch from static to kinetic friction const normalForce: IForce = { description: "Normal Force", magnitude: this.forceOfGravity.magnitude * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, }; let frictionForce: IForce = { description: "Kinetic Friction Force", magnitude: this.props.coefficientOfKineticFriction * this.forceOfGravity.magnitude * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), directionInDegrees: 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, }; // reduce magnitude of friction force if necessary such that block cannot slide up plane let yForce = -this.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) + this.forceOfGravity.magnitude) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); } if (this.props.coefficientOfKineticFriction != 0) { this.props.setUpdatedForces([this.forceOfGravity, normalForce, frictionForce]); } else { this.props.setUpdatedForces([this.forceOfGravity, normalForce]); } } } } resetEverything = () => { this.setState({kineticFriction: false}) this.setState({xPosition: this.state.updatedStartPosX}) this.setState({yPosition: this.state.updatedStartPosY}) this.setState({xVelocity: this.props.startVelX ?? 0}) this.setState({yVelocity: this.props.startVelY ?? 0}) this.props.setUpdatedForces(this.props.startForces) this.setDisplayValues(); }; getNewAccelerationX = (forceList: IForce[]) => { let newXAcc = 0; forceList.forEach((force) => { newXAcc += (force.magnitude * Math.cos((force.directionInDegrees * Math.PI) / 180)) / this.props.mass; }); return newXAcc; }; getNewAccelerationY = (forceList: IForce[]) => { let newYAcc = 0; forceList.forEach((force) => { newYAcc += (-1 * (force.magnitude * Math.sin((force.directionInDegrees * Math.PI) / 180))) / this.props.mass; }); return newYAcc; }; getNewForces = ( xPos: number, yPos: number, xVel: number, yVel: number ) => { if (!this.props.pendulum) { return this.props.updatedForces; } const x = this.props.xMax / 2 - xPos - this.props.radius; const y = yPos + this.props.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); this.props.setPendulumAngle(oppositeAngle); this.props.setPendulumLength(Math.sqrt(x * x + y * y)); const mag = this.props.mass * 9.81 * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; const forceOfTension: IForce = { description: "Tension", magnitude: mag, directionInDegrees: angle, }; return [this.forceOfGravity, forceOfTension]; }; getNewPosition = (pos: number, vel: number) => { return pos + vel * this.props.timestepSize; }; getNewVelocity = (vel: number, acc: number) => { return vel + acc * this.props.timestepSize; }; checkForCollisionsWithWall = () => { let collision = false; const minX = this.state.xPosition; const maxX = this.state.xPosition + 2 * this.props.radius; const containerWidth = 300; if (this.state.xVelocity != 0) { this.props.walls.forEach((wall) => { if (wall.angleInDegrees == 90) { const wallX = (wall.xPos / 100) * 300; if (wall.xPos < 0.35) { if (minX <= wallX) { if (this.props.elasticCollisions) { this.setState({xVelocity: -this.state.xVelocity}); } else { this.setState({xVelocity: 0}); this.setState({xPosition: wallX+5}); } collision = true; } } else { if (maxX >= wallX) { if (this.props.elasticCollisions) { this.setState({xVelocity: -this.state.xVelocity}); } else { this.setState({xVelocity: 0}); this.setState({xPosition: wallX - 2 * this.props.radius + 5}); } collision = true; } } } }); } return collision; }; checkForCollisionsWithGround = () => { let collision = false; const maxY = this.state.yPosition + 2 * this.props.radius; if (this.state.yVelocity > 0) { this.props.walls.forEach((wall) => { if (wall.angleInDegrees == 0) { const groundY = (wall.yPos / 100) * this.props.yMax; if (maxY >= groundY) { if (this.props.elasticCollisions) { this.setState({yVelocity: -this.state.yVelocity}) } else { this.setState({yVelocity: 0}) this.setState({yPosition: groundY - 2 * this.props.radius + 5}) const forceOfGravity: IForce = { description: "Gravity", magnitude: 9.81 * this.props.mass, directionInDegrees: 270, }; const normalForce: IForce = { description: "Normal force", magnitude: 9.81 * this.props.mass, directionInDegrees: wall.angleInDegrees + 90, }; this.props.setUpdatedForces([forceOfGravity, normalForce]); } collision = true; } } }); } return collision; }; update = () => { // RK4 update let xPos = this.state.xPosition; let yPos = this.state.yPosition; let xVel = this.state.xVelocity; let yVel = this.state.yVelocity; for (let i = 0; i < 60; i++) { let forces1 = this.getNewForces(xPos, yPos, xVel, yVel); const xAcc1 = this.getNewAccelerationX(forces1); const yAcc1 = this.getNewAccelerationY(forces1); const xVel1 = this.getNewVelocity(xVel, xAcc1); const yVel1 = this.getNewVelocity(yVel, yAcc1); let xVel2 = this.getNewVelocity(xVel, xAcc1 / 2); let yVel2 = this.getNewVelocity(yVel, yAcc1 / 2); let xPos2 = this.getNewPosition(xPos, xVel1 / 2); let yPos2 = this.getNewPosition(yPos, yVel1 / 2); const forces2 = this.getNewForces(xPos2, yPos2, xVel2, yVel2); const xAcc2 = this.getNewAccelerationX(forces2); const yAcc2 = this.getNewAccelerationY(forces2); xVel2 = this.getNewVelocity(xVel2, xAcc2); yVel2 = this.getNewVelocity(yVel2, yAcc2); xPos2 = this.getNewPosition(xPos2, xVel2); yPos2 = this.getNewPosition(yPos2, yVel2); let xVel3 = this.getNewVelocity(xVel, xAcc2 / 2); let yVel3 = this.getNewVelocity(yVel, yAcc2 / 2); let xPos3 = this.getNewPosition(xPos, xVel2 / 2); let yPos3 = this.getNewPosition(yPos, yVel2 / 2); const forces3 = this.getNewForces(xPos3, yPos3, xVel3, yVel3); const xAcc3 = this.getNewAccelerationX(forces3); const yAcc3 = this.getNewAccelerationY(forces3); xVel3 = this.getNewVelocity(xVel3, xAcc3); yVel3 = this.getNewVelocity(yVel3, yAcc3); xPos3 = this.getNewPosition(xPos3, xVel3); yPos3 = this.getNewPosition(yPos3, yVel3); let xVel4 = this.getNewVelocity(xVel, xAcc3); let yVel4 = this.getNewVelocity(yVel, yAcc3); let xPos4 = this.getNewPosition(xPos, xVel3); let yPos4 = this.getNewPosition(yPos, yVel3); const forces4 = this.getNewForces(xPos4, yPos4, xVel4, yVel4); const xAcc4 = this.getNewAccelerationX(forces4); const yAcc4 = this.getNewAccelerationY(forces4); xVel4 = this.getNewVelocity(xVel4, xAcc4); yVel4 = this.getNewVelocity(yVel4, yAcc4); xPos4 = this.getNewPosition(xPos4, xVel4); yPos4 = this.getNewPosition(yPos4, yVel4); xVel += this.props.timestepSize * (xAcc1 / 6.0 + xAcc2 / 3.0 + xAcc3 / 3.0 + xAcc4 / 6.0); yVel += this.props.timestepSize * (yAcc1 / 6.0 + yAcc2 / 3.0 + yAcc3 / 3.0 + yAcc4 / 6.0); xPos += this.props.timestepSize * (xVel1 / 6.0 + xVel2 / 3.0 + xVel3 / 3.0 + xVel4 / 6.0); yPos += this.props.timestepSize * (yVel1 / 6.0 + yVel2 / 3.0 + yVel3 / 3.0 + yVel4 / 6.0); } this.setState({xVelocity: xVel}); this.setState({yVelocity: yVel}); this.setState({xPosition: xPos}); this.setState({yPosition: yPos}); this.props.setUpdatedForces(this.getNewForces(xPos, yPos, xVel, yVel)); }; labelBackgroundColor = `rgba(255,255,255,0.5)`; render () { return (
{ if (this.draggable) { e.preventDefault(); this.props.setPaused(true); this.setState({dragging: true}); this.setState({clickPositionX: e.clientX}) this.setState({clickPositionY: e.clientY}) } }} onPointerMove={(e) => { e.preventDefault(); if (this.state.dragging) { let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; if (newY > this.props.yMax - 2 * this.props.radius) { newY = this.props.yMax - 2 * this.props.radius; } let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; if (newX > this.props.xMax - 2 * this.props.radius) { newX = this.props.xMax - 2 * this.props.radius; } else if (newX < 0) { newX = 0; } this.setState({xPosition: newX}) this.setState({yPosition: newY}) this.setState({updatedStartPosX: newX}) this.setState({updatedStartPosY: newY}) this.props.setDisplayYPosition(Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100) this.setState({clickPositionX: e.clientX}) this.setState({clickPositionY: e.clientY}) this.setDisplayValues(); } }} onPointerUp={(e) => { if (this.state.dragging) { e.preventDefault(); if (!this.props.pendulum) { this.resetEverything(); } this.setState({dragging: false}); let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; if (newY > this.props.yMax - 2 * this.props.radius) { newY = this.props.yMax - 2 * this.props.radius; } let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; if (newX > this.props.xMax - 2 * this.props.radius) { newX = this.props.xMax - 2 * this.props.radius; } else if (newX < 0) { newX = 0; } if (this.props.pendulum) { const x = this.props.xMax / 2 - newX - this.props.radius; const y = newY + this.props.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); this.props.setPendulumAngle(oppositeAngle); this.props.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, }; this.setState({kineticFriction: false}) this.setState({xVelocity: this.props.startVelX ?? 0}) this.setState({yVelocity: this.props.startVelY ?? 0}) this.setDisplayValues(); this.props.setUpdatedForces([this.forceOfGravity, forceOfTension]); } } }} >

{this.props.mass} kg

{this.props.pendulum && (
{!this.state.dragging && (
{/*

{Math.round(this.props.pendulumLength)} m

*/}

{Math.round(this.props.pendulumAngle * 100) / 100}°

)}
)} {!this.state.dragging && this.props.showAcceleration && (

{Math.round( 100 * Math.sqrt( Math.pow(this.getNewAccelerationX(this.props.updatedForces) * 3, 2) + Math.pow(this.getNewAccelerationY(this.props.updatedForces) * 3, 2) ) ) / 100}{" "} m/s2

)} {!this.state.dragging && this.props.showVelocity && (

{Math.round( 100 * Math.sqrt(this.state.xVelocity**2 + this.state.yVelocity**2) ) / 100}{" "} m/s

)} {!this.state.dragging && this.props.showForces && this.props.updatedForces.map((force, index) => { if (force.magnitude < this.epsilon) { return; } let arrowStartY: number = this.state.yPosition + this.props.radius; const arrowStartX: number = this.state.xPosition + this.props.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, this.props.yMax + 50); labelTop = Math.max(labelTop, this.props.yMin); labelLeft = Math.min(labelLeft, this.props.xMax - 60); labelLeft = Math.max(labelLeft, this.props.xMin); return (
{force.description &&

{force.description}

} {!force.description &&

Force

} {this.props.showForceMagnitudes && (

{Math.round(100 * force.magnitude) / 100} N

)}
); })}
); } };