aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx')
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx1674
1 files changed, 1674 insertions, 0 deletions
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx
new file mode 100644
index 000000000..88af37791
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx
@@ -0,0 +1,1674 @@
+import { Doc } from '../../../../fields/Doc';
+import React = require('react');
+import "./PhysicsSimulationBox.scss";
+
+interface IWallProps {
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
+}
+interface IForce {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+ component: boolean;
+}
+export interface IWeightProps {
+ dataDoc: Doc;
+ layoutDoc: Doc;
+ adjustPendulumAngle: { angle: number; length: number };
+ circularMotionRadius: number;
+ coefficientOfKineticFriction: number;
+ color: string;
+ componentForces: IForce[];
+ displayXVelocity: number;
+ displayYVelocity: number;
+ elasticCollisions: boolean;
+ gravity: 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[];
+ 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,
+ walls: IWallProps[],
+ xPosition: any,
+ xVelocity: number,
+ yPosition: any,
+ 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,
+ updatedStartPosX: this.props.startPosX ?? 0,
+ updatedStartPosY: this.props.startPosY ?? 0,
+ walls: [],
+ xPosition: this.props.startPosX ?? 0,
+ xVelocity: this.props.startVelX ? this.props.startVelX: 0,
+ yPosition: this.props.startPosY ?? 0,
+ 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['simulationType'] != "Inclined Plane" &&
+ this.props.dataDoc['simulationType'] != "Pendulum" &&
+ this.props.dataDoc['mode'] == "Freeform";
+ 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) => {
+ 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);
+ if (this.props.color == 'red') {
+ this.props.dataDoc['positionYDisplay'] = Math.round(displayPos * 100) / 100
+ } else {
+ this.props.dataDoc['positionYDisplay2'] = Math.round(displayPos * 100) / 100
+ }
+ };
+ setXPosDisplay = (xPos: number) => {
+ if (this.props.color == 'red') {
+ this.props.dataDoc['positionXDisplay'] = Math.round(xPos * 100) / 100;
+ } else {
+ this.props.dataDoc['positionXDisplay2'] = Math.round(xPos * 100) / 100;}
+ };
+ setYVelDisplay = (yVel: number) => {
+ if (this.props.color == 'red') {
+ this.props.dataDoc['velocityYDisplay'] = (-1 * Math.round(yVel * 100)) / 100;
+ } else {
+ this.props.dataDoc['velocityYDisplay2'] = (-1 * Math.round(yVel * 100)) / 100;}
+ };
+ setXVelDisplay = (xVel: number) => {
+ if (this.props.color == 'red') {
+ this.props.dataDoc['velocityXDisplay'] = Math.round(xVel * 100) / 100;
+ } else {
+ this.props.dataDoc['velocityXDisplay2'] = 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);
+ if (this.props.color == 'red') {
+ this.props.dataDoc['accelerationYDisplay'] =
+ (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100
+ ;
+ this.props.dataDoc['accelerationXDisplay'] =
+ Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100
+ ;
+ } else {
+ this.props.dataDoc['accelerationYDisplay2'] =
+ (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100
+ ;
+ this.props.dataDoc['accelerationXDisplay2'] =
+ Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100
+ ;}
+
+ this.setState({xAccel : (Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100)})
+ this.setState({yAccel :
+ (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100
+ });
+ };
+
+ componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void {
+ // Change pendulum angle from input field
+ if (prevProps.adjustPendulumAngle != this.props.adjustPendulumAngle) {
+ let length = this.props.adjustPendulumAngle.length;
+ const x =
+ length * Math.cos(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180);
+ const y =
+ length * Math.sin(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180);
+ const xPos = this.props.xMax / 2 - x - this.props.radius;
+ const yPos = y - this.props.radius - 5;
+ this.setState({xPosition: xPos})
+ this.setState({yPosition: yPos})
+ this.setState({updatedStartPosX: xPos})
+ this.setState({updatedStartPosY: yPos})
+ this.props.dataDoc['pendulumAngle'] = this.props.adjustPendulumAngle.angle
+ this.props.dataDoc['pendulumLength'] = this.props.adjustPendulumAngle.length
+ }
+
+ // When display values updated by user, update real values
+ if (prevProps.updateDisplay != this.props.updateDisplay) {
+ if (this.props.updateDisplay.xDisplay != this.state.xPosition) {
+ let x = this.props.updateDisplay.xDisplay;
+ x = Math.max(0, x);
+ x = Math.min(x, this.props.xMax - 2 * this.props.radius);
+ this.setState({updatedStartPosX: x})
+ this.setState({xPosition: x})
+ if (this.props.color == 'red') {
+ this.props.dataDoc['positionXDisplay'] = x
+ } else {
+ this.props.dataDoc['positionXDisplay2'] = x
+ }
+ }
+
+ if (this.props.updateDisplay.yDisplay != this.getDisplayYPos(this.state.yPosition)) {
+ let y = this.props.updateDisplay.yDisplay;
+ y = Math.max(0, y);
+ y = Math.min(y, this.props.yMax - 2 * this.props.radius);
+ let coordinatePosition = this.getYPosFromDisplay(y);
+ this.setState({updatedStartPosY: coordinatePosition})
+ this.setState({yPosition: coordinatePosition})
+ if (this.props.color == 'red') {
+ this.props.dataDoc['positionYDisplay'] = y
+ } else {
+ this.props.dataDoc['positionYDisplay2'] = y
+ }
+ }
+
+ if (this.props.displayXVelocity != this.state.xVelocity) {
+ let x = this.props.displayXVelocity;
+ this.setState({xVelocity: x})
+ if (this.props.color == 'red') {
+ this.props.dataDoc['velocityXDisplay'] = x
+ } else {
+ this.props.dataDoc['velocityXDisplay2'] = x
+ }
+ }
+
+ if (this.props.displayYVelocity != -this.state.yVelocity) {
+ let y = this.props.displayYVelocity;
+ this.setState({yVelocity: -y})
+ if (this.props.color == 'red') {
+ this.props.dataDoc['velocityYDisplay'] = y
+ } else {
+ this.props.dataDoc['velocityYDisplay2'] = y
+ }
+ }
+ }
+
+ // Prevent bug when switching between sims
+ if (prevProps.startForces != this.props.startForces) {
+ this.setState({xVelocity: this.props.startVelX})
+ this.setState({yVelocity: this.props.startVelY})
+ this.setDisplayValues();
+ }
+
+ // 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.dataDoc['simulationType'] == "One Weight") {
+ let maxYPos = this.state.updatedStartPosY;
+ if (this.props.startVelY != 0) {
+ maxYPos -= (this.props.startVelY * this.props.startVelY) / (2 * Math.abs(this.props.gravity));
+ }
+ if (maxYPos < 0) {
+ maxYPos = 0;
+ }
+ this.setState({maxPosYConservation: maxYPos})
+ }
+ }
+
+ // Check for collisions and update
+ if (!this.props.paused) {
+ if (prevState.timer != this.state.timer) {
+ if (!this.props.noMovement) {
+ let collisions = false;
+ if (
+ this.props.dataDoc['simulationType'] == "One Weight" ||
+ this.props.dataDoc['simulationType'] == "Inclined Plane"
+ ) {
+ const collisionsWithGround = this.checkForCollisionsWithGround();
+ const collisionsWithWalls = this.checkForCollisionsWithWall();
+ collisions = collisionsWithGround || collisionsWithWalls;
+ }
+ if (this.props.dataDoc['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();
+ }
+ }
+ }
+
+ // Reset everything on reset button click
+ if (prevProps.reset != this.props.reset) {
+ this.resetEverything();
+ }
+
+ // Convert from static to kinetic friction if/when weight slips on inclined plane
+ if (prevState.xVelocity != this.state.xVelocity) {
+ if (
+ this.props.dataDoc['simulationType'] == "Inclined Plane" &&
+ Math.abs(this.state.xVelocity) > 0.1 &&
+ this.props.dataDoc['mode'] != "Review" &&
+ !this.state.kineticFriction
+ ) {
+ this.setState({kineticFriction: true})
+ const normalForce: IForce = {
+ description: "Normal Force",
+ magnitude:
+ this.props.mass *
+ Math.abs(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,
+ component: false,
+ };
+ let frictionForce: IForce = {
+ description: "Kinetic Friction Force",
+ magnitude:
+ this.props.mass *
+ this.props.coefficientOfKineticFriction *
+ Math.abs(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,
+ component: false,
+ };
+ // reduce magnitude of friction force if necessary such that block cannot slide up plane
+ let yForce = -Math.abs(this.props.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(this.props.gravity)) /
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+
+ const frictionForceComponent: IForce = {
+ description: "Kinetic Friction Force",
+
+ magnitude:
+ this.props.mass *
+ this.props.coefficientOfKineticFriction *
+ Math.abs(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,
+ component: true,
+ };
+ const normalForceComponent: IForce = {
+ description: "Normal Force",
+ magnitude:
+ this.props.mass *
+ Math.abs(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,
+ component: true,
+ };
+ const gravityParallel: IForce = {
+ description: "Gravity Parallel Component",
+ magnitude:
+ this.props.mass *
+ Math.abs(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,
+ component: true,
+ };
+ const gravityPerpendicular: IForce = {
+ description: "Gravity Perpendicular Component",
+ magnitude:
+ this.props.mass *
+ Math.abs(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,
+ component: true,
+ };
+ const gravityForce: IForce = {
+ description: "Gravity",
+ magnitude: this.props.mass * Math.abs(this.props.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ if (this.props.coefficientOfKineticFriction != 0) {
+ this.props.dataDoc['updatedForces'] = ([gravityForce, normalForce, frictionForce]);
+ this.props.dataDoc['componentForces'] = ([
+ frictionForceComponent,
+ normalForceComponent,
+ gravityParallel,
+ gravityPerpendicular,
+ ]);
+ } else {
+ this.props.dataDoc['updatedForces'] = ([gravityForce, normalForce]);
+ this.props.dataDoc['componentForces'] = ([
+ normalForceComponent,
+ gravityParallel,
+ gravityPerpendicular,
+ ]);
+ }
+ }
+ }
+
+ // Add/remove walls when simulation type changes
+ if (prevProps.simulationType != this.props.simulationType) {
+ let w: IWallProps[] = [];
+ if (this.props.dataDoc['simulationType'] == "One Weight" || this.props.dataDoc['simulationType'] == "Inclined Plane") {
+ w = this.props.wallPositions
+ }
+ this.setState({walls: w})
+ }
+
+ // 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})
+ this.setState({updatedStartPosX: this.props.startPosX})
+ this.setXPosDisplay(this.props.startPosX)
+ }
+ }
+
+ // 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})
+ this.setState({updatedStartPosY: this.props.startPosY ?? 0})
+ this.setYPosDisplay(this.props.startPosY ?? 0)
+ }
+ }
+
+ // Update wedge coordinates
+ if (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);
+ const coord = coordinatePair1 + coordinatePair2 + coordinatePair3;
+ this.setState({coordinates: coord})
+ }
+
+ 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})
+ 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['updatedForces2'] = (this.props.dataDoc['startForces2'])
+ if (this.props.color == 'red') {
+ 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.props.dataDoc['accelerationXDisplay'] = 0
+ this.props.dataDoc['accelerationYDisplay'] = 0
+ } else {
+ this.props.dataDoc['positionXDisplay2'] = this.state.updatedStartPosX
+ this.props.dataDoc['positionYDisplay2'] = this.state.updatedStartPosY
+ this.props.dataDoc['velocityXDisplay2'] = this.props.startVelX ?? 0
+ this.props.dataDoc['velocityYDisplay2'] = this.props.startVelY ?? 0
+ this.props.dataDoc['accelerationXDisplay2'] = 0
+ this.props.dataDoc['accelerationYDisplay2'] = 0
+ }
+ this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle'] ?? 0 * 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 * Math.abs(this.props.gravity) * 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
+ 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) * this.props.layoutDoc._width;
+ 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
+ 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) * this.props.layoutDoc._height;
+ 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) * this.props.layoutDoc._height;
+ 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
+ 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 = this.props.updatedForces;
+ if (this.props.dataDoc['simulationType'] == "Pendulum") {
+ forces = this.getNewPendulumForces(newXPos, newYPos, newXVel, newYVel);
+ } else if (this.props.dataDoc['simulationType'] == "Spring") {
+ forces = this.getNewSpringForces(newYPos);
+ } else if (this.props.dataDoc['simulationType'] == "Circular Motion") {
+ forces = this.getNewCircularMotionForces(newXPos, newYPos);
+ }
+ const newDeltaXVel = this.getNewAccelerationX(forces);
+ const newDeltaYVel = this.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
+ update = () => {
+ let startXVel = this.state.xVelocity;
+ let startYVel = this.state.yVelocity;
+ let xPos = this.state.xPosition;
+ let yPos = this.state.yPosition;
+ let xVel = this.state.xVelocity;
+ let yVel = this.state.yVelocity;
+ let forces: IForce[] = this.props.dataDoc['updatedForces'];
+ if (this.props.dataDoc['simulationType'] == "Pendulum") {
+ forces = this.getNewPendulumForces(xPos, yPos, xVel, yVel);
+ } else if (this.props.dataDoc['simulationType'] == "Spring") {
+ forces = this.getNewSpringForces(yPos);
+ } else if (this.props.dataDoc['simulationType'] == "Circular Motion") {
+ forces = this.getNewCircularMotionForces(xPos, yPos);
+ }
+ const xAcc = this.getNewAccelerationX(forces);
+ const yAcc = this.getNewAccelerationY(forces);
+ 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 +=
+ ((this.props.timestepSize * 1.0) / 6.0) *
+ (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel);
+ yVel +=
+ ((this.props.timestepSize * 1.0) / 6.0) *
+ (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel);
+ xPos +=
+ ((this.props.timestepSize * 1.0) / 6.0) *
+ (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos);
+ yPos +=
+ ((this.props.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 < this.props.springRestLength) {
+ let equilibriumPos =
+ this.props.springRestLength + (this.props.mass * Math.abs(this.props.gravity)) / this.props.springConstant;
+ let amplitude = Math.abs(equilibriumPos - this.props.springStartLength);
+ yPos = equilibriumPos - amplitude;
+ } else if (startYVel > 0 && yVel < 0 && yPos > this.props.springRestLength) {
+ let equilibriumPos =
+ this.props.springRestLength + (this.props.mass * Math.abs(this.props.gravity)) / this.props.springConstant;
+ let amplitude = Math.abs(equilibriumPos - this.props.springStartLength);
+ yPos = equilibriumPos + amplitude;
+ }
+ }
+ if (this.props.dataDoc['simulationType'] == "Pendulum") {
+ let 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;
+ }
+ }
+ if (this.props.dataDoc['simulationType'] == "One Weight") {
+ if (yPos < this.state.maxPosYConservation) {
+ yPos = this.state.maxPosYConservation;
+ }
+ }
+ this.setState({xVelocity: xVel});
+ this.setState({yVelocity: yVel});
+ this.setState({xPosition: xPos});
+ this.setState({yPosition: yPos});
+ let forcesn = this.props.dataDoc['updatedForces']
+ if (this.props.dataDoc['simulationType'] == "Pendulum") {
+ forcesn = this.getNewPendulumForces(xPos, yPos, xVel, yVel);
+ } else if (this.props.dataDoc['simulationType'] == "Spring") {
+ forcesn = this.getNewSpringForces(yPos);
+ } else if (this.props.dataDoc['simulationType'] == "Circular Motion") {
+ forcesn = this.getNewCircularMotionForces(xPos, yPos);
+ }
+ this.props.dataDoc['updatedForces'] = (forcesn);
+
+ // set component forces if they change
+ if (this.props.dataDoc['simulationType'] == "Pendulum") {
+ let x = this.props.xMax / 2 - xPos - this.props.radius;
+ let 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 mag =
+ this.props.mass * Math.abs(this.props.gravity) * Math.cos((oppositeAngle * Math.PI) / 180) +
+ (this.props.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(this.props.gravity) * Math.cos(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: 270 - (90 - angle),
+ component: true,
+ };
+ const gravityPerpendicular: IForce = {
+ description: "Gravity Perpendicular Component",
+ magnitude: Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: -(90 - angle),
+ component: true,
+ };
+ if (Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180) < 0) {
+ gravityPerpendicular.magnitude = Math.abs(
+ Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180)
+ );
+ gravityPerpendicular.directionInDegrees = 180 - (90 - angle);
+ }
+ this.props.dataDoc['componentForces'] = ([
+ tensionComponent,
+ gravityParallel,
+ gravityPerpendicular,
+ ]);
+ }
+ };
+
+ // Render weight, spring, rod(s), vectors
+ render () {
+ return (
+ <div>
+ <div
+ className="weightContainer"
+ onPointerDown={(e) => {
+ if (this.draggable) {
+ this.props.dataDoc['paused'] = true;
+ this.setState({dragging: true});
+ this.setState({clickPositionX: e.clientX});
+ this.setState({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.dataDoc['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.dataDoc['positionYDisplay'] =
+ Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100
+ if (this.props.dataDoc['simulationType'] != "Pulley") {
+ this.setState({xPosition: newX});
+ this.props.dataDoc['positionXDisplay'] = newX
+ }
+ if (this.props.dataDoc['simulationType'] != "Suspension") {
+ if (this.props.dataDoc['simulationType'] != "Pulley") {
+ this.setState({updatedStartPosX: newX});
+ }
+ this.setState({updatedStartPosY: newY});
+ }
+ this.setState({clickPositionX: e.clientX});
+ this.setState({clickPositionY: e.clientY});
+ this.setDisplayValues();
+ }
+ }}
+ onPointerUp={(e) => {
+ if (this.state.dragging) {
+ if (
+ this.props.dataDoc['simulationType'] != "Pendulum" &&
+ this.props.dataDoc['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.dataDoc['simulationType'] == "Spring") {
+ this.props.dataDoc.springStartLength = newY
+ }
+ if (this.props.dataDoc['simulationType'] == "Suspension") {
+ let x1rod = (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200;
+ let x2rod = (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius;
+ let deltaX1 = this.state.xPosition + this.props.radius - x1rod;
+ let deltaX2 = x2rod - (this.state.xPosition + this.props.radius);
+ let deltaY = this.state.yPosition + this.props.radius;
+ let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ let dir2T = Math.atan(deltaY / deltaX2);
+ let tensionMag2 =
+ (this.props.mass * Math.abs(this.props.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: this.props.mass * Math.abs(this.props.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ this.props.dataDoc['updatedForces'] = ([tensionForce1, tensionForce2, grav]);
+ }
+ }
+ }}
+ >
+ <div className="weight" style={this.weightStyle}>
+ <p className="weightLabel">{this.props.mass} kg</p>
+ </div>
+ </div>
+ {this.props.dataDoc['simulationType'] == "Spring" && (
+ <div
+ className="spring"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "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 = this.state.xPosition + this.props.radius - 20;
+ xPos2 = this.state.xPosition + this.props.radius + 20;
+ } else {
+ xPos1 = this.state.xPosition + this.props.radius + 20;
+ xPos2 = this.state.xPosition + this.props.radius - 20;
+ }
+ yPos1 = (val * this.state.yPosition) / count;
+ yPos2 = ((val + 1) * this.state.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="rod"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={this.state.xPosition + this.props.radius}
+ y2={this.props.yMin}
+ stroke={"#deb887"}
+ strokeWidth="10"
+ />
+ </svg>
+ </div>
+ )}
+ {this.props.dataDoc['simulationType'] == "Pulley" && (
+ <div
+ className="wheel"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}>
+ <circle
+ cx={(this.props.xMax + this.props.xMin) / 2}
+ cy={this.props.radius}
+ r={this.props.radius * 1.5}
+ fill={"#808080"}
+ />
+ </svg>
+ </div>
+ )}
+ {this.props.dataDoc['simulationType'] == "Suspension" && (
+ <div
+ className="rod"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={(this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200}
+ y2={this.props.yMin}
+ stroke={"#deb887"}
+ strokeWidth="10"
+ />
+ </svg>
+ <p
+ style={{
+ position: "absolute",
+ left: (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200 + 80 + "px",
+ top: 10 + "px",
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {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}
+ °
+ </p>
+ <svg width={this.props.layoutDoc._width + "px"} height={this.props.layoutDoc._height + "px"}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={(this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius}
+ y2={this.props.yMin}
+ stroke={"#deb887"}
+ strokeWidth="10"
+ />
+ </svg>
+
+ <p
+ style={{
+ position: "absolute",
+ left: (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius - 80 + "px",
+ top: 10 + "px",
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {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}
+ °
+ </p>
+ </div>
+ )}
+ {this.props.dataDoc['simulationType'] == "Circular Motion" && (
+ <div
+ className="rod"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={(this.props.xMin + this.props.xMax) / 2}
+ y2={(this.props.yMin + this.props.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,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={this.props.xMax / 2}
+ y2={-5}
+ stroke={"#deb887"}
+ strokeWidth="10"
+ />
+ </svg>
+ {!this.state.dragging && (
+ <div>
+ <p
+ style={{
+ position: "absolute",
+ zIndex: 5,
+ left: this.state.xPosition + "px",
+ top: this.state.yPosition - 70 + "px",
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {Math.round(this.props.pendulumLength)} m
+ </p>
+ <p
+ style={{
+ position: "absolute",
+ left: this.props.xMax / 2 + "px",
+ top: 30 + "px",
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {Math.round(this.props.pendulumAngle * 100) / 100}°
+ </p>
+ </div>
+ )}
+ </div>
+ )}
+ {this.props.dataDoc['simulationType'] == "Inclined Plane" && (
+ <div>
+ <div
+ style={{ position: "absolute", left: "0", top: "0" }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.yMax + "px"}>
+ <polygon points={this.state.coordinates} style={{ fill: "burlywood" }} />
+ </svg>
+ </div>
+
+ <p
+ style={{
+ position: "absolute",
+ left: Math.round(this.props.xMax * 0.5 - 200 + this.props.wedgeWidth - 80) + "px",
+ top: Math.round(this.props.yMax - 40) + "px",
+ }}
+ >
+ {Math.round(
+ ((Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI) * 100
+ ) / 100}
+ °
+ </p>
+ </div>
+ )}
+ {!this.state.dragging && this.props.showAcceleration && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}>
+ <defs>
+ <marker
+ id="accArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="3"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,6 L9,3 z" fill="green" />
+ </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.updatedForces) * 15}
+ y2={this.state.yPosition + this.props.radius + this.getNewAccelerationY(this.props.updatedForces) * 15}
+ stroke={"green"}
+ strokeWidth="5"
+ markerEnd="url(#accArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: this.state.xPosition + this.props.radius + this.state.xAccel * 15 + 25 + "px",
+ top: this.state.yPosition + this.props.radius + this.state.yAccel * 15 + 70 + "px",
+ lineHeight: 1,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 * Math.sqrt(this.state.xAccel * this.state.xAccel + this.state.yAccel * this.state.yAccel)
+ ) / 100}{" "}
+ m/s
+ <sup>2</sup>
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!this.state.dragging && this.props.showVelocity && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}>
+ <defs>
+ <marker
+ id="velArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="3"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,6 L9,3 z" fill="blue" />
+ </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 * 7}
+ y2={this.state.yPosition + this.props.radius + this.state.yVelocity * 7}
+ stroke={"blue"}
+ strokeWidth="5"
+ markerEnd="url(#velArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: this.state.xPosition + this.props.radius + this.state.xVelocity * 7 + 25 + "px",
+ top: this.state.yPosition + this.props.radius + this.state.yVelocity * 7 + "px",
+ lineHeight: 1,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 *
+ Math.sqrt(
+ this.props.displayXVelocity * this.props.displayXVelocity +
+ this.props.displayYVelocity * this.props.displayYVelocity
+ )
+ ) / 100}{" "}
+ m/s
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!this.state.dragging &&
+ this.props.showComponentForces &&
+ this.props.componentForces.map((force, index) => {
+ if (force.magnitude < this.epsilon) {
+ return;
+ }
+ let arrowStartY: number = this.state.yPosition + this.props.radius;
+ const arrowStartX: number = this.state.xPosition + this.props.radius;
+ let arrowEndY: number =
+ arrowStartY -
+ Math.abs(force.magnitude) *
+ 20 *
+ Math.sin((force.directionInDegrees * Math.PI) / 180);
+ const arrowEndX: number =
+ arrowStartX +
+ Math.abs(force.magnitude) *
+ 20 *
+ Math.cos((force.directionInDegrees * Math.PI) / 180);
+
+ let color = "#0d0d0d";
+
+ let labelTop = arrowEndY;
+ let labelLeft = arrowEndX;
+ if (force.directionInDegrees > 90 && force.directionInDegrees < 270) {
+ labelLeft -= 120;
+ } else {
+ labelLeft += 30;
+ }
+ if (force.directionInDegrees >= 0 && force.directionInDegrees < 180) {
+ labelTop += 40;
+ } else {
+ labelTop -= 40;
+ }
+ labelTop = Math.min(labelTop, this.props.yMax + 50);
+ labelTop = Math.max(labelTop, this.props.yMin);
+ labelLeft = Math.min(labelLeft, this.props.xMax - 60);
+ labelLeft = Math.max(labelLeft, this.props.xMin);
+
+ return (
+ <div key={index}>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: this.props.xMin,
+ top: this.props.yMin,
+ }}
+ >
+ <svg
+ width={this.props.xMax - this.props.xMin + "px"}
+ height={this.props.layoutDoc._height + "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",
+ lineHeight: 1,
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {force.description && <p>{force.description}</p>}
+ {!force.description && <p>Force</p>}
+ {this.props.showForceMagnitudes && (
+ <p>{Math.round(100 * force.magnitude) / 100} N</p>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ {!this.state.dragging &&
+ this.props.showForces && this.props.updatedForces &&
+ this.props.updatedForces.map((force, index) => {
+ if (force.magnitude < this.epsilon) {
+ return;
+ }
+ let arrowStartY: number = this.state.yPosition + this.props.radius;
+ const arrowStartX: number = this.state.xPosition + this.props.radius;
+ let arrowEndY: number =
+ arrowStartY -
+ Math.abs(force.magnitude) *
+ 20 *
+ Math.sin((force.directionInDegrees * Math.PI) / 180);
+ const arrowEndX: number =
+ arrowStartX +
+ Math.abs(force.magnitude) *
+ 20 *
+ Math.cos((force.directionInDegrees * Math.PI) / 180);
+
+ let color = "#0d0d0d";
+
+ let labelTop = arrowEndY;
+ let labelLeft = arrowEndX;
+ if (force.directionInDegrees > 90 && force.directionInDegrees < 270) {
+ labelLeft -= 120;
+ } else {
+ labelLeft += 30;
+ }
+ if (force.directionInDegrees >= 0 && force.directionInDegrees < 180) {
+ labelTop += 40;
+ } else {
+ labelTop -= 40;
+ }
+ labelTop = Math.min(labelTop, this.props.yMax + 50);
+ labelTop = Math.max(labelTop, this.props.yMin);
+ labelLeft = Math.min(labelLeft, this.props.xMax - 60);
+ labelLeft = Math.max(labelLeft, this.props.xMin);
+
+ return (
+ <div key={index}>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: this.props.xMin,
+ top: this.props.yMin,
+ }}
+ >
+ <svg
+ width={this.props.xMax - this.props.xMin + "px"}
+ height={this.props.layoutDoc._height + "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",
+ lineHeight: 1,
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {force.description && <p>{force.description}</p>}
+ {!force.description && <p>Force</p>}
+ {this.props.showForceMagnitudes && (
+ <p>{Math.round(100 * force.magnitude) / 100} N</p>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ )}
+};