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.tsx990
1 files changed, 990 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..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>
+ );
+ }
+}