import { Doc } from '../../../fields/Doc'; import React = require('react'); import { useEffect, useState } from "react"; import { IWallProps } from "./Wall"; import "./Weight.scss"; export interface IForce { description: string; magnitude: number; directionInDegrees: number; component: boolean; } export interface IWeightProps { dataDoc: doc; adjustPendulumAngle: { angle: number; length: number }; circularMotionRadius: number; coefficientOfKineticFriction: number; color: string; componentForces: IForce[]; displayXVelocity: number; displayYVelocity: number; elasticCollisions: boolean; gravity: number; incrementTime: number; mass: number; mode: string; noMovement: boolean; paused: boolean; pendulumAngle: number; pendulumLength: number; radius: number; reset: boolean; showAcceleration: boolean; showComponentForces: boolean; showForceMagnitudes: boolean; showForces: boolean; showVelocity: boolean; simulationSpeed: number; simulationType: string; springConstant: number; springRestLength: number; springStartLength: number; startForces: IForce[]; startPendulumAngle: number; startPosX: number; startPosY: number; startVelX: number; startVelY: number; timestepSize: number; updateDisplay: { xDisplay: number; yDisplay: number }; updatedForces: IForce[]; wedgeHeight: number; wedgeWidth: number; xMax: number; xMin: number; yMax: number; yMin: number; } interface IState { angleLabel: number, clickPositionX: number, clickPositionY: number, coordinates: string, dragging: boolean, kineticFriction: boolean, maxPosYConservation: number, timer: number, update: boolean, updatedStartPosX: number, updatedStartPosY: number, walls: IWallProps[], xPosition: number, xVelocity: number, yPosition: number, yVelocity: number, xAccel: number, yAccel: number, } export default class Weight extends React.Component { constructor(props: any) { super(props) this.state = { angleLabel: 0, clickPositionX: 0, clickPositionY: 0, coordinates: "", dragging: false, kineticFriction: false, maxPosYConservation: 0, timer: 0, updatedStartPosX: this.props.dataDoc['startPosX'], updatedStartPosY: this.props.dataDoc['startPosY'], walls: [], xPosition: this.props.dataDoc['startPosX'], xVelocity: this.props.startVelX ? this.props.startVelX: 0, yPosition: this.props.dataDoc['startPosY'], yVelocity: this.props.startVelY ? this.props.startVelY: 0, xAccel: 0, yAccel: 0, } } componentDidMount() { // Timer for animating the simulation setInterval(() => { this.setState({timer: this.state.timer + 1}); }, 50); } // Constants const draggable = this.props.dataDoc['simulationType'] != "Inclined Plane" && this.props.dataDoc['simulationType'] != "Pendulum" && this.props.dataDoc['mode'] == "Freeform"; const epsilon = 0.0001; const labelBackgroundColor = `rgba(255,255,255,0.5)`; // Variables weightStyle = { alignItems: "center", backgroundColor: this.props.color, borderColor: "black", borderRadius: 50 + "%", borderStyle: "solid", display: "flex", height: 2 * this.props.radius + "px", justifyContent: "center", left: this.props.dataDoc['startPosX'] + "px", position: "absolute" as "absolute", top: this.props.dataDoc['startPosY'] + "px", touchAction: "none", width: 2 * this.props.radius + "px", zIndex: 5, }; // 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.dataDoc['positionYDisplay'] = Math.round(displayPos * 100) / 100 }; setXPosDisplay = (xPos: number) => { this.props.dataDoc['positionXDisplay'] = Math.round(xPos * 100) / 100; }; setYVelDisplay = (yVel: number) => { this.props.dataDoc['velocityYDisplay'] = (-1 * Math.round(yVel * 100)) / 100; }; setXVelDisplay = (xVel: number) => { this.props.dataDoc['velocityXDisplay'] = Math.round(xVel * 100) / 100; }; // Update display values when simulation updates 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.dataDoc['accelerationYDisplay'] = (-1 * Math.round(this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 100)) / 100 ; this.props.dataDoc['accelerationXDisplay'] = Math.round(this.getNewAccelerationX(this.props.dataDoc['updatedForces']) * 100) / 100 ; }; componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { } // Reset simulation on reset button click 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.dataDoc['pendulumAngle'] = this.props.dataDoc['startPendulumAngle'] this.props.dataDoc['updatedForces'] = (this.props.dataDoc['startForces']) this.props.dataDoc['accelerationXDisplay'] = 0 this.props.dataDoc['accelerationYDisplay'] = 0 this.props.dataDoc['positionXDisplay'] = this.state.updatedStartPosX this.props.dataDoc['positionYDisplay'] = this.state.updatedStartPosY this.props.dataDoc['velocityXDisplay'] = this.props.startVelX ?? 0 this.props.dataDoc['velocityYDisplay'] = this.props.startVelY ?? 0 this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle']* 100) / 100}) }; // Compute x acceleration from forces, F=ma getNewAccelerationX = (forceList: IForce[]) => { let newXAcc = 0; if (forceList) { forceList.forEach((force) => { newXAcc += (force.magnitude * Math.cos((force.directionInDegrees * Math.PI) / 180)) / this.props.mass; }); } return newXAcc; }; // Compute y acceleration from forces, F=ma getNewAccelerationY = (forceList: IForce[]) => { let newYAcc = 0; if (forceList) { forceList.forEach((force) => { newYAcc += (-1 * (force.magnitude * Math.sin((force.directionInDegrees * Math.PI) / 180))) / this.props.mass; }); } return newYAcc; }; // Compute uniform circular motion forces given x, y positions getNewCircularMotionForces = (xPos: number, yPos: number) => { let deltaX = (this.props.xMin + this.props.xMax) / 2 - (xPos + this.props.radius); let deltaY = yPos + this.props.radius - (this.props.yMin + this.props.yMax) / 2; let dir = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; const tensionForce: IForce = { description: "Centripetal Force", magnitude: (this.props.startVelX ** 2 * this.props.mass) / this.props.circularMotionRadius, directionInDegrees: dir, component: false, }; return [tensionForce]; }; // Compute spring forces given y position getNewSpringForces = (yPos: number) => { let springForce: IForce = { description: "Spring Force", magnitude: 0, directionInDegrees: 90, component: false, }; if (yPos - this.props.springRestLength > 0) { springForce = { description: "Spring Force", magnitude: this.props.springConstant * (yPos - this.props.springRestLength), directionInDegrees: 90, component: false, }; } else if (yPos - this.props.springRestLength < 0) { springForce = { description: "Spring Force", magnitude: this.props.springConstant * (this.props.springRestLength - yPos), directionInDegrees: 270, component: false, }; } return [ { description: "Gravity", magnitude: Math.abs(this.props.gravity) * this.props.mass, directionInDegrees: 270, component: false, }, springForce, ]; }; // Compute pendulum forces given position, velocity getNewPendulumForces = ( xPos: number, yPos: number, xVel: number, yVel: number ) => { 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.dataDoc['pendulumAngle'] = oppositeAngle; 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, component: false, }; return [ { description: "Gravity", magnitude: Math.abs(this.props.gravity) * this.props.mass, directionInDegrees: 270, component: false, }, forceOfTension, ]; }; // Check for collisions in x direction const checkForCollisionsWithWall = () => { let collision = false; const minX = this.state.xPosition; const maxX = this.state.xPosition + 2 * this.props.radius; if (this.state.xVelocity != 0) { this.state.walls.forEach((wall) => { if (wall.angleInDegrees == 90) { const wallX = (wall.xPos / 100) * window.innerWidth; if (wall.xPos < 0.35) { if (minX <= wallX) { this.setState({xPosition: wallX+0.01}); if (this.props.elasticCollisions) { this.setState({xVelocity: -this.state.xVelocity}); } else { this.setState({xVelocity: 0}); } collision = true; } } else { if (maxX >= wallX) { this.setState({xPosition: wallX- 2 * this.props.radius-0.01}); if (this.props.elasticCollisions) { this.setState({xVelocity: -this.state.xVelocity}); } else { this.setState({xVelocity: 0}); } collision = true; } } } }); } return collision; }; // Check for collisions in y direction const checkForCollisionsWithGround = () => { let collision = false; const minY = this.state.yPosition; const maxY = this.state.yPosition + 2 * this.props.radius; if (this.state.yVelocity > 0) { this.state.walls.forEach((wall) => { if (wall.angleInDegrees == 0 && wall.yPos > 0.4) { const groundY = (wall.yPos / 100) * window.innerHeight; if (maxY > groundY) { this.setState({yPosition: groundY- 2 * this.props.radius-0.01}); if (this.props.elasticCollisions) { this.setState({yVelocity: -this.state.yVelocity}); } else { this.setState({yVelocity: 0}); if (this.props.dataDoc['simulationType'] != "Two Weights") { const forceOfGravity: IForce = { description: "Gravity", magnitude: Math.abs(this.props.gravity) * this.props.mass, directionInDegrees: 270, component: false, }; const normalForce: IForce = { description: "Normal force", magnitude: Math.abs(this.props.gravity) * this.props.mass, directionInDegrees: wall.angleInDegrees + 90, component: false, }; this.props.dataDoc['updatedForces'] = ([forceOfGravity, normalForce]); if (this.props.dataDoc['simulationType'] == "Inclined Plane") { const forceOfGravityC: IForce = { description: "Gravity", magnitude: Math.abs(this.props.gravity) * this.props.mass, directionInDegrees: 270, component: true, }; const normalForceC: IForce = { description: "Normal force", magnitude: Math.abs(this.props.gravity) * this.props.mass, directionInDegrees: wall.angleInDegrees + 90, component: true, }; this.props.dataDoc['componentForces'] = ([forceOfGravityC, normalForceC]); } } } collision = true; } } }); } if (this.state.yVelocity < 0) { this.state.walls.forEach((wall) => { if (wall.angleInDegrees == 0 && wall.yPos < 0.4) { const groundY = (wall.yPos / 100) * window.innerHeight; if (minY < groundY) { this.setState({yPosition: groundY + 0.01}); if (this.props.elasticCollisions) { this.setState({yVelocity: -this.state.yVelocity}); } else { this.setState({yVelocity: 0}); } collision = true; } } }); } return collision; }; // Called at each RK4 step const evaluate = ( currentXPos: number, currentYPos: number, currentXVel: number, currentYVel: number, deltaXPos: number, deltaYPos: number, deltaXVel: number, deltaYVel: number, dt: number ) => { const newXPos = currentXPos + deltaXPos * dt; const newYPos = currentYPos + deltaYPos * dt; const newXVel = currentXVel + deltaXVel * dt; const newYVel = currentYVel + deltaYVel * dt; const newDeltaXPos = newXVel; const newDeltaYPos = newYVel; let forces = updatedForces; if (this.props.dataDoc['simulationType'] == "Pendulum") { forces = getNewPendulumForces(newXPos, newYPos, newXVel, newYVel); } else if (this.props.dataDoc['simulationType'] == "Spring") { forces = getNewSpringForces(newYPos); } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { forces = getNewCircularMotionForces(newXPos, newYPos); } const newDeltaXVel = getNewAccelerationX(forces); const newDeltaYVel = getNewAccelerationY(forces); return { xPos: newXPos, yPos: newYPos, xVel: newXVel, yVel: newYVel, deltaXPos: newDeltaXPos, deltaYPos: newDeltaYPos, deltaXVel: newDeltaXVel, deltaYVel: newDeltaYVel, }; }; // Update position, velocity using RK4 method const update = () => { let startXVel = xVelocity; let startYVel = yVelocity; let xPos = xPosition; let yPos = yPosition; let xVel = xVelocity; let yVel = yVelocity; let forces = updatedForces; if (this.props.dataDoc['simulationType'] == "Pendulum") { forces = getNewPendulumForces(xPos, yPos, xVel, yVel); } else if (this.props.dataDoc['simulationType'] == "Spring") { forces = getNewSpringForces(yPos); } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { forces = getNewCircularMotionForces(xPos, yPos); } const xAcc = getNewAccelerationX(forces); const yAcc = getNewAccelerationY(forces); for (let i = 0; i < simulationSpeed; i++) { const k1 = evaluate(xPos, yPos, xVel, yVel, xVel, yVel, xAcc, yAcc, 0); const k2 = evaluate( xPos, yPos, xVel, yVel, k1.deltaXPos, k1.deltaYPos, k1.deltaXVel, k1.deltaYVel, timestepSize * 0.5 ); const k3 = evaluate( xPos, yPos, xVel, yVel, k2.deltaXPos, k2.deltaYPos, k2.deltaXVel, k2.deltaYVel, timestepSize * 0.5 ); const k4 = evaluate( xPos, yPos, xVel, yVel, k3.deltaXPos, k3.deltaYPos, k3.deltaXVel, k3.deltaYVel, timestepSize ); xVel += ((timestepSize * 1.0) / 6.0) * (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel); yVel += ((timestepSize * 1.0) / 6.0) * (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel); xPos += ((timestepSize * 1.0) / 6.0) * (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos); yPos += ((timestepSize * 1.0) / 6.0) * (k1.deltaYPos + 2 * (k2.deltaYPos + k3.deltaYPos) + k4.deltaYPos); } // make sure harmonic motion maintained and errors don't propagate if (this.props.dataDoc['simulationType'] == "Spring") { if (startYVel < 0 && yVel > 0 && yPos < springRestLength) { let equilibriumPos = springRestLength + (mass * Math.abs(gravity)) / springConstant; let amplitude = Math.abs(equilibriumPos - springStartLength); yPos = equilibriumPos - amplitude; } else if (startYVel > 0 && yVel < 0 && yPos > springRestLength) { let equilibriumPos = springRestLength + (mass * Math.abs(gravity)) / springConstant; let amplitude = Math.abs(equilibriumPos - springStartLength); yPos = equilibriumPos + amplitude; } } if (this.props.dataDoc['simulationType'] == "Pendulum") { let startX = updatedStartPosX; if (startXVel <= 0 && xVel > 0) { xPos = updatedStartPosX; if (updatedStartPosX > xMax / 2) { xPos = xMax / 2 + (xMax / 2 - startX) - 2 * radius; } yPos = startPosY; } else if (startXVel >= 0 && xVel < 0) { xPos = updatedStartPosX; if (updatedStartPosX < xMax / 2) { xPos = xMax / 2 + (xMax / 2 - startX) - 2 * radius; } yPos = startPosY; } } if (this.props.dataDoc['simulationType'] == "One Weight") { if (yPos < maxPosYConservation) { yPos = maxPosYConservation; } } setXVelocity(xVel); setYVelocity(yVel); setXPosition(xPos); setYPosition(yPos); let forcesn = updatedForces; if (this.props.dataDoc['simulationType'] == "Pendulum") { forcesn = getNewPendulumForces(xPos, yPos, xVel, yVel); } else if (this.props.dataDoc['simulationType'] == "Spring") { forcesn = getNewSpringForces(yPos); } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { forcesn = getNewCircularMotionForces(xPos, yPos); } setUpdatedForces(forcesn); // set component forces if they change if (this.props.dataDoc['simulationType'] == "Pendulum") { let x = xMax / 2 - xPos - radius; let 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); const mag = mass * Math.abs(gravity) * Math.cos((oppositeAngle * Math.PI) / 180) + (mass * (xVel * xVel + yVel * yVel)) / pendulumLength; const tensionComponent: IForce = { description: "Tension", magnitude: mag, directionInDegrees: angle, component: true, }; const gravityParallel: IForce = { description: "Gravity Parallel Component", magnitude: Math.abs(gravity) * Math.cos(((90 - angle) * Math.PI) / 180), directionInDegrees: 270 - (90 - angle), component: true, }; const gravityPerpendicular: IForce = { description: "Gravity Perpendicular Component", magnitude: Math.abs(gravity) * Math.sin(((90 - angle) * Math.PI) / 180), directionInDegrees: -(90 - angle), component: true, }; if (Math.abs(gravity) * Math.sin(((90 - angle) * Math.PI) / 180) < 0) { gravityPerpendicular.magnitude = Math.abs( Math.abs(gravity) * Math.sin(((90 - angle) * Math.PI) / 180) ); gravityPerpendicular.directionInDegrees = 180 - (90 - angle); } setComponentForces([ tensionComponent, gravityParallel, gravityPerpendicular, ]); } }; // 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]); // 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]); // Prevent bug when switching between sims useEffect(() => { setXVelocity(startVelX); setYVelocity(startVelY); setDisplayValues(); }, [startForces]); // Make sure weight doesn't go above max height useEffect(() => { if (this.props.dataDoc['simulationType'] == "One Weight") { let maxYPos = updatedStartPosY; if (startVelY != 0) { maxYPos -= (startVelY * startVelY) / (2 * Math.abs(gravity)); } if (maxYPos < 0) { maxYPos = 0; } setMaxPosYConservation(maxYPos); } }, [updatedStartPosY, startVelY]); // Check for collisions and update useEffect(() => { if (!paused && !noMovement) { let collisions = false; if ( this.props.dataDoc['simulationType'] == "One Weight" || this.props.dataDoc['simulationType'] == "Inclined Plane" ) { const collisionsWithGround = checkForCollisionsWithGround(); const collisionsWithWalls = checkForCollisionsWithWall(); collisions = collisionsWithGround || collisionsWithWalls; } if (this.props.dataDoc['simulationType'] == "Pulley") { if (yPosition <= yMin + 100 || yPosition >= yMax - 100) { collisions = true; } } if (!collisions) { update(); } setDisplayValues(); } }, [incrementTime]); // Reset everything on reset button click useEffect(() => { resetEverything(); }, [reset]); // Convert from static to kinetic friction if/when weight slips on inclined plane useEffect(() => { if ( this.props.dataDoc['simulationType'] == "Inclined Plane" && Math.abs(xVelocity) > 0.1 && this.props.dataDoc['mode'] != "Review" && !kineticFriction ) { setKineticFriction(true); //switch from static to kinetic friction const normalForce: IForce = { description: "Normal Force", magnitude: mass * Math.abs(gravity) * Math.cos(Math.atan(wedgeHeight / wedgeWidth)), directionInDegrees: 180 - 90 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI, component: false, }; let frictionForce: IForce = { description: "Kinetic Friction Force", magnitude: mass * coefficientOfKineticFriction * Math.abs(gravity) * Math.cos(Math.atan(wedgeHeight / wedgeWidth)), directionInDegrees: 180 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI, component: false, }; // reduce magnitude of friction force if necessary such that block cannot slide up plane let yForce = -Math.abs(gravity); 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) + Math.abs(gravity)) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); } const frictionForceComponent: IForce = { description: "Kinetic Friction Force", magnitude: mass * coefficientOfKineticFriction * Math.abs(gravity) * Math.cos(Math.atan(wedgeHeight / wedgeWidth)), directionInDegrees: 180 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI, component: true, }; const normalForceComponent: IForce = { description: "Normal Force", magnitude: mass * Math.abs(gravity) * Math.cos(Math.atan(wedgeHeight / wedgeWidth)), directionInDegrees: 180 - 90 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI, component: true, }; const gravityParallel: IForce = { description: "Gravity Parallel Component", magnitude: mass * Math.abs(gravity) * Math.sin(Math.PI / 2 - Math.atan(wedgeHeight / wedgeWidth)), directionInDegrees: 180 - 90 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI + 180, component: true, }; const gravityPerpendicular: IForce = { description: "Gravity Perpendicular Component", magnitude: mass * Math.abs(gravity) * Math.cos(Math.PI / 2 - Math.atan(wedgeHeight / wedgeWidth)), directionInDegrees: 360 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI, component: true, }; const gravityForce: IForce = { description: "Gravity", magnitude: mass * Math.abs(gravity), directionInDegrees: 270, component: false, }; if (coefficientOfKineticFriction != 0) { setUpdatedForces([gravityForce, normalForce, frictionForce]); setComponentForces([ frictionForceComponent, normalForceComponent, gravityParallel, gravityPerpendicular, ]); } else { setUpdatedForces([gravityForce, normalForce]); setComponentForces([ normalForceComponent, gravityParallel, gravityPerpendicular, ]); } } }, [xVelocity]); // Add/remove walls when simulation type changes useEffect(() => { let w: IWallProps[] = []; if (this.props.dataDoc['simulationType'] == "One Weight" || this.props.dataDoc['simulationType'] == "Inclined Plane") { w.push({ length: 70, xPos: 0, yPos: 0, angleInDegrees: 0 }); w.push({ length: 70, xPos: 0, yPos: 80, angleInDegrees: 0 }); w.push({ length: 80, xPos: 0, yPos: 0, angleInDegrees: 90 }); w.push({ length: 80, xPos: 69.5, yPos: 0, angleInDegrees: 90 }); } setWalls(w); }, [this.props.dataDoc['simulationType']]); // Update x position when start pos x changes useEffect(() => { if (paused) { setUpdatedStartPosX(startPosX); setXPosition(startPosX); setXPosDisplay(startPosX); } }, [startPosX]); // Update y position when start pos y changes useEffect(() => { if (paused) { setUpdatedStartPosY(startPosY); setYPosition(startPosY); setYPosDisplay(startPosY); } }, [startPosY]); // Update wedge coordinates useEffect(() => { const left = xMax * 0.5 - 200; const coordinatePair1 = Math.round(left) + "," + yMax + " "; const coordinatePair2 = Math.round(left + wedgeWidth) + "," + yMax + " "; const coordinatePair3 = Math.round(left) + "," + (yMax - wedgeHeight); const coord = coordinatePair1 + coordinatePair2 + coordinatePair3; setCoordinates(coord); }, [wedgeWidth, wedgeHeight]); // Render weight, spring, rod(s), vectors return (
{ if (draggable) { e.preventDefault(); setPaused(true); setDragging(true); setClickPositionX(e.clientX); setClickPositionY(e.clientY); } else if (this.props.dataDoc['mode'] == "Review") { setSketching(true); } }} onPointerMove={(e) => { e.preventDefault(); if (dragging) { let newY = yPosition + e.clientY - clickPositionY; if (newY > yMax - 2 * radius - 10) { newY = yMax - 2 * radius - 10; } else if (newY < 10) { newY = 10; } let newX = xPosition + e.clientX - clickPositionX; if (newX > xMax - 2 * radius - 10) { newX = xMax - 2 * radius - 10; } else if (newX < 10) { newX = 10; } if (this.props.dataDoc['simulationType'] == "Suspension") { if (newX < (xMax + xMin) / 4 - radius - 15) { newX = (xMax + xMin) / 4 - radius - 15; } else if (newX > (3 * (xMax + xMin)) / 4 - radius / 2 - 15) { newX = (3 * (xMax + xMin)) / 4 - radius / 2 - 15; } } setYPosition(newY); setDisplayYPosition( Math.round((yMax - 2 * radius - newY + 5) * 100) / 100 ); if (this.props.dataDoc['simulationType'] != "Pulley") { setXPosition(newX); setDisplayXPosition(newX); } if (this.props.dataDoc['simulationType'] != "Suspension") { if (this.props.dataDoc['simulationType'] != "Pulley") { setUpdatedStartPosX(newX); } setUpdatedStartPosY(newY); } setClickPositionX(e.clientX); setClickPositionY(e.clientY); setDisplayValues(); } }} onPointerUp={(e) => { if (dragging) { e.preventDefault(); if ( this.props.dataDoc['simulationType'] != "Pendulum" && this.props.dataDoc['simulationType'] != "Suspension" ) { resetEverything(); } setDragging(false); let newY = yPosition + e.clientY - clickPositionY; if (newY > yMax - 2 * radius - 10) { newY = yMax - 2 * radius - 10; } else if (newY < 10) { newY = 10; } let newX = xPosition + e.clientX - clickPositionX; if (newX > xMax - 2 * radius - 10) { newX = xMax - 2 * radius - 10; } else if (newX < 10) { newX = 10; } if (this.props.dataDoc['simulationType'] == "Spring") { setSpringStartLength(newY); } if (this.props.dataDoc['simulationType'] == "Suspension") { let x1rod = (xMax + xMin) / 2 - radius - yMin - 200; let x2rod = (xMax + xMin) / 2 + yMin + 200 + radius; let deltaX1 = xPosition + radius - x1rod; let deltaX2 = x2rod - (xPosition + radius); let deltaY = yPosition + radius; let dir1T = Math.PI - Math.atan(deltaY / deltaX1); let dir2T = Math.atan(deltaY / deltaX2); let tensionMag2 = (mass * Math.abs(gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); dir1T = (dir1T * 180) / Math.PI; dir2T = (dir2T * 180) / Math.PI; const tensionForce1: IForce = { description: "Tension", magnitude: tensionMag1, directionInDegrees: dir1T, component: false, }; const tensionForce2: IForce = { description: "Tension", magnitude: tensionMag2, directionInDegrees: dir2T, component: false, }; const grav: IForce = { description: "Gravity", magnitude: mass * Math.abs(gravity), directionInDegrees: 270, component: false, }; setUpdatedForces([tensionForce1, tensionForce2, grav]); } } }} >

{mass} kg

{this.props.dataDoc['simulationType'] == "Spring" && (
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((val) => { const count = 10; let xPos1; let yPos1; let xPos2; let yPos2; if (val % 2 == 0) { xPos1 = xPosition + radius - 20; xPos2 = xPosition + radius + 20; } else { xPos1 = xPosition + radius + 20; xPos2 = xPosition + radius - 20; } yPos1 = (val * yPosition) / count; yPos2 = ((val + 1) * yPosition) / count; return ( ); })}
)} {this.props.dataDoc['simulationType'] == "Pulley" && (
)} {this.props.dataDoc['simulationType'] == "Pulley" && (
)} {this.props.dataDoc['simulationType'] == "Suspension" && (

{Math.round( ((Math.atan( (yPosition + radius) / (xPosition + radius - ((xMax + xMin) / 2 - radius - yMin - 200)) ) * 180) / Math.PI) * 100 ) / 100} °

)} {this.props.dataDoc['simulationType'] == "Suspension" && (

{Math.round( ((Math.atan( (yPosition + radius) / ((xMax + xMin) / 2 + yMin + 200 + radius - (xPosition + radius)) ) * 180) / Math.PI) * 100 ) / 100} °

)} {this.props.dataDoc['simulationType'] == "Circular Motion" && (
)} {this.props.dataDoc['simulationType'] == "Pendulum" && (
{!dragging && (

{Math.round(pendulumLength)} m

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

)}
)} {this.props.dataDoc['simulationType'] == "Inclined Plane" && (

{Math.round( ((Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI) * 100 ) / 100} °

)} {!dragging && showAcceleration && (

{Math.round( 100 * Math.sqrt(xAccel * xAccel + yAccel * yAccel) ) / 100}{" "} m/s 2

)} {!dragging && showVelocity && (

{Math.round( 100 * Math.sqrt( displayXVelocity * displayXVelocity + displayYVelocity * displayYVelocity ) ) / 100}{" "} m/s

)} {!dragging && showComponentForces && componentForces.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 (
{force.component == true && ( )} {force.component == false && ( )}
{force.description &&

{force.description}

} {!force.description &&

Force

} {showForceMagnitudes && (

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

)}
); })} {!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 (
{force.component == true && ( )} {force.component == false && ( )}
{force.description &&

{force.description}

} {!force.description &&

Force

} {showForceMagnitudes && (

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

)}
); })}
); };