diff options
Diffstat (limited to 'src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx')
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx | 1697 |
1 files changed, 1240 insertions, 457 deletions
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx index 39b3249e8..3bc02cfdd 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx @@ -1,27 +1,54 @@ -import React = require('react'); import { Doc } from '../../../fields/Doc'; -import { IWallProps } from "./PhysicsSimulationWall"; +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 { - adjustPendulumAngle: boolean; + dataDoc: doc; + adjustPendulumAngle: { angle: number; length: number }; + circularMotionRadius: number; + coefficientOfKineticFriction: number; color: string; - dataDoc: Doc; + 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; - simulationReset: boolean; + 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; + startVelX: number; + startVelY: number; timestepSize: number; - updateDisplay: boolean, - walls: IWallProps[]; - wedge: boolean; + updateDisplay: { xDisplay: number; yDisplay: number }; + updatedForces: IForce[]; wedgeHeight: number; wedgeWidth: number; xMax: number; @@ -34,47 +61,64 @@ interface IState { angleLabel: number, clickPositionX: number, clickPositionY: number, + coordinates: string, dragging: boolean, kineticFriction: boolean, - timer: number; + 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<IWeightProps, IState> { constructor(props: any) { super(props) this.state = { + angleLabel: 0, clickPositionX: 0, clickPositionY: 0, + coordinates: "", dragging: false, kineticFriction: false, + maxPosYConservation: 0, timer: 0, - angleLabel: 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 - draggable = !this.props.dataDoc['wedge'] ; - epsilon = 0.0001; - forceOfGravity: IForce = { - description: "Gravity", - magnitude: this.props.mass * 9.81, - directionInDegrees: 270, - }; + 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)`; - // Var + // Variables weightStyle = { alignItems: "center", backgroundColor: this.props.color, @@ -115,6 +159,7 @@ export default class Weight extends React.Component<IWeightProps, IState> { 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, @@ -133,208 +178,121 @@ export default class Weight extends React.Component<IWeightProps, IState> { ; }; - componentDidMount() { - // Timer for animating the simulation - setInterval(() => { - this.setState({timer: this.state.timer + 1}); - }, 60); + componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void { } - componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void { - - // When display values updated by user, update real values - if (this.props.updateDisplay != prevProps.updateDisplay) { - if (this.props.dataDoc['positionXDisplay'] != this.state.xPosition) { - let x = this.props.dataDoc['positionXDisplay']; - 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.dataDoc['positionXDisplay'] = x; - } - - if (this.props.dataDoc['positionYDisplay'] != this.getDisplayYPos(this.state.yPosition)) { - let y = this.props.dataDoc['positionYDisplay']; - y = Math.max(0, y); - y = Math.min(y, this.props.yMax - 2 * this.props.radius); - this.props.dataDoc['positionYDisplay'] = y; - let coordinatePosition = this.getYPosFromDisplay(y); - this.setState({updatedStartPosY: coordinatePosition}) - this.setState({yPosition: coordinatePosition}) - } - - if (this.props.dataDoc['velocityXDisplay'] != this.state.xVelocity) { - let x = this.props.dataDoc['velocityXDisplay']; - this.setState({xVelocity: x}) - this.props.dataDoc['velocityXDisplay'] = x; - } - - if (this.props.dataDoc['velocityYDisplay'] != this.state.yVelocity) { - let y = this.props.dataDoc['velocityYDisplay']; - this.setState({yVelocity: -y}) - this.props.dataDoc['velocityYDisplay'] - } - } - // Update sim - if (this.state.timer != prevState.timer) { - if (!this.props.dataDoc['simulationPaused']) { - let collisions = false; - if (!this.props.dataDoc['pendulum']) { - const collisionsWithGround = this.checkForCollisionsWithGround(); - const collisionsWithWalls = this.checkForCollisionsWithWall(); - collisions = collisionsWithGround || collisionsWithWalls; - } - if (!collisions) { - this.update(); - } - this.setDisplayValues(); - } - } - if (this.props.simulationReset != prevProps.simulationReset) { - this.resetEverything(); - } - if (this.props.adjustPendulumAngle != prevProps.adjustPendulumAngle) { - console.log('update angle') - // Change pendulum angle based on input field - let length = this.props.dataDoc['pendulumLength'] ?? 0; - const x = - length * Math.cos(((90 - this.props.dataDoc['pendulumAngle']) * Math.PI) / 180); - const y = - length * Math.sin(((90 - this.props.dataDoc['pendulumAngle']) * 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.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle'] * 100) / 100}) - } - // Update x start position - if (this.props.startPosX != prevProps.startPosX) { - this.setState({updatedStartPosX: this.props.dataDoc['startPosX']}) - this.setState({xPosition: this.props.dataDoc['startPosX']}) - this.setXPosDisplay(this.props.dataDoc['startPosX']); - } - // Update y start position - if (this.props.startPosY != prevProps.startPosY) { - this.setState({updatedStartPosY: this.props.dataDoc['startPosY']}) - this.setState({yPosition: this.props.dataDoc['startPosY']}) - this.setYPosDisplay(this.props.dataDoc['startPosY']); - } - if (!this.props.dataDoc['simulationPaused']) { - if (this.state.xVelocity != prevState.xVelocity) { - if (this.props.dataDoc['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.dataDoc['wedgeHeight'] / this.props.dataDoc['wedgeWidth'] )), - directionInDegrees: - 180 - 90 - (Math.atan(this.props.dataDoc['wedgeHeight'] / this.props.dataDoc['wedgeWidth'] ) * 180) / Math.PI, - }; - let frictionForce: IForce = { - description: "Kinetic Friction Force", - magnitude: - this.props.dataDoc['coefficientOfKineticFriction'] * - this.forceOfGravity.magnitude * - Math.cos(Math.atan(this.props.dataDoc['wedgeHeight'] / this.props.dataDoc['wedgeWidth'] )), - directionInDegrees: - 180 - (Math.atan(this.props.dataDoc['wedgeHeight'] / this.props.dataDoc['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.dataDoc['coefficientOfKineticFriction'] != 0) { - this.props.dataDoc['updatedForces'] = [this.forceOfGravity, normalForce, frictionForce]; - } else { - this.props.dataDoc['updatedForces'] = ([this.forceOfGravity, normalForce]); - } - } - } - } - this.weightStyle = { - alignItems: "center", - backgroundColor: this.props.color, - borderColor: this.state.dragging ? "lightblue" : "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}) 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}) - this.setDisplayValues(); }; - getNewAccelerationX = (forceList: IForce[]) => { + + // Compute x acceleration from forces, F=ma + const getNewAccelerationX = (forceList: IForce[]) => { let newXAcc = 0; - if (forceList) { - forceList.forEach((force) => { + forceList.forEach((force) => { + if (force.component == false) { newXAcc += (force.magnitude * Math.cos((force.directionInDegrees * Math.PI) / 180)) / - this.props.mass; - }); - } + mass; + } + }); return newXAcc; }; - getNewAccelerationY = (forceList: IForce[]) => { + // Compute y acceleration from forces, F=ma + const getNewAccelerationY = (forceList: IForce[]) => { let newYAcc = 0; - if (forceList) { - forceList.forEach((force) => { + forceList.forEach((force) => { + if (force.component == false) { newYAcc += (-1 * (force.magnitude * Math.sin((force.directionInDegrees * Math.PI) / 180))) / - this.props.mass; - }); - } + mass; + } + }); return newYAcc; }; - getNewForces = ( + // Compute uniform circular motion forces given x, y positions + const getNewCircularMotionForces = (xPos: number, yPos: number) => { + let deltaX = (xMin + xMax) / 2 - (xPos + radius); + let deltaY = yPos + radius - (yMin + yMax) / 2; + let dir = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; + const tensionForce: IForce = { + description: "Centripetal Force", + magnitude: (startVelX ** 2 * mass) / circularMotionRadius, + directionInDegrees: dir, + component: false, + }; + return [tensionForce]; + }; + + // Compute spring forces given y position + const getNewSpringForces = (yPos: number) => { + let springForce: IForce = { + description: "Spring Force", + magnitude: 0, + directionInDegrees: 90, + component: false, + }; + if (yPos - springRestLength > 0) { + springForce = { + description: "Spring Force", + magnitude: springConstant * (yPos - springRestLength), + directionInDegrees: 90, + component: false, + }; + } else if (yPos - springRestLength < 0) { + springForce = { + description: "Spring Force", + magnitude: springConstant * (springRestLength - yPos), + directionInDegrees: 270, + component: false, + }; + } + + return [ + { + description: "Gravity", + magnitude: Math.abs(gravity) * mass, + directionInDegrees: 270, + component: false, + }, + springForce, + ]; + }; + + // Compute pendulum forces given position, velocity + const getNewPendulumForces = ( xPos: number, yPos: number, xVel: number, yVel: number ) => { - if (!this.props.dataDoc['pendulum']) { - return this.props.dataDoc['updatedForces']; - } - const x = this.props.xMax / 2 - xPos - this.props.radius; - const y = yPos + this.props.radius + 5; + 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; @@ -345,265 +303,816 @@ export default class Weight extends React.Component<IWeightProps, IState> { } const pendulumLength = Math.sqrt(x * x + y * y); - this.props.dataDoc['pendulumAngle'] = oppositeAngle; - this.props.dataDoc['pendulumLength'] = Math.sqrt(x * x + y * y); + setPendulumAngle(oppositeAngle); const mag = - this.props.mass * 9.81 * Math.cos((oppositeAngle * Math.PI) / 180) + - (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; + mass * Math.abs(gravity) * Math.cos((oppositeAngle * Math.PI) / 180) + + (mass * (xVel * xVel + yVel * yVel)) / pendulumLength; const forceOfTension: IForce = { description: "Tension", magnitude: mag, directionInDegrees: angle, + component: false, }; - this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle']* 100) / 100}) - - return [this.forceOfGravity, forceOfTension]; - }; - getNewPosition = (pos: number, vel: number) => { - return pos + vel * this.props.timestepSize; + return [ + { + description: "Gravity", + magnitude: Math.abs(gravity) * mass, + directionInDegrees: 270, + component: false, + }, + forceOfTension, + ]; }; - getNewVelocity = (vel: number, acc: number) => { - return vel + acc * this.props.timestepSize; - }; - - checkForCollisionsWithWall = () => { + // 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; - const containerWidth = 300; - if (this.state.xVelocity != 0) { - if (this.props.dataDoc.wallPositions) { - this.props.dataDoc['wallPositions'].forEach((wall) => { + const minX = xPosition; + const maxX = xPosition + 2 * radius; + if (xVelocity != 0) { + walls.forEach((wall) => { if (wall.angleInDegrees == 90) { - const wallX = (wall.xPos / 100) * 300; + const wallX = (wall.xPos / 100) * window.innerWidth; if (wall.xPos < 0.35) { if (minX <= wallX) { - if (this.props.dataDoc['elasticCollisions']) { - this.setState({xVelocity: -this.state.xVelocity}); + setXPosition(wallX + 0.01); + if (elasticCollisions) { + setXVelocity(-xVelocity); } else { - this.setState({xVelocity: 0}); - this.setState({xPosition: wallX+5}); + setXVelocity(0); } collision = true; } } else { if (maxX >= wallX) { - if (this.props.dataDoc['elasticCollisions']) { - this.setState({xVelocity: -this.state.xVelocity}); + setXPosition(wallX - 2 * radius - 0.01); + if (elasticCollisions) { + setXVelocity(-xVelocity); } else { - this.setState({xVelocity: 0}); - this.setState({xPosition: wallX - 2 * this.props.radius + 5}); + setXVelocity(0); } collision = true; } } } }); - } } return collision; }; - checkForCollisionsWithGround = () => { + // Check for collisions in y direction + const checkForCollisionsWithGround = () => { let collision = false; - const maxY = this.state.yPosition + 2 * this.props.radius; - if (this.state.yVelocity > 0) { - if (this.props.dataDoc.wallPositions) { - this.props.dataDoc['wallPositions'].forEach((wall) => { - if (wall.angleInDegrees == 0) { - const groundY = (wall.yPos / 100) * this.props.yMax; - if (maxY >= groundY) { - if (this.props.dataDoc['elasticCollisions']) { - this.setState({yVelocity: -this.state.yVelocity}) + const minY = yPosition; + const maxY = yPosition + 2 * radius; + if (yVelocity > 0) { + walls.forEach((wall) => { + if (wall.angleInDegrees == 0 && wall.yPos > 0.4) { + const groundY = (wall.yPos / 100) * window.innerHeight; + if (maxY > groundY) { + setYPosition(groundY - 2 * radius - 0.01); + if (elasticCollisions) { + setYVelocity(-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.dataDoc['updatedForces'] = ([forceOfGravity, normalForce]); + setYVelocity(0); + if (this.props.dataDoc['simulationType'] != "Two Weights") { + const forceOfGravity: IForce = { + description: "Gravity", + magnitude: Math.abs(gravity) * mass, + directionInDegrees: 270, + component: false, + }; + const normalForce: IForce = { + description: "Normal force", + magnitude: Math.abs(gravity) * mass, + directionInDegrees: wall.angleInDegrees + 90, + component: false, + }; + setUpdatedForces([forceOfGravity, normalForce]); + if (this.props.dataDoc['simulationType'] == "Inclined Plane") { + const forceOfGravityC: IForce = { + description: "Gravity", + magnitude: Math.abs(gravity) * mass, + directionInDegrees: 270, + component: true, + }; + const normalForceC: IForce = { + description: "Normal force", + magnitude: Math.abs(gravity) * mass, + directionInDegrees: wall.angleInDegrees + 90, + component: true, + }; + setComponentForces([forceOfGravityC, normalForceC]); + } + } + } + collision = true; + } + } + }); + } + if (yVelocity < 0) { + walls.forEach((wall) => { + if (wall.angleInDegrees == 0 && wall.yPos < 0.4) { + const groundY = (wall.yPos / 100) * window.innerHeight; + if (minY < groundY) { + setYPosition(groundY + 5); + if (elasticCollisions) { + setYVelocity(-yVelocity); + } else { + setYVelocity(0); } 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); + // 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 += - this.props.timestepSize * (xAcc1 / 6.0 + xAcc2 / 3.0 + xAcc3 / 3.0 + xAcc4 / 6.0); + ((timestepSize * 1.0) / 6.0) * + (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel); yVel += - this.props.timestepSize * (yAcc1 / 6.0 + yAcc2 / 3.0 + yAcc3 / 3.0 + yAcc4 / 6.0); + ((timestepSize * 1.0) / 6.0) * + (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel); xPos += - this.props.timestepSize * (xVel1 / 6.0 + xVel2 / 3.0 + xVel3 / 3.0 + xVel4 / 6.0); + ((timestepSize * 1.0) / 6.0) * + (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos); yPos += - this.props.timestepSize * (yVel1 / 6.0 + yVel2 / 3.0 + yVel3 / 3.0 + yVel4 / 6.0); + ((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); - this.setState({xVelocity: xVel}); - this.setState({yVelocity: yVel}); - this.setState({xPosition: xPos}); - this.setState({yPosition: yPos}); - - this.props.dataDoc['updatedForces'] = (this.getNewForces(xPos, yPos, xVel, yVel)); + // 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]); - labelBackgroundColor = `rgba(255,255,255,0.5)`; + // 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 () { - return ( - <div> + // Render weight, spring, rod(s), vectors + return ( + <div style={{ zIndex: -1000 }}> <div className="weightContainer" - // onPointerDown={(e) => { - // if (this.draggable) { - // e.preventDefault(); - // this.props.dataDoc['simulationPaused'] = 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.dataDoc['positionYDisplay'] = 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.dataDoc['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.dataDoc['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.dataDoc['pendulumAngle'] = oppositeAngle; - // this.props.dataDoc['pendulumLength'] = 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.dataDoc['updatedForces'] = ([this.forceOfGravity, forceOfTension]); - // } - // } - // }} + onPointerDown={(e) => { + 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]); + } + } + }} > - <div className="weight" style={this.weightStyle}> - <p className="weightLabel">{this.props.mass} kg</p> + <div className="weight" style={weightStyle}> + <p className="weightLabel">{mass} kg</p> </div> </div> - {this.props.dataDoc['pendulum'] && ( + {this.props.dataDoc['simulationType'] == "Spring" && ( + <div + className="spring" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + zIndex: -2, + }} + > + <svg width={xMax + "px"} height={window.innerHeight + "px"}> + {[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 ( + <line + key={val} + x1={xPos1} + y1={yPos1} + x2={xPos2} + y2={yPos2} + stroke={"#808080"} + strokeWidth="10" + /> + ); + })} + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Pulley" && ( + <div + className="wheel" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + zIndex: -1, + }} + > + <svg width={xMax + "px"} height={window.innerHeight + "px"}> + <circle + cx={(xMax + xMin) / 2} + cy={radius} + r={radius * 1.5} + fill={"#808080"} + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Pulley" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + zIndex: -2, + }} + > + <svg width={xMax + "px"} height={window.innerHeight + "px"}> + <line + x1={xPosition + radius} + y1={yPosition + radius} + x2={xPosition + radius} + y2={yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Suspension" && ( <div className="rod" style={{ @@ -611,45 +1120,200 @@ export default class Weight extends React.Component<IWeightProps, IState> { position: "absolute", left: 0, top: 0, + zIndex: -2, }} > - <svg width={this.props.xMax + "px"} height={300 + "px"}> + <svg width={xMax + "px"} height={window.innerHeight + "px"}> <line - x1={this.state.xPosition + this.props.radius} - y1={this.state.yPosition + this.props.radius} - x2={this.props.xMax / 2} + x1={xPosition + radius} + y1={yPosition + radius} + x2={(xMax + xMin) / 2 - radius - yMin - 200} + y2={yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + <p + style={{ + position: "absolute", + zIndex: 10, + left: (xMax + xMin) / 2 - radius - yMin - 200 + 80 + "px", + top: 10 + "px", + backgroundColor: labelBackgroundColor, + }} + > + {Math.round( + ((Math.atan( + (yPosition + radius) / + (xPosition + + radius - + ((xMax + xMin) / 2 - radius - yMin - 200)) + ) * + 180) / + Math.PI) * + 100 + ) / 100} + ° + </p> + </div> + )} + {this.props.dataDoc['simulationType'] == "Suspension" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + zIndex: -2, + }} + > + <svg width={xMax + "px"} height={window.innerHeight + "px"}> + <line + x1={xPosition + radius} + y1={yPosition + radius} + x2={(xMax + xMin) / 2 + yMin + 200 + radius} + y2={yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + + <p + style={{ + position: "absolute", + zIndex: 10, + left: (xMax + xMin) / 2 + yMin + 200 + radius - 80 + "px", + top: 10 + "px", + backgroundColor: labelBackgroundColor, + }} + > + {Math.round( + ((Math.atan( + (yPosition + radius) / + ((xMax + xMin) / 2 + + yMin + + 200 + + radius - + (xPosition + radius)) + ) * + 180) / + Math.PI) * + 100 + ) / 100} + ° + </p> + </div> + )} + {this.props.dataDoc['simulationType'] == "Circular Motion" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + zIndex: -2, + }} + > + <svg width={xMax + "px"} height={window.innerHeight + "px"}> + <line + x1={xPosition + radius} + y1={yPosition + radius} + x2={(xMin + xMax) / 2} + y2={(yMin + yMax) / 2} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Pendulum" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + zIndex: -2, + }} + > + <svg width={xMax + "px"} height={window.innerHeight + "px"}> + <line + x1={xPosition + radius} + y1={yPosition + radius} + x2={xMax / 2} y2={-5} stroke={"#deb887"} strokeWidth="10" /> </svg> - {!this.state.dragging && ( + {!dragging && ( <div> <p style={{ position: "absolute", - left: this.props.xMax / 2 + "px", + zIndex: 5, + left: xPosition + "px", + top: yPosition - 70 + "px", + backgroundColor: labelBackgroundColor, + }} + > + {Math.round(pendulumLength)} m + </p> + <p + style={{ + position: "absolute", + zIndex: -1, + left: xMax / 2 + "px", top: 30 + "px", - backgroundColor: this.labelBackgroundColor, + backgroundColor: labelBackgroundColor, }} > - {this.state.angleLabel}° + {Math.round(pendulumAngle * 100) / 100}° </p> </div> )} </div> )} - {!this.state.dragging && this.props.dataDoc['showAcceleration'] && ( + {this.props.dataDoc['simulationType'] == "Inclined Plane" && ( + <div> + <div + style={{ position: "absolute", left: "0", top: "0", zIndex: -5 }} + > + <svg width={xMax + "px"} height={yMax + "px"}> + <polygon points={coordinates} style={{ fill: "burlywood" }} /> + </svg> + </div> + + <p + style={{ + position: "absolute", + zIndex: 500, + left: Math.round(xMax * 0.5 - 200 + wedgeWidth - 80) + "px", + top: Math.round(window.innerHeight * 0.8 - 40) + "px", + }} + > + {Math.round( + ((Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI) * 100 + ) / 100} + ° + </p> + </div> + )} + {!dragging && showAcceleration && ( <div> <div style={{ pointerEvents: "none", position: "absolute", + zIndex: -1, left: 0, top: 0, }} > - <svg width={this.props.xMax + "px"} height={300 + "px"}> + <svg width={xMax + "px"} height={window.innerHeight + "px"}> <defs> <marker id="accArrow" @@ -664,10 +1328,10 @@ export default class Weight extends React.Component<IWeightProps, IState> { </marker> </defs> <line - x1={this.state.xPosition + this.props.radius} - y1={this.state.yPosition + this.props.radius} - x2={this.state.xPosition + this.props.radius + this.getNewAccelerationX(this.props.dataDoc['updatedForces']) * 5} - y2={this.state.yPosition + this.props.radius + this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 5} + x1={xPosition + radius} + y1={yPosition + radius} + x2={xPosition + radius + getNewAccelerationX(updatedForces) * 7} + y2={yPosition + radius + getNewAccelerationY(updatedForces) * 7} stroke={"green"} strokeWidth="5" markerEnd="url(#accArrow)" @@ -677,46 +1341,35 @@ export default class Weight extends React.Component<IWeightProps, IState> { style={{ pointerEvents: "none", position: "absolute", - left: - this.state.xPosition + - this.props.radius + - this.getNewAccelerationX(this.props.dataDoc['updatedForces']) * 5 + - 25 + - "px", - top: - this.state.yPosition + - this.props.radius + - this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 5 + - 25 + - "px", + left: xPosition + radius + xAccel * 3 + 25 + "px", + top: yPosition + radius + yAccel * 3 + 70 + "px", + zIndex: -1, lineHeight: 0.5, }} > <p> {Math.round( - 100 * - Math.sqrt( - Math.pow(this.getNewAccelerationX(this.props.dataDoc['updatedForces']) * 3, 2) + - Math.pow(this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 3, 2) - ) + 100 * Math.sqrt(xAccel * xAccel + yAccel * yAccel) ) / 100}{" "} - m/s<sup>2</sup> + m/s + <sup>2</sup> </p> </div> </div> </div> )} - {!this.state.dragging && this.props.dataDoc['showVelocity'] && ( + {!dragging && showVelocity && ( <div> <div style={{ pointerEvents: "none", position: "absolute", + zIndex: -1, left: 0, top: 0, }} > - <svg width={this.props.xMax + "px"} height={300 + "px"}> + <svg width={xMax + "px"} height={window.innerHeight + "px"}> <defs> <marker id="velArrow" @@ -731,10 +1384,10 @@ export default class Weight extends React.Component<IWeightProps, IState> { </marker> </defs> <line - x1={this.state.xPosition + this.props.radius} - y1={this.state.yPosition + this.props.radius} - x2={this.state.xPosition + this.props.radius + this.state.xVelocity * 3} - y2={this.state.yPosition + this.props.radius + this.state.yVelocity * 3} + x1={xPosition + radius} + y1={yPosition + radius} + x2={xPosition + radius + xVelocity * 3} + y2={yPosition + radius + yVelocity * 3} stroke={"blue"} strokeWidth="5" markerEnd="url(#velArrow)" @@ -744,14 +1397,19 @@ export default class Weight extends React.Component<IWeightProps, IState> { style={{ pointerEvents: "none", position: "absolute", - left: this.state.xPosition + this.props.radius + this.state.xVelocity * 3 + 25 + "px", - top: this.state.yPosition + this.props.radius + this.state.yVelocity * 3 + "px", + left: xPosition + radius + xVelocity * 3 + 25 + "px", + top: yPosition + radius + yVelocity * 3 + "px", + zIndex: -1, lineHeight: 0.5, }} > <p> {Math.round( - 100 * Math.sqrt(this.state.xVelocity**2 + this.state.yVelocity**2) + 100 * + Math.sqrt( + displayXVelocity * displayXVelocity + + displayYVelocity * displayYVelocity + ) ) / 100}{" "} m/s </p> @@ -759,23 +1417,134 @@ export default class Weight extends React.Component<IWeightProps, IState> { </div> </div> )} - {!this.state.dragging && - this.props.dataDoc['showForces'] && - this.props.dataDoc['updatedForces'].map((force, index) => { - if (force.magnitude < this.epsilon) { + {!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 ( + <div key={index}> + <div + style={{ + pointerEvents: "none", + position: "absolute", + zIndex: -1, + left: xMin, + top: yMin, + }} + > + <svg + width={xMax - xMin + "px"} + height={window.innerHeight + "px"} + > + <defs> + <marker + id="forceArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill={color} /> + </marker> + </defs> + {force.component == true && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + strokeDasharray="10,10" + markerEnd="url(#forceArrow)" + /> + )} + {force.component == false && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + markerEnd="url(#forceArrow)" + /> + )} + </svg> + </div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: labelLeft + "px", + top: labelTop + "px", + // zIndex: -1, + lineHeight: 0.5, + backgroundColor: labelBackgroundColor, + }} + > + {force.description && <p>{force.description}</p>} + {!force.description && <p>Force</p>} + {showForceMagnitudes && ( + <p>{Math.round(100 * force.magnitude) / 100} N</p> + )} + </div> + </div> + ); + })} + {!dragging && + showForces && + updatedForces.map((force, index) => { + if (force.magnitude < epsilon) { return; } - let arrowStartY: number = this.state.yPosition + this.props.radius; - const arrowStartX: number = this.state.xPosition + this.props.radius; + let arrowStartY: number = yPosition + radius; + const arrowStartX: number = xPosition + radius; let arrowEndY: number = arrowStartY - Math.abs(force.magnitude) * - 10 * + 20 * Math.sin((force.directionInDegrees * Math.PI) / 180); const arrowEndX: number = arrowStartX + Math.abs(force.magnitude) * - 10 * + 20 * Math.cos((force.directionInDegrees * Math.PI) / 180); let color = "#0d0d0d"; @@ -792,10 +1561,10 @@ export default class Weight extends React.Component<IWeightProps, IState> { } 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); + labelTop = Math.min(labelTop, yMax + 50); + labelTop = Math.max(labelTop, yMin); + labelLeft = Math.min(labelLeft, xMax - 60); + labelLeft = Math.max(labelLeft, xMin); return ( <div key={index}> @@ -803,13 +1572,14 @@ export default class Weight extends React.Component<IWeightProps, IState> { style={{ pointerEvents: "none", position: "absolute", - left: this.props.xMin, - top: this.props.yMin, + zIndex: -1, + left: xMin, + top: yMin, }} > <svg - width={this.props.xMax - this.props.xMin + "px"} - height={300 + "px"} + width={xMax - xMin + "px"} + height={window.innerHeight + "px"} > <defs> <marker @@ -824,15 +1594,29 @@ export default class Weight extends React.Component<IWeightProps, IState> { <path d="M0,0 L0,6 L9,3 z" fill={color} /> </marker> </defs> - <line - x1={arrowStartX} - y1={arrowStartY} - x2={arrowEndX} - y2={arrowEndY} - stroke={color} - strokeWidth="5" - markerEnd="url(#forceArrow)" - /> + {force.component == true && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + strokeDasharray="10,10" + markerEnd="url(#forceArrow)" + /> + )} + {force.component == false && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + markerEnd="url(#forceArrow)" + /> + )} </svg> </div> <div @@ -842,12 +1626,12 @@ export default class Weight extends React.Component<IWeightProps, IState> { left: labelLeft + "px", top: labelTop + "px", lineHeight: 0.5, - backgroundColor: this.labelBackgroundColor, + backgroundColor: labelBackgroundColor, }} > {force.description && <p>{force.description}</p>} {!force.description && <p>Force</p>} - {this.props.dataDoc['showForceMagnitudes'] && ( + {showForceMagnitudes && ( <p>{Math.round(100 * force.magnitude) / 100} N</p> )} </div> @@ -855,6 +1639,5 @@ export default class Weight extends React.Component<IWeightProps, IState> { ); })} </div> - ); - } + ); }; |