From 7b2553514bb000eb7f618eb0f0d653baee78742c Mon Sep 17 00:00:00 2001
From: Eric <ericmabr@gmail.com>
Date: Sun, 13 Aug 2023 15:43:53 -0400
Subject: 1

---
 .../nodes/PhysicsBox/PhysicsSimulationWeight.tsx   | 990 +++++++++++++++++++++
 1 file changed, 990 insertions(+)
 create mode 100644 src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx

(limited to 'src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx')

diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx
new file mode 100644
index 000000000..2165c8ba9
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx
@@ -0,0 +1,990 @@
+import { computed, IReactionDisposer, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import './PhysicsSimulationBox.scss';
+import React = require('react');
+
+interface IWallProps {
+    length: number;
+    xPos: number;
+    yPos: number;
+    angleInDegrees: number;
+}
+interface IForce {
+    description: string;
+    magnitude: number;
+    directionInDegrees: number;
+}
+export interface IWeightProps {
+    pause: () => void;
+    panelWidth: () => number;
+    panelHeight: () => number;
+    resetRequest: () => number;
+    circularMotionRadius: number;
+    coefficientOfKineticFriction: number;
+    color: string;
+    componentForces: () => IForce[];
+    setComponentForces: (x: IForce[]) => {};
+    displayXVelocity: number;
+    displayYVelocity: number;
+    elasticCollisions: boolean;
+    gravity: number;
+    mass: number;
+    simulationMode: string;
+    noMovement: boolean;
+    paused: boolean;
+    pendulumAngle: number;
+    pendulumLength: number;
+    radius: number;
+    showAcceleration: boolean;
+    showComponentForces: boolean;
+    showForceMagnitudes: boolean;
+    showForces: boolean;
+    showVelocity: boolean;
+    simulationSpeed: number;
+    simulationType: string;
+    springConstant: number;
+    springRestLength: number;
+    springStartLength: number;
+    startForces: () => IForce[];
+    startPendulumAngle: number;
+    startPendulumLength: number;
+    startPosX: number;
+    startPosY: number;
+    startVelX: number;
+    startVelY: number;
+    timestepSize: number;
+    updateMassPosX: number;
+    updateMassPosY: number;
+    forcesUpdated: () => IForce[];
+    setForcesUpdated: (x: IForce[]) => {};
+    setPosition: (x: number | undefined, y: number | undefined) => void;
+    setVelocity: (x: number | undefined, y: number | undefined) => void;
+    setAcceleration: (x: number, y: number) => void;
+    setPendulumAngle: (ang: number | undefined, length: number | undefined) => void;
+    setSpringLength: (length: number) => void;
+    wallPositions: IWallProps[];
+    wedgeHeight: number;
+    wedgeWidth: number;
+    xMax: number;
+    xMin: number;
+    yMax: number;
+    yMin: number;
+}
+
+interface IState {
+    angleLabel: number;
+    clickPositionX: number;
+    clickPositionY: number;
+    coordinates: string;
+    dragging: boolean;
+    kineticFriction: boolean;
+    maxPosYConservation: number;
+    timer: number;
+    updatedStartPosX: any;
+    updatedStartPosY: any;
+    xPosition: number;
+    xVelocity: number;
+    yPosition: number;
+    yVelocity: number;
+    xAccel: number;
+    yAccel: number;
+}
+@observer
+export default class Weight extends React.Component<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,
+            updatedStartPosY: this.props.startPosY,
+            xPosition: this.props.startPosX,
+            xVelocity: this.props.startVelX,
+            yPosition: this.props.startPosY,
+            yVelocity: this.props.startVelY,
+            xAccel: 0,
+            yAccel: 0,
+        };
+    }
+
+    _timer: NodeJS.Timeout | undefined;
+    _resetDisposer: IReactionDisposer | undefined;
+
+    componentWillUnmount() {
+        this._timer && clearTimeout(this._timer);
+        this._resetDisposer?.();
+    }
+    componentWillUpdate(nextProps: Readonly<IWeightProps>, nextState: Readonly<IState>, nextContext: any): void {
+        if (nextProps.paused) {
+            this._timer && clearTimeout(this._timer);
+            this._timer = undefined;
+        } else if (this.props.paused) {
+            this._timer && clearTimeout(this._timer);
+            this._timer = setInterval(() => this.setState({ timer: this.state.timer + 1 }), 50);
+        }
+    }
+
+    // Constants
+    @computed get draggable() {
+        return !['Inclined Plane', 'Pendulum'].includes(this.props.simulationType) && this.props.simulationMode === 'Freeform';
+    }
+    @computed get panelHeight() {
+        return Math.max(800, this.props.panelHeight()) + 'px';
+    }
+    @computed get panelWidth() {
+        return Math.max(1000, this.props.panelWidth()) + 'px';
+    }
+
+    @computed get walls() {
+        return ['One Weight', 'Inclined Plane'].includes(this.props.simulationType) ? this.props.wallPositions : [];
+    }
+    epsilon = 0.0001;
+    labelBackgroundColor = `rgba(255,255,255,0.5)`;
+
+    // Variables
+    weightStyle = {
+        alignItems: 'center',
+        backgroundColor: this.props.color,
+        borderColor: 'black',
+        borderRadius: 50 + '%',
+        borderStyle: 'solid',
+        display: 'flex',
+        height: 2 * this.props.radius + 'px',
+        justifyContent: 'center',
+        left: this.props.startPosX + 'px',
+        position: 'absolute' as 'absolute',
+        top: this.props.startPosY + 'px',
+        touchAction: 'none',
+        width: 2 * this.props.radius + 'px',
+        zIndex: 5,
+    };
+
+    // Helper function to go between display and real values
+    getDisplayYPos = (yPos: number) => this.props.yMax - yPos - 2 * this.props.radius + 5;
+    gravityForce = (): IForce => ({
+        description: 'Gravity',
+        magnitude: this.props.mass * this.props.gravity,
+        directionInDegrees: 270,
+    });
+    // Update display values when simulation updates
+    setDisplayValues = (xPos: number = this.state.xPosition, yPos: number = this.state.yPosition, xVel: number = this.state.xVelocity, yVel: number = this.state.yVelocity) => {
+        this.props.setPosition(xPos, this.getDisplayYPos(yPos));
+        this.props.setVelocity(xVel, yVel);
+        const xAccel = Math.round(this.getNewAccelerationX(this.props.forcesUpdated()) * 100) / 100;
+        const yAccel = (-1 * Math.round(this.getNewAccelerationY(this.props.forcesUpdated()) * 100)) / 100;
+        this.props.setAcceleration(xAccel, yAccel);
+        this.setState({ xAccel, yAccel });
+    };
+    componentDidMount() {
+        this._resetDisposer = reaction(() => this.props.resetRequest(), this.resetEverything);
+    }
+    componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void {
+        if (prevProps.simulationType != this.props.simulationType) {
+            this.setState({ xVelocity: this.props.startVelX, yVelocity: this.props.startVelY });
+            this.setDisplayValues();
+        }
+
+        // Change pendulum angle from input field
+        if (prevProps.startPendulumAngle != this.props.startPendulumAngle || prevProps.startPendulumLength !== this.props.startPendulumLength) {
+            const length = this.props.startPendulumLength;
+            const x = length * Math.cos(((90 - this.props.startPendulumAngle) * Math.PI) / 180);
+            const y = length * Math.sin(((90 - this.props.startPendulumAngle) * Math.PI) / 180);
+            const xPosition = this.props.xMax / 2 - x - this.props.radius;
+            const yPosition = y - this.props.radius - 5;
+            this.setState({ xPosition, yPosition, updatedStartPosX: xPosition, updatedStartPosY: yPosition });
+            this.props.setPendulumAngle(this.props.startPendulumAngle, this.props.startPendulumLength);
+        }
+
+        // When display values updated by user, update real value
+        if (prevProps.updateMassPosX !== this.props.updateMassPosX) {
+            const x = Math.min(Math.max(0, this.props.updateMassPosX), this.props.xMax - 2 * this.props.radius);
+            this.setState({ updatedStartPosX: x, xPosition: x });
+            this.props.setPosition(x, undefined);
+        }
+        if (prevProps.updateMassPosY != this.props.updateMassPosY) {
+            const y = Math.min(Math.max(0, this.props.updateMassPosY), this.props.yMax - 2 * this.props.radius);
+            const coordinatePosition = this.getDisplayYPos(y);
+            this.setState({ yPosition: coordinatePosition, updatedStartPosY: coordinatePosition });
+            this.props.setPosition(undefined, this.getDisplayYPos(y));
+
+            if (this.props.displayXVelocity != this.state.xVelocity) {
+                this.setState({ xVelocity: this.props.displayXVelocity });
+                this.props.setVelocity(this.props.displayXVelocity, undefined);
+            }
+
+            if (this.props.displayYVelocity != -this.state.yVelocity) {
+                this.setState({ yVelocity: -this.props.displayYVelocity });
+                this.props.setVelocity(undefined, this.props.displayYVelocity);
+            }
+        }
+
+        // Make sure weight doesn't go above max height
+        if ((prevState.updatedStartPosY != this.state.updatedStartPosY || prevProps.startVelY != this.props.startVelY) && !isNaN(this.state.updatedStartPosY) && !isNaN(this.props.startVelY)) {
+            if (this.props.simulationType == 'One Weight') {
+                let maxYPos = this.state.updatedStartPosY;
+                if (this.props.startVelY != 0) {
+                    maxYPos -= (this.props.startVelY * this.props.startVelY) / (2 * this.props.gravity);
+                }
+                if (maxYPos < 0) maxYPos = 0;
+
+                this.setState({ maxPosYConservation: maxYPos });
+            }
+        }
+
+        // Check for collisions and update
+        if (!this.props.paused && !this.props.noMovement && prevState.timer != this.state.timer) {
+            let collisions = false;
+            if (this.props.simulationType == 'One Weight' || this.props.simulationType == 'Inclined Plane') {
+                const collisionsWithGround = this.checkForCollisionsWithGround();
+                const collisionsWithWalls = this.checkForCollisionsWithWall();
+                collisions = collisionsWithGround || collisionsWithWalls;
+            }
+            if (this.props.simulationType == 'Pulley') {
+                if (this.state.yPosition <= this.props.yMin + 100 || this.state.yPosition >= this.props.yMax - 100) {
+                    collisions = true;
+                }
+            }
+            if (!collisions) this.update();
+
+            this.setDisplayValues();
+        }
+
+        // Convert from static to kinetic friction if/when weight slips on inclined plane
+        if (prevState.xVelocity != this.state.xVelocity) {
+            if (this.props.simulationType == 'Inclined Plane' && Math.abs(this.state.xVelocity) > 0.1 && this.props.simulationMode != 'Review' && !this.state.kineticFriction) {
+                this.setState({ kineticFriction: true });
+                const normalForce: IForce = {
+                    description: 'Normal Force',
+                    magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+                    directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+                };
+                const frictionForce: IForce = {
+                    description: 'Kinetic Friction Force',
+                    magnitude: this.props.mass * this.props.coefficientOfKineticFriction * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+                    directionInDegrees: 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+                };
+                // reduce magnitude of friction force if necessary such that block cannot slide up plane
+                // prettier-ignore
+                const yForce = - this.props.gravity +
+                        normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
+                        frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+                if (yForce > 0) {
+                    frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + this.props.gravity) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+                }
+
+                const normalForceComponent: IForce = {
+                    description: 'Normal Force',
+                    magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+                    directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+                };
+                const gravityParallel: IForce = {
+                    description: 'Gravity Parallel Component',
+                    magnitude: this.props.mass * this.props.gravity * Math.sin(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+                    directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI + 180,
+                };
+                const gravityPerpendicular: IForce = {
+                    description: 'Gravity Perpendicular Component',
+                    magnitude: this.props.mass * this.props.gravity * Math.cos(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+                    directionInDegrees: 360 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+                };
+                const kineticFriction = this.props.coefficientOfKineticFriction != 0 ? [frictionForce] : [];
+                this.props.setForcesUpdated([this.gravityForce(), normalForce, ...kineticFriction]);
+                this.props.setComponentForces([normalForceComponent, gravityParallel, gravityPerpendicular, ...kineticFriction]);
+            }
+        }
+
+        // Update x position when start pos x changes
+        if (prevProps.startPosX != this.props.startPosX) {
+            if (this.props.paused && !isNaN(this.props.startPosX)) {
+                this.setState({ xPosition: this.props.startPosX, updatedStartPosX: this.props.startPosX });
+                this.props.setPosition(this.props.startPosX, undefined);
+            }
+        }
+
+        // Update y position when start pos y changes TODO debug
+        if (prevProps.startPosY != this.props.startPosY) {
+            if (this.props.paused && !isNaN(this.props.startPosY)) {
+                this.setState({ yPosition: this.props.startPosY, updatedStartPosY: this.props.startPosY ?? 0 });
+                this.props.setPosition(undefined, this.getDisplayYPos(this.props.startPosY));
+            }
+        }
+
+        // Update wedge coordinates
+        if (!this.state.coordinates || this.props.yMax !== prevProps.yMax || prevProps.wedgeWidth != this.props.wedgeWidth || prevProps.wedgeHeight != this.props.wedgeHeight) {
+            const left = this.props.xMax * 0.25;
+            const coordinatePair1 = Math.round(left) + ',' + this.props.yMax + ' ';
+            const coordinatePair2 = Math.round(left + this.props.wedgeWidth) + ',' + this.props.yMax + ' ';
+            const coordinatePair3 = Math.round(left) + ',' + (this.props.yMax - this.props.wedgeHeight);
+            this.setState({ coordinates: coordinatePair1 + coordinatePair2 + coordinatePair3 });
+        }
+
+        if (this.state.xPosition != prevState.xPosition || this.state.yPosition != prevState.yPosition) {
+            this.weightStyle = {
+                alignItems: 'center',
+                backgroundColor: this.props.color,
+                borderColor: 'black',
+                borderRadius: 50 + '%',
+                borderStyle: 'solid',
+                display: 'flex',
+                height: 2 * this.props.radius + 'px',
+                justifyContent: 'center',
+                left: this.state.xPosition + 'px',
+                position: 'absolute' as 'absolute',
+                top: this.state.yPosition + 'px',
+                touchAction: 'none',
+                width: 2 * this.props.radius + 'px',
+                zIndex: 5,
+            };
+        }
+    }
+
+    // Reset simulation on reset button click
+    resetEverything = () => {
+        this.setState({
+            kineticFriction: false,
+            xPosition: this.state.updatedStartPosX,
+            yPosition: this.state.updatedStartPosY,
+            xVelocity: this.props.startVelX,
+            yVelocity: this.props.startVelY,
+            angleLabel: Math.round(this.props.pendulumAngle * 100) / 100,
+        });
+        this.props.setPendulumAngle(this.props.startPendulumAngle, undefined);
+        this.props.setForcesUpdated(this.props.startForces());
+        this.props.setPosition(this.state.updatedStartPosX, this.state.updatedStartPosY);
+        this.props.setVelocity(this.props.startVelX, this.props.startVelY);
+        this.props.setAcceleration(0, 0);
+        setTimeout(() => this.setState({ timer: this.state.timer + 1 }));
+    };
+
+    // Compute x acceleration from forces, F=ma
+    getNewAccelerationX = (forceList: IForce[]) => {
+        // prettier-ignore
+        return forceList.reduce((newXacc, force) => 
+              newXacc + (force.magnitude * Math.cos((force.directionInDegrees * Math.PI) / 180)) / this.props.mass, 0);
+    };
+
+    // Compute y acceleration from forces, F=ma
+    getNewAccelerationY = (forceList: IForce[]) => {
+        // prettier-ignore
+        return forceList.reduce((newYacc, force) => 
+              newYacc + (-1 * (force.magnitude * Math.sin((force.directionInDegrees * Math.PI) / 180))) / this.props.mass, 0);
+    };
+
+    // Compute uniform circular motion forces given x, y positions
+    getNewCircularMotionForces = (xPos: number, yPos: number): IForce[] => {
+        const deltaX = (this.props.xMin + this.props.xMax) / 2 - (xPos + this.props.radius);
+        const deltaY = yPos + this.props.radius - (this.props.yMin + this.props.yMax) / 2;
+        return [
+            {
+                description: 'Centripetal Force',
+                magnitude: (this.props.startVelX ** 2 * this.props.mass) / this.props.circularMotionRadius,
+                directionInDegrees: (Math.atan2(deltaY, deltaX) * 180) / Math.PI,
+            },
+        ];
+    };
+
+    // Compute spring forces given y position
+    getNewSpringForces = (yPos: number): IForce[] => {
+        const yPosPlus = yPos - this.props.springRestLength > 0;
+        const yPosMinus = yPos - this.props.springRestLength < 0;
+        return [
+            this.gravityForce(),
+            {
+                description: 'Spring Force',
+                magnitude: this.props.springConstant * (yPos - this.props.springRestLength) * (yPosPlus ? 1 : yPosMinus ? -1 : 0),
+                directionInDegrees: yPosPlus ? 90 : 270,
+            },
+        ];
+    };
+
+    // Compute pendulum forces given position, velocity
+    getNewPendulumForces = (xPos: number, yPos: number, xVel: number, yVel: number): IForce[] => {
+        const x = this.props.xMax / 2 - xPos - this.props.radius;
+        const y = yPos + this.props.radius + 5;
+        const angle = (ang => (ang < 0 ? ang + 180 : ang))((Math.atan(y / x) * 180) / Math.PI);
+
+        let oppositeAngle = 90 - angle;
+        if (oppositeAngle < 0) {
+            oppositeAngle = 90 - (180 - angle);
+        }
+
+        const pendulumLength = Math.sqrt(x * x + y * y);
+        this.props.setPendulumAngle(oppositeAngle, undefined);
+
+        const mag = this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength;
+
+        return [
+            this.gravityForce(),
+            {
+                description: 'Tension',
+                magnitude: mag,
+                directionInDegrees: angle,
+            },
+        ];
+    };
+
+    // Check for collisions in x direction
+    checkForCollisionsWithWall = () => {
+        let collision = false;
+        if (this.state.xVelocity !== 0) {
+            this.walls
+                .filter(wall => wall.angleInDegrees === 90)
+                .forEach(wall => {
+                    const wallX = (wall.xPos / 100) * this.props.panelWidth();
+                    const minX = this.state.xPosition < wallX && wall.xPos < 0.35;
+                    const maxX = this.state.xPosition + 2 * this.props.radius >= wallX && wall.xPos > 0.35;
+                    if (minX || maxX) {
+                        this.setState({
+                            xPosition: minX ? wallX + 0.01 : wallX - 2 * this.props.radius - 0.01,
+                            xVelocity: this.props.elasticCollisions ? -this.state.xVelocity : 0,
+                        });
+                        collision = true;
+                    }
+                });
+        }
+        return collision;
+    };
+
+    // Check for collisions in y direction
+    checkForCollisionsWithGround = () => {
+        let collision = false;
+        const minY = this.state.yPosition;
+        const maxY = this.state.yPosition + 2 * this.props.radius;
+        if (this.state.yVelocity > 0) {
+            this.walls.forEach(wall => {
+                if (wall.angleInDegrees == 0 && wall.yPos > 0.4) {
+                    const groundY = (wall.yPos / 100) * this.props.panelHeight();
+                    const gravity = this.gravityForce();
+                    if (maxY > groundY) {
+                        this.setState({ yPosition: groundY - 2 * this.props.radius - 0.01 });
+                        if (this.props.elasticCollisions) {
+                            this.setState({ yVelocity: -this.state.yVelocity });
+                        } else {
+                            this.setState({ yVelocity: 0 });
+                            const normalForce: IForce = {
+                                description: 'Normal force',
+                                magnitude: gravity.magnitude,
+                                directionInDegrees: -gravity.directionInDegrees,
+                            };
+                            this.props.setForcesUpdated([gravity, normalForce]);
+                            if (this.props.simulationType === 'Inclined Plane') {
+                                this.props.setComponentForces([gravity, normalForce]);
+                            }
+                        }
+                        collision = true;
+                    }
+                }
+            });
+        }
+        if (this.state.yVelocity < 0) {
+            this.walls.forEach(wall => {
+                if (wall.angleInDegrees == 0 && wall.yPos < 0.4) {
+                    const groundY = (wall.yPos / 100) * this.props.panelHeight();
+                    if (minY < groundY) {
+                        this.setState({
+                            yPosition: groundY + 0.01,
+                            yVelocity: this.props.elasticCollisions ? -this.state.yVelocity : 0,
+                        });
+                        collision = true;
+                    }
+                }
+            });
+        }
+        return collision;
+    };
+
+    // Called at each RK4 step
+    evaluate = (currentXPos: number, currentYPos: number, currentXVel: number, currentYVel: number, currdeltaXPos: number, currdeltaYPos: number, currdeltaXVel: number, currdeltaYVel: number, dt: number) => {
+        const xPos = currentXPos + currdeltaXPos * dt;
+        const yPos = currentYPos + currdeltaYPos * dt;
+        const xVel = currentXVel + currdeltaXVel * dt;
+        const yVel = currentYVel + currdeltaYVel * dt;
+        const forces = this.getForces(xPos, yPos, xVel, yVel);
+        return {
+            xPos,
+            yPos,
+            xVel,
+            yVel,
+            deltaXPos: xVel,
+            deltaYPos: yVel,
+            deltaXVel: this.getNewAccelerationX(forces),
+            deltaYVel: this.getNewAccelerationY(forces),
+        };
+    };
+
+    getForces = (xPos: number, yPos: number, xVel: number, yVel: number) => {
+        // prettier-ignore
+        switch (this.props.simulationType) {
+            case 'Pendulum':        return this.getNewPendulumForces(xPos, yPos, xVel, yVel);
+            case 'Spring'  :        return this.getNewSpringForces(yPos);
+            case 'Circular Motion': return this.getNewCircularMotionForces(xPos, yPos);
+            default:                return this.props.forcesUpdated();
+        }
+    };
+
+    // Update position, velocity using RK4 method
+    update = () => {
+        const startXVel = this.state.xVelocity;
+        const startYVel = this.state.yVelocity;
+        let xPos = this.state.xPosition;
+        let yPos = this.state.yPosition;
+        let xVel = this.state.xVelocity;
+        let yVel = this.state.yVelocity;
+        const forces = this.getForces(xPos, yPos, xVel, yVel);
+        const xAcc = this.getNewAccelerationX(forces);
+        const yAcc = this.getNewAccelerationY(forces);
+        const coeff = (this.props.timestepSize * 1.0) / 6.0;
+        for (let i = 0; i < this.props.simulationSpeed; i++) {
+            const k1 = this.evaluate(xPos, yPos, xVel, yVel, xVel, yVel, xAcc, yAcc, 0);
+            const k2 = this.evaluate(xPos, yPos, xVel, yVel, k1.deltaXPos, k1.deltaYPos, k1.deltaXVel, k1.deltaYVel, this.props.timestepSize * 0.5);
+            const k3 = this.evaluate(xPos, yPos, xVel, yVel, k2.deltaXPos, k2.deltaYPos, k2.deltaXVel, k2.deltaYVel, this.props.timestepSize * 0.5);
+            const k4 = this.evaluate(xPos, yPos, xVel, yVel, k3.deltaXPos, k3.deltaYPos, k3.deltaXVel, k3.deltaYVel, this.props.timestepSize);
+
+            xVel += coeff * (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel);
+            yVel += coeff * (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel);
+            xPos += coeff * (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos);
+            yPos += coeff * (k1.deltaYPos + 2 * (k2.deltaYPos + k3.deltaYPos) + k4.deltaYPos);
+        }
+        // make sure harmonic motion maintained and errors don't propagate
+        switch (this.props.simulationType) {
+            case 'Spring':
+                const equilibriumPos = this.props.springRestLength + (this.props.mass * this.props.gravity) / this.props.springConstant;
+                const amplitude = Math.abs(equilibriumPos - this.props.springStartLength);
+                if (startYVel < 0 && yVel > 0 && yPos < this.props.springRestLength) {
+                    yPos = equilibriumPos - amplitude;
+                } else if (startYVel > 0 && yVel < 0 && yPos > this.props.springRestLength) {
+                    yPos = equilibriumPos + amplitude;
+                }
+                break;
+            case 'Pendulum':
+                const startX = this.state.updatedStartPosX;
+                if (startXVel <= 0 && xVel > 0) {
+                    xPos = this.state.updatedStartPosX;
+                    if (this.state.updatedStartPosX > this.props.xMax / 2) {
+                        xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius;
+                    }
+                    yPos = this.props.startPosY;
+                } else if (startXVel >= 0 && xVel < 0) {
+                    xPos = this.state.updatedStartPosX;
+                    if (this.state.updatedStartPosX < this.props.xMax / 2) {
+                        xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius;
+                    }
+                    yPos = this.props.startPosY;
+                }
+                break;
+            case 'One Weight':
+                if (yPos < this.state.maxPosYConservation) {
+                    yPos = this.state.maxPosYConservation;
+                }
+        }
+        this.setState({ xVelocity: xVel, yVelocity: yVel, xPosition: xPos, yPosition: yPos });
+
+        const forcesn = this.getForces(xPos, yPos, xVel, yVel);
+        this.props.setForcesUpdated(forcesn);
+
+        // set component forces if they change
+        if (this.props.simulationType == 'Pendulum') {
+            const x = this.props.xMax / 2 - xPos - this.props.radius;
+            const y = yPos + this.props.radius + 5;
+            let angle = (Math.atan(y / x) * 180) / Math.PI;
+            if (angle < 0) {
+                angle += 180;
+            }
+            let oppositeAngle = 90 - angle;
+            if (oppositeAngle < 0) {
+                oppositeAngle = 90 - (180 - angle);
+            }
+
+            const pendulumLength = Math.sqrt(x * x + y * y);
+
+            const tensionComponent: IForce = {
+                description: 'Tension',
+                magnitude: this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength,
+                directionInDegrees: angle,
+            };
+            const gravityParallel: IForce = {
+                description: 'Gravity Parallel Component',
+                magnitude: this.props.gravity * Math.cos(((90 - angle) * Math.PI) / 180),
+                directionInDegrees: 270 - (90 - angle),
+            };
+            const gravityPerpendicular: IForce = {
+                description: 'Gravity Perpendicular Component',
+                magnitude: this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180),
+                directionInDegrees: -(90 - angle),
+            };
+            if (this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180) < 0) {
+                gravityPerpendicular.magnitude = Math.abs(this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180));
+                gravityPerpendicular.directionInDegrees = 180 - (90 - angle);
+            }
+            this.props.setComponentForces([tensionComponent, gravityParallel, gravityPerpendicular]);
+        }
+    };
+
+    renderForce = (force: IForce, index: number, asComponent: boolean, color = '#0d0d0d') => {
+        if (force.magnitude < this.epsilon) return;
+
+        const angle = (force.directionInDegrees * Math.PI) / 180;
+        const arrowStartY = this.state.yPosition + this.props.radius - this.props.radius * Math.sin(angle);
+        const arrowStartX = this.state.xPosition + this.props.radius + this.props.radius * Math.cos(angle);
+        const arrowEndY = arrowStartY - Math.abs(force.magnitude) * Math.sin(angle) - this.props.radius * Math.sin(angle);
+        const arrowEndX = arrowStartX + Math.abs(force.magnitude) * Math.cos(angle) + this.props.radius * Math.cos(angle);
+
+        let labelTop = arrowEndY + (force.directionInDegrees >= 0 && force.directionInDegrees < 180 ? 40 : -40);
+        let labelLeft = arrowEndX + (force.directionInDegrees > 90 && force.directionInDegrees < 270 ? -120 : 30);
+
+        labelTop = Math.max(Math.min(labelTop, this.props.yMax + 50), this.props.yMin);
+        labelLeft = Math.max(Math.min(labelLeft, this.props.xMax - 60), this.props.xMin);
+
+        return (
+            <div key={index} style={{ zIndex: 6, position: 'absolute' }}>
+                <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.panelHeight}>
+                        <defs>
+                            <marker id="forceArrow" markerWidth="4" markerHeight="4" refX="0" refY="2" orient="auto" markerUnits="strokeWidth">
+                                <path d="M0,0 L0,4 L4,2 z" fill={color} />
+                            </marker>
+                        </defs>
+                        <line strokeDasharray={asComponent ? '10,10' : undefined} 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,
+                    }}>
+                    <p>{force.description || 'Force'}</p>
+                    {this.props.showForceMagnitudes && <p>{Math.round(100 * force.magnitude) / 100} N</p>}
+                </div>
+            </div>
+        );
+    };
+
+    renderVector = (id: string, magX: number, magY: number, color: string, label: string) => {
+        const mag = Math.sqrt(magX * magX + magY * magY);
+        return (
+            <div className="showvecs" style={{ zIndex: 6 }}>
+                <svg width={this.panelWidth} height={this.panelHeight}>
+                    <defs>
+                        <marker id={id} 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>
+                    <line
+                        x1={this.state.xPosition + this.props.radius + (magX / mag) * this.props.radius}
+                        y1={this.state.yPosition + this.props.radius + (magY / mag) * this.props.radius}
+                        x2={this.state.xPosition + this.props.radius + (magX / mag) * this.props.radius + magX}
+                        y2={this.state.yPosition + this.props.radius + (magY / mag) * this.props.radius + magY}
+                        stroke={color}
+                        strokeWidth="5"
+                        markerEnd={`url(#${id})`}
+                    />
+                </svg>
+                <div
+                    style={{
+                        pointerEvents: 'none',
+                        position: 'absolute',
+                        left: this.state.xPosition + this.props.radius + 2 * (magX / mag) * this.props.radius + magX + 'px',
+                        top: this.state.yPosition + this.props.radius + 2 * (magY / mag) * this.props.radius + magY + 'px',
+                        lineHeight: 1,
+                    }}>
+                    <p style={{ background: 'white' }}>{label}</p>
+                </div>
+            </div>
+        );
+    };
+
+    // Render weight, spring, rod(s), vectors
+    render() {
+        return (
+            <div>
+                <div
+                    className="weightContainer"
+                    onPointerDown={e => {
+                        if (this.draggable) {
+                            this.props.pause();
+                            this.setState({
+                                dragging: true,
+                                clickPositionX: e.clientX,
+                                clickPositionY: e.clientY,
+                            });
+                        }
+                    }}
+                    onPointerMove={e => {
+                        if (this.state.dragging) {
+                            let newY = this.state.yPosition + e.clientY - this.state.clickPositionY;
+                            if (newY > this.props.yMax - 2 * this.props.radius - 10) {
+                                newY = this.props.yMax - 2 * this.props.radius - 10;
+                            } else if (newY < 10) {
+                                newY = 10;
+                            }
+
+                            let newX = this.state.xPosition + e.clientX - this.state.clickPositionX;
+                            if (newX > this.props.xMax - 2 * this.props.radius - 10) {
+                                newX = this.props.xMax - 2 * this.props.radius - 10;
+                            } else if (newX < 10) {
+                                newX = 10;
+                            }
+                            if (this.props.simulationType == 'Suspension') {
+                                if (newX < (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15) {
+                                    newX = (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15;
+                                } else if (newX > (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15) {
+                                    newX = (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15;
+                                }
+                            }
+
+                            this.setState({ yPosition: newY });
+                            this.props.setPosition(undefined, Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100);
+                            if (this.props.simulationType != 'Pulley') {
+                                this.setState({ xPosition: newX });
+                                this.props.setPosition(newX, undefined);
+                            }
+                            if (this.props.simulationType != 'Suspension') {
+                                if (this.props.simulationType != 'Pulley') {
+                                    this.setState({ updatedStartPosX: newX });
+                                }
+                                this.setState({ updatedStartPosY: newY });
+                            }
+                            this.setState({
+                                clickPositionX: e.clientX,
+                                clickPositionY: e.clientY,
+                            });
+                            this.setDisplayValues();
+                        }
+                    }}
+                    onPointerUp={e => {
+                        if (this.state.dragging) {
+                            if (this.props.simulationType != 'Pendulum' && this.props.simulationType != 'Suspension') {
+                                this.resetEverything();
+                            }
+                            this.setState({ dragging: false });
+                            let newY = this.state.yPosition + e.clientY - this.state.clickPositionY;
+                            if (newY > this.props.yMax - 2 * this.props.radius - 10) {
+                                newY = this.props.yMax - 2 * this.props.radius - 10;
+                            } else if (newY < 10) {
+                                newY = 10;
+                            }
+
+                            let newX = this.state.xPosition + e.clientX - this.state.clickPositionX;
+                            if (newX > this.props.xMax - 2 * this.props.radius - 10) {
+                                newX = this.props.xMax - 2 * this.props.radius - 10;
+                            } else if (newX < 10) {
+                                newX = 10;
+                            }
+                            if (this.props.simulationType == 'Spring') {
+                                this.props.setSpringLength(newY);
+                            }
+                            if (this.props.simulationType == 'Suspension') {
+                                const x1rod = (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200;
+                                const x2rod = (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius;
+                                const deltaX1 = this.state.xPosition + this.props.radius - x1rod;
+                                const deltaX2 = x2rod - (this.state.xPosition + this.props.radius);
+                                const deltaY = this.state.yPosition + this.props.radius;
+                                const dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+                                const dir2T = Math.atan(deltaY / deltaX2);
+                                const tensionMag2 = (this.props.mass * this.props.gravity) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+                                const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+                                const tensionForce1: IForce = {
+                                    description: 'Tension',
+                                    magnitude: tensionMag1,
+                                    directionInDegrees: (dir1T * 180) / Math.PI,
+                                };
+                                const tensionForce2: IForce = {
+                                    description: 'Tension',
+                                    magnitude: tensionMag2,
+                                    directionInDegrees: (dir2T * 180) / Math.PI,
+                                };
+                                this.props.setForcesUpdated([tensionForce1, tensionForce2, this.gravityForce()]);
+                            }
+                        }
+                    }}>
+                    <div className="weight" style={this.weightStyle}>
+                        <p className="weightLabel">{this.props.mass} kg</p>
+                    </div>
+                </div>
+                {this.props.simulationType == 'Spring' && (
+                    <div className="spring">
+                        <svg width={this.panelWidth} height={this.panelHeight}>
+                            {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(val => {
+                                const count = 10;
+                                const xPos1 = this.state.xPosition + this.props.radius + (val % 2 === 0 ? -20 : 20);
+                                const xPos2 = this.state.xPosition + this.props.radius + (val === 10 ? 0 : val % 2 === 0 ? 20 : -20);
+                                const yPos1 = (val * this.state.yPosition) / count;
+                                const yPos2 = val === 10 ? this.state.yPosition + this.props.radius : ((val + 1) * this.state.yPosition) / count;
+                                return <line key={val} x1={xPos1} strokeLinecap="round" y1={yPos1} x2={xPos2} y2={yPos2} stroke={'#808080'} strokeWidth="10" />;
+                            })}
+                        </svg>
+                    </div>
+                )}
+
+                {this.props.simulationType == 'Pulley' && (
+                    <div className="rod">
+                        <svg width={this.panelWidth} height={this.panelHeight}>
+                            <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.simulationType == 'Pulley' && (
+                    <div className="wheel">
+                        <svg width={this.panelWidth} height={this.panelHeight}>
+                            <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.simulationType == 'Suspension' && (
+                    <div className="rod">
+                        <svg width={this.panelWidth} height={this.panelHeight}>
+                            <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>
+                        <div className="rod">
+                            <svg width={this.props.panelWidth() + 'px'} height={this.panelHeight}>
+                                <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>
+                        </div>
+                        <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.simulationType == 'Circular Motion' && (
+                    <div className="rod">
+                        <svg width={this.panelWidth} height={this.panelHeight}>
+                            <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.simulationType == 'Pendulum' && (
+                    <div className="rod">
+                        <svg width={this.panelWidth} height={this.panelHeight}>
+                            <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.simulationType == 'Inclined Plane' && (
+                    <div>
+                        <div className="wedge">
+                            <svg width={this.panelWidth} 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.25 + this.props.wedgeWidth / 3) + '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 &&
+                    this.renderVector(
+                        'accArrow',
+                        this.getNewAccelerationX(this.props.forcesUpdated()),
+                        this.getNewAccelerationY(this.props.forcesUpdated()),
+                        'green',
+                        `${Math.round(100 * Math.sqrt(this.state.xAccel * this.state.xAccel + this.state.yAccel * this.state.yAccel)) / 100} m/s^2`
+                    )}
+                {!this.state.dragging &&
+                    this.props.showVelocity &&
+                    this.renderVector(
+                        'velArrow',
+                        this.state.xVelocity,
+                        this.state.yVelocity,
+                        'blue',
+                        `${Math.round(100 * Math.sqrt(this.props.displayXVelocity * this.props.displayXVelocity + this.props.displayYVelocity * this.props.displayYVelocity)) / 100} m/s`
+                    )}
+                {!this.state.dragging && this.props.showComponentForces && this.props.componentForces().map((force, index) => this.renderForce(force, index, true))}
+                {!this.state.dragging && this.props.showForces && this.props.forcesUpdated().map((force, index) => this.renderForce(force, index, false))}
+            </div>
+        );
+    }
+}
-- 
cgit v1.2.3-70-g09d2

