import { computed, IReactionDisposer, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import './PhysicsSimulationBox.scss'; import * as React from 'react'; interface IWallProps { length: number; xPos: number; yPos: number; angleInDegrees: number; } interface IForce { description: string; magnitude: number; directionInDegrees: number; } export interface IWeightProps { pause: () => void; panelWidth: () => number; panelHeight: () => number; resetRequest: () => number; circularMotionRadius: number; coefficientOfKineticFriction: number; color: string; componentForces: () => IForce[]; setComponentForces: (x: IForce[]) => {}; displayXVelocity: number; displayYVelocity: number; elasticCollisions: boolean; gravity: number; mass: number; simulationMode: string; noMovement: boolean; paused: boolean; pendulumAngle: number; pendulumLength: number; radius: number; showAcceleration: boolean; showComponentForces: boolean; showForceMagnitudes: boolean; showForces: boolean; showVelocity: boolean; simulationSpeed: number; simulationType: string; springConstant: number; springRestLength: number; springStartLength: number; startForces: () => IForce[]; startPendulumAngle: number; startPendulumLength: number; startPosX: number; startPosY: number; startVelX: number; startVelY: number; timestepSize: number; updateMassPosX: number; updateMassPosY: number; forcesUpdated: () => IForce[]; setForcesUpdated: (x: IForce[]) => {}; setPosition: (x: number | undefined, y: number | undefined) => void; setVelocity: (x: number | undefined, y: number | undefined) => void; setAcceleration: (x: number, y: number) => void; setPendulumAngle: (ang: number | undefined, length: number | undefined) => void; setSpringLength: (length: number) => void; wallPositions: IWallProps[]; 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; updatedStartPosX: any; updatedStartPosY: any; xPosition: number; xVelocity: number; yPosition: number; yVelocity: number; xAccel: number; yAccel: number; } @observer export default class Weight extends React.Component { constructor(props: any) { super(props); makeObservable(this); this.state = { angleLabel: 0, clickPositionX: 0, clickPositionY: 0, coordinates: '', dragging: false, kineticFriction: false, maxPosYConservation: 0, timer: 0, updatedStartPosX: this.props.startPosX, updatedStartPosY: this.props.startPosY, xPosition: this.props.startPosX, xVelocity: this.props.startVelX, yPosition: this.props.startPosY, yVelocity: this.props.startVelY, xAccel: 0, yAccel: 0, }; } _timer: NodeJS.Timeout | undefined; _resetDisposer: IReactionDisposer | undefined; componentWillUnmount() { this._timer && clearTimeout(this._timer); this._resetDisposer?.(); } componentWillUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): void { if (nextProps.paused) { this._timer && clearTimeout(this._timer); this._timer = undefined; } else if (this.props.paused) { this._timer && clearTimeout(this._timer); this._timer = setInterval(() => this.setState({ timer: this.state.timer + 1 }), 50); } } // Constants @computed get draggable() { return !['Inclined Plane', 'Pendulum'].includes(this.props.simulationType) && this.props.simulationMode === 'Freeform'; } @computed get panelHeight() { return Math.max(800, this.props.panelHeight()) + 'px'; } @computed get panelWidth() { return Math.max(1000, this.props.panelWidth()) + 'px'; } @computed get walls() { return ['One Weight', 'Inclined Plane'].includes(this.props.simulationType) ? this.props.wallPositions : []; } epsilon = 0.0001; 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.startPosX + 'px', position: 'absolute' as 'absolute', top: this.props.startPosY + 'px', touchAction: 'none', width: 2 * this.props.radius + 'px', zIndex: 5, }; // Helper function to go between display and real values getDisplayYPos = (yPos: number) => this.props.yMax - yPos - 2 * this.props.radius + 5; gravityForce = (): IForce => ({ description: 'Gravity', magnitude: this.props.mass * this.props.gravity, directionInDegrees: 270, }); // 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.props.setPosition(xPos, this.getDisplayYPos(yPos)); this.props.setVelocity(xVel, yVel); const xAccel = Math.round(this.getNewAccelerationX(this.props.forcesUpdated()) * 100) / 100; const yAccel = (-1 * Math.round(this.getNewAccelerationY(this.props.forcesUpdated()) * 100)) / 100; this.props.setAcceleration(xAccel, yAccel); this.setState({ xAccel, yAccel }); }; componentDidMount() { this._resetDisposer = reaction(() => this.props.resetRequest(), this.resetEverything); } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { if (prevProps.simulationType != this.props.simulationType) { this.setState({ xVelocity: this.props.startVelX, yVelocity: this.props.startVelY }); this.setDisplayValues(); } // Change pendulum angle from input field if (prevProps.startPendulumAngle != this.props.startPendulumAngle || prevProps.startPendulumLength !== this.props.startPendulumLength) { const length = this.props.startPendulumLength; const x = length * Math.cos(((90 - this.props.startPendulumAngle) * Math.PI) / 180); const y = length * Math.sin(((90 - this.props.startPendulumAngle) * Math.PI) / 180); const xPosition = this.props.xMax / 2 - x - this.props.radius; const yPosition = y - this.props.radius - 5; this.setState({ xPosition, yPosition, updatedStartPosX: xPosition, updatedStartPosY: yPosition }); this.props.setPendulumAngle(this.props.startPendulumAngle, this.props.startPendulumLength); } // When display values updated by user, update real value if (prevProps.updateMassPosX !== this.props.updateMassPosX) { const x = Math.min(Math.max(0, this.props.updateMassPosX), this.props.xMax - 2 * this.props.radius); this.setState({ updatedStartPosX: x, xPosition: x }); this.props.setPosition(x, undefined); } if (prevProps.updateMassPosY != this.props.updateMassPosY) { const y = Math.min(Math.max(0, this.props.updateMassPosY), this.props.yMax - 2 * this.props.radius); const coordinatePosition = this.getDisplayYPos(y); this.setState({ yPosition: coordinatePosition, updatedStartPosY: coordinatePosition }); this.props.setPosition(undefined, this.getDisplayYPos(y)); if (this.props.displayXVelocity != this.state.xVelocity) { this.setState({ xVelocity: this.props.displayXVelocity }); this.props.setVelocity(this.props.displayXVelocity, undefined); } if (this.props.displayYVelocity != -this.state.yVelocity) { this.setState({ yVelocity: -this.props.displayYVelocity }); this.props.setVelocity(undefined, this.props.displayYVelocity); } } // Make sure weight doesn't go above max height if ((prevState.updatedStartPosY != this.state.updatedStartPosY || prevProps.startVelY != this.props.startVelY) && !isNaN(this.state.updatedStartPosY) && !isNaN(this.props.startVelY)) { if (this.props.simulationType == 'One Weight') { let maxYPos = this.state.updatedStartPosY; if (this.props.startVelY != 0) { maxYPos -= (this.props.startVelY * this.props.startVelY) / (2 * this.props.gravity); } if (maxYPos < 0) maxYPos = 0; this.setState({ maxPosYConservation: maxYPos }); } } // Check for collisions and update if (!this.props.paused && !this.props.noMovement && prevState.timer != this.state.timer) { let collisions = false; if (this.props.simulationType == 'One Weight' || this.props.simulationType == 'Inclined Plane') { const collisionsWithGround = this.checkForCollisionsWithGround(); const collisionsWithWalls = this.checkForCollisionsWithWall(); collisions = collisionsWithGround || collisionsWithWalls; } if (this.props.simulationType == 'Pulley') { if (this.state.yPosition <= this.props.yMin + 100 || this.state.yPosition >= this.props.yMax - 100) { collisions = true; } } if (!collisions) this.update(); this.setDisplayValues(); } // Convert from static to kinetic friction if/when weight slips on inclined plane if (prevState.xVelocity != this.state.xVelocity) { if (this.props.simulationType == 'Inclined Plane' && Math.abs(this.state.xVelocity) > 0.1 && this.props.simulationMode != 'Review' && !this.state.kineticFriction) { this.setState({ kineticFriction: true }); const normalForce: IForce = { description: 'Normal Force', magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, }; const frictionForce: IForce = { description: 'Kinetic Friction Force', magnitude: this.props.mass * this.props.coefficientOfKineticFriction * this.props.gravity * 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 // prettier-ignore const yForce = - this.props.gravity + normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); if (yForce > 0) { frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + this.props.gravity) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); } const normalForceComponent: IForce = { description: 'Normal Force', magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, }; const gravityParallel: IForce = { description: 'Gravity Parallel Component', magnitude: this.props.mass * this.props.gravity * Math.sin(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI + 180, }; const gravityPerpendicular: IForce = { description: 'Gravity Perpendicular Component', magnitude: this.props.mass * this.props.gravity * Math.cos(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), directionInDegrees: 360 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, }; const kineticFriction = this.props.coefficientOfKineticFriction != 0 ? [frictionForce] : []; this.props.setForcesUpdated([this.gravityForce(), normalForce, ...kineticFriction]); this.props.setComponentForces([normalForceComponent, gravityParallel, gravityPerpendicular, ...kineticFriction]); } } // Update x position when start pos x changes if (prevProps.startPosX != this.props.startPosX) { if (this.props.paused && !isNaN(this.props.startPosX)) { this.setState({ xPosition: this.props.startPosX, updatedStartPosX: this.props.startPosX }); this.props.setPosition(this.props.startPosX, undefined); } } // Update y position when start pos y changes TODO debug if (prevProps.startPosY != this.props.startPosY) { if (this.props.paused && !isNaN(this.props.startPosY)) { this.setState({ yPosition: this.props.startPosY, updatedStartPosY: this.props.startPosY ?? 0 }); this.props.setPosition(undefined, this.getDisplayYPos(this.props.startPosY)); } } // Update wedge coordinates if (!this.state.coordinates || this.props.yMax !== prevProps.yMax || prevProps.wedgeWidth != this.props.wedgeWidth || prevProps.wedgeHeight != this.props.wedgeHeight) { const left = this.props.xMax * 0.25; const coordinatePair1 = Math.round(left) + ',' + this.props.yMax + ' '; const coordinatePair2 = Math.round(left + this.props.wedgeWidth) + ',' + this.props.yMax + ' '; const coordinatePair3 = Math.round(left) + ',' + (this.props.yMax - this.props.wedgeHeight); this.setState({ coordinates: coordinatePair1 + coordinatePair2 + coordinatePair3 }); } if (this.state.xPosition != prevState.xPosition || this.state.yPosition != prevState.yPosition) { this.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.state.xPosition + 'px', position: 'absolute' as 'absolute', top: this.state.yPosition + 'px', touchAction: 'none', width: 2 * this.props.radius + 'px', zIndex: 5, }; } } // Reset simulation on reset button click resetEverything = () => { this.setState({ kineticFriction: false, xPosition: this.state.updatedStartPosX, yPosition: this.state.updatedStartPosY, xVelocity: this.props.startVelX, yVelocity: this.props.startVelY, angleLabel: Math.round(this.props.pendulumAngle * 100) / 100, }); this.props.setPendulumAngle(this.props.startPendulumAngle, undefined); this.props.setForcesUpdated(this.props.startForces()); this.props.setPosition(this.state.updatedStartPosX, this.state.updatedStartPosY); this.props.setVelocity(this.props.startVelX, this.props.startVelY); this.props.setAcceleration(0, 0); setTimeout(() => this.setState({ timer: this.state.timer + 1 })); }; // Compute x acceleration from forces, F=ma getNewAccelerationX = (forceList: IForce[]) => { // prettier-ignore return forceList.reduce((newXacc, force) => newXacc + (force.magnitude * Math.cos((force.directionInDegrees * Math.PI) / 180)) / this.props.mass, 0); }; // Compute y acceleration from forces, F=ma getNewAccelerationY = (forceList: IForce[]) => { // prettier-ignore return forceList.reduce((newYacc, force) => newYacc + (-1 * (force.magnitude * Math.sin((force.directionInDegrees * Math.PI) / 180))) / this.props.mass, 0); }; // Compute uniform circular motion forces given x, y positions getNewCircularMotionForces = (xPos: number, yPos: number): IForce[] => { const deltaX = (this.props.xMin + this.props.xMax) / 2 - (xPos + this.props.radius); const deltaY = yPos + this.props.radius - (this.props.yMin + this.props.yMax) / 2; return [ { description: 'Centripetal Force', magnitude: (this.props.startVelX ** 2 * this.props.mass) / this.props.circularMotionRadius, directionInDegrees: (Math.atan2(deltaY, deltaX) * 180) / Math.PI, }, ]; }; // Compute spring forces given y position getNewSpringForces = (yPos: number): IForce[] => { const yPosPlus = yPos - this.props.springRestLength > 0; const yPosMinus = yPos - this.props.springRestLength < 0; return [ this.gravityForce(), { description: 'Spring Force', magnitude: this.props.springConstant * (yPos - this.props.springRestLength) * (yPosPlus ? 1 : yPosMinus ? -1 : 0), directionInDegrees: yPosPlus ? 90 : 270, }, ]; }; // Compute pendulum forces given position, velocity getNewPendulumForces = (xPos: number, yPos: number, xVel: number, yVel: number): IForce[] => { const x = this.props.xMax / 2 - xPos - this.props.radius; const y = yPos + this.props.radius + 5; const angle = (ang => (ang < 0 ? ang + 180 : ang))((Math.atan(y / x) * 180) / Math.PI); let oppositeAngle = 90 - angle; if (oppositeAngle < 0) { oppositeAngle = 90 - (180 - angle); } const pendulumLength = Math.sqrt(x * x + y * y); this.props.setPendulumAngle(oppositeAngle, undefined); const mag = this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; return [ this.gravityForce(), { description: 'Tension', magnitude: mag, directionInDegrees: angle, }, ]; }; // Check for collisions in x direction checkForCollisionsWithWall = () => { let collision = false; if (this.state.xVelocity !== 0) { this.walls .filter(wall => wall.angleInDegrees === 90) .forEach(wall => { const wallX = (wall.xPos / 100) * this.props.panelWidth(); const minX = this.state.xPosition < wallX && wall.xPos < 0.35; const maxX = this.state.xPosition + 2 * this.props.radius >= wallX && wall.xPos > 0.35; if (minX || maxX) { this.setState({ xPosition: minX ? wallX + 0.01 : wallX - 2 * this.props.radius - 0.01, xVelocity: this.props.elasticCollisions ? -this.state.xVelocity : 0, }); collision = true; } }); } return collision; }; // Check for collisions in y direction checkForCollisionsWithGround = () => { let collision = false; const minY = this.state.yPosition; const maxY = this.state.yPosition + 2 * this.props.radius; if (this.state.yVelocity > 0) { this.walls.forEach(wall => { if (wall.angleInDegrees == 0 && wall.yPos > 0.4) { const groundY = (wall.yPos / 100) * this.props.panelHeight(); const gravity = this.gravityForce(); 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 }); const normalForce: IForce = { description: 'Normal force', magnitude: gravity.magnitude, directionInDegrees: -gravity.directionInDegrees, }; this.props.setForcesUpdated([gravity, normalForce]); if (this.props.simulationType === 'Inclined Plane') { this.props.setComponentForces([gravity, normalForce]); } } collision = true; } } }); } if (this.state.yVelocity < 0) { this.walls.forEach(wall => { if (wall.angleInDegrees == 0 && wall.yPos < 0.4) { const groundY = (wall.yPos / 100) * this.props.panelHeight(); if (minY < groundY) { this.setState({ yPosition: groundY + 0.01, yVelocity: this.props.elasticCollisions ? -this.state.yVelocity : 0, }); collision = true; } } }); } return collision; }; // Called at each RK4 step evaluate = (currentXPos: number, currentYPos: number, currentXVel: number, currentYVel: number, currdeltaXPos: number, currdeltaYPos: number, currdeltaXVel: number, currdeltaYVel: number, dt: number) => { const xPos = currentXPos + currdeltaXPos * dt; const yPos = currentYPos + currdeltaYPos * dt; const xVel = currentXVel + currdeltaXVel * dt; const yVel = currentYVel + currdeltaYVel * dt; const forces = this.getForces(xPos, yPos, xVel, yVel); return { xPos, yPos, xVel, yVel, deltaXPos: xVel, deltaYPos: yVel, deltaXVel: this.getNewAccelerationX(forces), deltaYVel: this.getNewAccelerationY(forces), }; }; getForces = (xPos: number, yPos: number, xVel: number, yVel: number) => { // prettier-ignore switch (this.props.simulationType) { case 'Pendulum': return this.getNewPendulumForces(xPos, yPos, xVel, yVel); case 'Spring' : return this.getNewSpringForces(yPos); case 'Circular Motion': return this.getNewCircularMotionForces(xPos, yPos); default: return this.props.forcesUpdated(); } }; // Update position, velocity using RK4 method update = () => { const startXVel = this.state.xVelocity; const startYVel = this.state.yVelocity; let xPos = this.state.xPosition; let yPos = this.state.yPosition; let xVel = this.state.xVelocity; let yVel = this.state.yVelocity; const forces = this.getForces(xPos, yPos, xVel, yVel); const xAcc = this.getNewAccelerationX(forces); const yAcc = this.getNewAccelerationY(forces); const coeff = (this.props.timestepSize * 1.0) / 6.0; for (let i = 0; i < this.props.simulationSpeed; i++) { const k1 = this.evaluate(xPos, yPos, xVel, yVel, xVel, yVel, xAcc, yAcc, 0); const k2 = this.evaluate(xPos, yPos, xVel, yVel, k1.deltaXPos, k1.deltaYPos, k1.deltaXVel, k1.deltaYVel, this.props.timestepSize * 0.5); const k3 = this.evaluate(xPos, yPos, xVel, yVel, k2.deltaXPos, k2.deltaYPos, k2.deltaXVel, k2.deltaYVel, this.props.timestepSize * 0.5); const k4 = this.evaluate(xPos, yPos, xVel, yVel, k3.deltaXPos, k3.deltaYPos, k3.deltaXVel, k3.deltaYVel, this.props.timestepSize); xVel += coeff * (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel); yVel += coeff * (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel); xPos += coeff * (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos); yPos += coeff * (k1.deltaYPos + 2 * (k2.deltaYPos + k3.deltaYPos) + k4.deltaYPos); } // make sure harmonic motion maintained and errors don't propagate switch (this.props.simulationType) { case 'Spring': const equilibriumPos = this.props.springRestLength + (this.props.mass * this.props.gravity) / this.props.springConstant; const amplitude = Math.abs(equilibriumPos - this.props.springStartLength); if (startYVel < 0 && yVel > 0 && yPos < this.props.springRestLength) { yPos = equilibriumPos - amplitude; } else if (startYVel > 0 && yVel < 0 && yPos > this.props.springRestLength) { yPos = equilibriumPos + amplitude; } break; case 'Pendulum': const startX = this.state.updatedStartPosX; if (startXVel <= 0 && xVel > 0) { xPos = this.state.updatedStartPosX; if (this.state.updatedStartPosX > this.props.xMax / 2) { xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; } yPos = this.props.startPosY; } else if (startXVel >= 0 && xVel < 0) { xPos = this.state.updatedStartPosX; if (this.state.updatedStartPosX < this.props.xMax / 2) { xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; } yPos = this.props.startPosY; } break; case 'One Weight': if (yPos < this.state.maxPosYConservation) { yPos = this.state.maxPosYConservation; } } this.setState({ xVelocity: xVel, yVelocity: yVel, xPosition: xPos, yPosition: yPos }); const forcesn = this.getForces(xPos, yPos, xVel, yVel); this.props.setForcesUpdated(forcesn); // set component forces if they change if (this.props.simulationType == 'Pendulum') { 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); const tensionComponent: IForce = { description: 'Tension', magnitude: this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength, directionInDegrees: angle, }; const gravityParallel: IForce = { description: 'Gravity Parallel Component', magnitude: this.props.gravity * Math.cos(((90 - angle) * Math.PI) / 180), directionInDegrees: 270 - (90 - angle), }; const gravityPerpendicular: IForce = { description: 'Gravity Perpendicular Component', magnitude: this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180), directionInDegrees: -(90 - angle), }; if (this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180) < 0) { gravityPerpendicular.magnitude = Math.abs(this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180)); gravityPerpendicular.directionInDegrees = 180 - (90 - angle); } this.props.setComponentForces([tensionComponent, gravityParallel, gravityPerpendicular]); } }; renderForce = (force: IForce, index: number, asComponent: boolean, color = '#0d0d0d') => { if (force.magnitude < this.epsilon) return; const angle = (force.directionInDegrees * Math.PI) / 180; const arrowStartY = this.state.yPosition + this.props.radius - this.props.radius * Math.sin(angle); const arrowStartX = this.state.xPosition + this.props.radius + this.props.radius * Math.cos(angle); const arrowEndY = arrowStartY - Math.abs(force.magnitude) * Math.sin(angle) - this.props.radius * Math.sin(angle); const arrowEndX = arrowStartX + Math.abs(force.magnitude) * Math.cos(angle) + this.props.radius * Math.cos(angle); let labelTop = arrowEndY + (force.directionInDegrees >= 0 && force.directionInDegrees < 180 ? 40 : -40); let labelLeft = arrowEndX + (force.directionInDegrees > 90 && force.directionInDegrees < 270 ? -120 : 30); labelTop = Math.max(Math.min(labelTop, this.props.yMax + 50), this.props.yMin); labelLeft = Math.max(Math.min(labelLeft, this.props.xMax - 60), this.props.xMin); return (

{force.description || 'Force'}

{this.props.showForceMagnitudes &&

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

}
); }; renderVector = (id: string, magX: number, magY: number, color: string, label: string) => { const mag = Math.sqrt(magX * magX + magY * magY); return (

{label}

); }; // Render weight, spring, rod(s), vectors render() { return (
{ if (this.draggable) { this.props.pause(); this.setState({ dragging: true, clickPositionX: e.clientX, clickPositionY: e.clientY, }); } }} onPointerMove={e => { if (this.state.dragging) { let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; if (newY > this.props.yMax - 2 * this.props.radius - 10) { newY = this.props.yMax - 2 * this.props.radius - 10; } else if (newY < 10) { newY = 10; } let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; if (newX > this.props.xMax - 2 * this.props.radius - 10) { newX = this.props.xMax - 2 * this.props.radius - 10; } else if (newX < 10) { newX = 10; } if (this.props.simulationType == 'Suspension') { if (newX < (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15) { newX = (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15; } else if (newX > (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15) { newX = (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15; } } this.setState({ yPosition: newY }); this.props.setPosition(undefined, Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100); if (this.props.simulationType != 'Pulley') { this.setState({ xPosition: newX }); this.props.setPosition(newX, undefined); } if (this.props.simulationType != 'Suspension') { if (this.props.simulationType != 'Pulley') { this.setState({ updatedStartPosX: newX }); } this.setState({ updatedStartPosY: newY }); } this.setState({ clickPositionX: e.clientX, clickPositionY: e.clientY, }); this.setDisplayValues(); } }} onPointerUp={e => { if (this.state.dragging) { if (this.props.simulationType != 'Pendulum' && this.props.simulationType != 'Suspension') { 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 - 10) { newY = this.props.yMax - 2 * this.props.radius - 10; } else if (newY < 10) { newY = 10; } let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; if (newX > this.props.xMax - 2 * this.props.radius - 10) { newX = this.props.xMax - 2 * this.props.radius - 10; } else if (newX < 10) { newX = 10; } if (this.props.simulationType == 'Spring') { this.props.setSpringLength(newY); } if (this.props.simulationType == 'Suspension') { const x1rod = (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200; const x2rod = (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius; const deltaX1 = this.state.xPosition + this.props.radius - x1rod; const deltaX2 = x2rod - (this.state.xPosition + this.props.radius); const deltaY = this.state.yPosition + this.props.radius; const dir1T = Math.PI - Math.atan(deltaY / deltaX1); const dir2T = Math.atan(deltaY / deltaX2); const tensionMag2 = (this.props.mass * this.props.gravity) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); const tensionForce1: IForce = { description: 'Tension', magnitude: tensionMag1, directionInDegrees: (dir1T * 180) / Math.PI, }; const tensionForce2: IForce = { description: 'Tension', magnitude: tensionMag2, directionInDegrees: (dir2T * 180) / Math.PI, }; this.props.setForcesUpdated([tensionForce1, tensionForce2, this.gravityForce()]); } } }}>

{this.props.mass} kg

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

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

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

)} {this.props.simulationType == 'Circular Motion' && (
)} {this.props.simulationType == 'Pendulum' && (
{!this.state.dragging && (

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

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

)}
)} {this.props.simulationType == 'Inclined Plane' && (

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

)} {!this.state.dragging && this.props.showAcceleration && this.renderVector( 'accArrow', this.getNewAccelerationX(this.props.forcesUpdated()), this.getNewAccelerationY(this.props.forcesUpdated()), 'green', `${Math.round(100 * Math.sqrt(this.state.xAccel * this.state.xAccel + this.state.yAccel * this.state.yAccel)) / 100} m/s^2` )} {!this.state.dragging && this.props.showVelocity && this.renderVector( 'velArrow', this.state.xVelocity, this.state.yVelocity, 'blue', `${Math.round(100 * Math.sqrt(this.props.displayXVelocity * this.props.displayXVelocity + this.props.displayYVelocity * this.props.displayYVelocity)) / 100} m/s` )} {!this.state.dragging && this.props.showComponentForces && this.props.componentForces().map((force, index) => this.renderForce(force, index, true))} {!this.state.dragging && this.props.showForces && this.props.forcesUpdated().map((force, index) => this.renderForce(force, index, false))}
); } }