aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts12
-rw-r--r--src/client/util/CurrentUserUtils.ts2
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/PhysicsSimulationBox.scss72
-rw-r--r--src/client/views/nodes/PhysicsSimulationBox.tsx494
-rw-r--r--src/client/views/nodes/PhysicsSimulationWall.tsx34
-rw-r--r--src/client/views/nodes/PhysicsSimulationWedge.tsx83
-rw-r--r--src/client/views/nodes/PhysicsSimulationWeight.tsx860
9 files changed, 1560 insertions, 0 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index d99cd2dac..738bc8265 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -27,6 +27,7 @@ export enum DocumentType {
MAP = 'map',
DATAVIZ = 'dataviz',
LOADING = 'loading',
+ SIMULATION = 'simulation', //physics simulation
// special purpose wrappers that either take no data or are compositions of lower level types
LINK = 'link',
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 80b040cc0..c7556d668 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -50,6 +50,7 @@ import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup';
import { LoadingBox } from '../views/nodes/LoadingBox';
import { MapBox } from '../views/nodes/MapBox/MapBox';
import { PDFBox } from '../views/nodes/PDFBox';
+import PhysicsSimulationBox from '../views/nodes/PhysicsSimulationBox';
import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox';
import { ScreenshotBox } from '../views/nodes/ScreenshotBox';
import { ScriptingBox } from '../views/nodes/ScriptingBox';
@@ -656,6 +657,13 @@ export namespace Docs {
options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' },
},
],
+ [
+ DocumentType.SIMULATION,
+ {
+ layout: { view: PhysicsSimulationBox, dataField: defaultDataKey },
+ options: { _height: 150 }
+ }
+ ]
]);
const suffix = 'Proto';
@@ -1175,6 +1183,10 @@ export namespace Docs {
export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) {
return InstanceFromProto(proto, undefined, options);
}
+
+ export function SimulationDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.SIMULATION), undefined, { ...(options || {}) });
+ }
}
}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 5f183cf91..c8b36ff3a 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -260,6 +260,7 @@ export class CurrentUserUtils {
{key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200 }},
{key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100 }},
{key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _backgroundGridShow: true, }},
+ {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, _backgroundGridShow: true, }},
{key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }},
{key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
@@ -286,6 +287,7 @@ export class CurrentUserUtils {
{ toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, },
{ toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
{ toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, },
+ { toolTip: "Tap or drag to create a physics simulation", title: "Simulation", icon: "atom", dragFactory: doc.emptySimulation as Doc, },
{ toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, },
{ toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, },
{ toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 569579996..c6818bf3c 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -34,6 +34,7 @@ import { LinkAnchorBox } from './LinkAnchorBox';
import { LinkBox } from './LinkBox';
import { MapBox } from './MapBox/MapBox';
import { PDFBox } from './PDFBox';
+import PhysicsSimulationBox from './PhysicsSimulationBox'
import { RecordingBox } from './RecordingBox';
import { ScreenshotBox } from './ScreenshotBox';
import { ScriptingBox } from './ScriptingBox';
@@ -268,6 +269,7 @@ export class DocumentContentsView extends React.Component<
HTMLtag,
ComparisonBox,
LoadingBox,
+ PhysicsSimulationBox,
}}
bindings={bindings}
jsx={layoutFrame}
diff --git a/src/client/views/nodes/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsSimulationBox.scss
new file mode 100644
index 000000000..0f05010b4
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationBox.scss
@@ -0,0 +1,72 @@
+* {
+ box-sizing: border-box;
+ font-size: 14px;
+}
+
+.mechanicsSimulationContainer {
+ background-color: white;
+ height: 100%;
+ width: 100%;
+ display: flex;
+
+ .mechanicsSimulationEquationContainer {
+ position: fixed;
+ left: 70%;
+ padding: 1em;
+
+ .mechanicsSimulationControls {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+}
+
+.coordinateSystem {
+ z-index: -100;
+}
+
+th,
+td {
+ border-collapse: collapse;
+ padding: 1em;
+}
+
+table {
+ min-width: 300px;
+}
+
+tr:nth-child(even) {
+ background-color: #d6eeee;
+}
+
+button {
+ z-index: 50;
+}
+
+.angleLabel {
+ font-weight: bold;
+ font-size: 20px;
+ user-select: none;
+ pointer-events: none;
+}
+
+.mechanicsSimulationSettingsMenu {
+ width: 100%;
+ height: 100%;
+ font-size: 12px;
+ background-color: rgb(224, 224, 224);
+ border-radius: 2px;
+ border-color: black;
+ border-style: solid;
+ padding: 10px;
+ position: fixed;
+ z-index: 1000;
+}
+
+.mechanicsSimulationSettingsMenuRow {
+ display: flex;
+}
+
+.mechanicsSimulationSettingsMenuRowDescription {
+ width: 50%;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsSimulationBox.tsx
new file mode 100644
index 000000000..d0e854263
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationBox.tsx
@@ -0,0 +1,494 @@
+import "./PhysicsSimulationBox.scss";
+import { FieldView, FieldViewProps } from './FieldView';
+import React = require('react');
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { observer } from 'mobx-react';
+import "./PhysicsSimulationBox.scss";
+import Weight from "./PhysicsSimulationWeight";
+import Wall from "./PhysicsSimulationWall"
+import Wedge from "./PhysicsSimulationWedge"
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { CheckBox } from "../search/CheckBox";
+export interface IForce {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWallProps {
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
+}
+
+interface PhysicsVectorTemplate {
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+ weightX: number;
+ weightY: number;
+}
+
+@observer
+export default class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PhysicsSimulationBox, fieldKey); }
+
+ // Constants
+ gravityMagnitude = 9.81;
+ forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: this.gravityMagnitude,
+ directionInDegrees: 270,
+ };
+ xMin = 0;
+ yMin = 0;
+ xMax = 300;
+ yMax = 300;
+ color = `rgba(0,0,0,0.5)`;
+ radius = 0.1*this.yMax
+ update = true
+ menuIsOpen = false
+
+ constructor(props: any) {
+ super(props);
+ }
+
+ // Add one weight to the simulation
+ addWeight () {
+ this.dataDoc.weight = true;
+ this.dataDoc.wedge = false;
+ this.dataDoc.pendulum = false;
+ this.addWalls();
+ };
+
+ // Set weight defaults
+ setToWeightDefault () {
+ this.dataDoc.startPosY = this.yMin+this.radius;
+ this.dataDoc.startPosX = (this.xMax+this.xMin-this.radius)/2;
+ this.dataDoc.updatedForces = [this.forceOfGravity];
+ this.dataDoc.startForces = [this.forceOfGravity];
+ }
+
+ // Add a wedge with a One Weight to the simulation
+ addWedge () {
+ this.dataDoc.weight = true;
+ this.dataDoc.wedge = true;
+ this.dataDoc.pendulum = false;
+ this.addWalls();
+ };
+
+ // Set wedge defaults
+ setToWedgeDefault () {
+ this.changeWedgeBasedOnNewAngle(26);
+ this.updateForcesWithFriction(this.dataDoc.coefficientOfStaticFriction);
+ }
+
+ // Add a simple pendulum to the simulation
+ addPendulum = () => {
+ this.dataDoc.weight = true;
+ this.dataDoc.wedge = false;
+ this.dataDoc.pendulum = true;
+ this.removeWalls();
+ let angle = this.dataDoc.pendulumAngle;
+ let mag = 9.81 * Math.cos((angle * Math.PI) / 180);
+ let forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: 90 - angle,
+ };
+ this.dataDoc.updatedForces = [this.forceOfGravity, forceOfTension];
+ this.dataDoc.startForces = [this.forceOfGravity, forceOfTension];
+ };
+
+ // Set pendulum defaults
+ setToPendulumDefault () {
+ let length = this.xMax*0.7;
+ let angle = 35;
+ let x = length * Math.cos(((90 - angle) * Math.PI) / 180);
+ let y = length * Math.sin(((90 - angle) * Math.PI) / 180);
+ let xPos = this.xMax / 2 - x - this.radius;
+ let yPos = y - this.radius;
+ this.dataDoc.startPosX = xPos;
+ this.dataDoc.startPosY = yPos;
+ let mag = 9.81 * Math.cos((angle * Math.PI) / 180);
+ this.dataDoc.pendulumAngle = angle;
+ this.dataDoc.pendulumLength = length;
+ this.dataDoc.startPendulumAngle = angle;
+ this.dataDoc.adjustPendulumAngle = !this.dataDoc.adjustPendulumAngle;
+ }
+
+ // Update forces when coefficient of static friction changes in freeform mode
+ updateForcesWithFriction (
+ coefficient: number,
+ width: number = this.dataDoc.wedgeWidth,
+ height: number = this.dataDoc.wedgeHeight
+ ) {
+ let normalForce = {
+ description: "Normal Force",
+ magnitude: this.forceOfGravity.magnitude * Math.cos(Math.atan(height / width)),
+ directionInDegrees:
+ 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ let frictionForce: IForce = {
+ description: "Static Friction Force",
+ magnitude:
+ coefficient *
+ this.forceOfGravity.magnitude *
+ Math.cos(Math.atan(height / width)),
+ directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ // reduce magnitude of friction force if necessary such that block cannot slide up plane
+ let yForce = -this.forceOfGravity.magnitude;
+ yForce +=
+ normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+ yForce +=
+ frictionForce.magnitude *
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude =
+ (-normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
+ this.forceOfGravity.magnitude) /
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+ if (coefficient != 0) {
+ this.dataDoc.startForces = [this.forceOfGravity, normalForce, frictionForce];
+ this.dataDoc.updatedForces = [this.forceOfGravity, normalForce, frictionForce];
+ } else {
+ this.dataDoc.startForces = [this.forceOfGravity, normalForce];
+ this.dataDoc.updatedForces = [this.forceOfGravity, normalForce];
+ }
+ };
+
+ // Change wedge height and width and weight position to match new wedge angle
+ changeWedgeBasedOnNewAngle = (angle: number) => {
+ let width = 0;
+ let height = 0;
+ if (angle < 50) {
+ width = this.xMax*0.6;
+ height = Math.tan((angle * Math.PI) / 180) * width;
+ this.dataDoc.wedgeWidth = width;
+ this.dataDoc.wedgeHeight = height;
+ } else if (angle < 70) {
+ width = this.xMax*0.3;
+ height = Math.tan((angle * Math.PI) / 180) * width;
+ this.dataDoc.wedgeWidth = width;
+ this.dataDoc.wedgeHeight = height;
+ } else {
+ width = this.xMax*0.15;
+ height = Math.tan((angle * Math.PI) / 180) * width;
+ this.dataDoc.wedgeWidth = width;
+ this.dataDoc.wedgeHeight = height;
+ }
+
+ // update weight position based on updated wedge width/height
+ let xPos = (this.xMax * 0.2)-this.radius;
+ let yPos = width * Math.tan((angle * Math.PI) / 180) - this.radius;
+
+ this.dataDoc.startPosX = xPos;
+ this.dataDoc.startPosY = this.getDisplayYPos(yPos);
+ this.updateForcesWithFriction(
+ Number(this.dataDoc.coefficientOfStaticFriction),
+ width,
+ height
+ );
+ this.dataDoc['updateDisplay'] = !this.dataDoc['updateDisplay']
+ };
+
+ // Helper function to go between display and real values
+ getDisplayYPos = (yPos: number) => {
+ return this.yMax - yPos - 2 * 50 + 5;
+ };
+
+ // In review mode, edit force arrow sketch on mouse movement
+ editForce = (element: PhysicsVectorTemplate) => {
+ if (!this.dataDoc.sketching) {
+ let sketches = this.dataDoc.forceSketches.filter((sketch: PhysicsVectorTemplate) => sketch != element);
+ this.dataDoc.forceSketches = sketches;
+ this.dataDoc.currentForceSketch = element;
+ this.dataDoc.sketching = true;
+ }
+ };
+
+ // In review mode, used to delete force arrow sketch on SHIFT+click
+ deleteForce = (element: PhysicsVectorTemplate) => {
+ if (!this.dataDoc.sketching) {
+ let sketches = this.dataDoc.forceSketches.filter((sketch: PhysicsVectorTemplate) => sketch != element);
+ this.dataDoc.forceSketches = sketches;
+ }
+ };
+
+ // Remove floor and walls from simulation
+ removeWalls = () => {
+ this.dataDoc.wallPositions = []
+ };
+
+ // Add floor and walls to simulation
+ addWalls = () => {
+ let walls = [];
+ walls.push({ length: 100, xPos: 0, yPos: 97, angleInDegrees: 0 });
+ walls.push({ length: 100, xPos: 0, yPos: 0, angleInDegrees: 90 });
+ walls.push({ length: 100, xPos: 97, yPos: 0, angleInDegrees: 90 });
+ this.dataDoc.wallPositions = walls
+ };
+
+
+ componentDidMount() {
+ this.xMax = this.layoutDoc._width;
+ this.yMax = this.layoutDoc._height;
+ this.radius = 0.1*this.layoutDoc._height;
+
+ // Add weight
+ if (this.dataDoc.simulationType == "Inclined Plane") {
+ this.addWedge()
+ } else if (this.dataDoc.simulationType == "Pendulum") {
+ this.addPendulum()
+ } else {
+ this.dataDoc.simulationType = "Free Weight"
+ this.addWeight()
+ }
+ this.dataDoc.accelerationXDisplay = this.dataDoc.accelerationXDisplay ?? 0;
+ this.dataDoc.accelerationYDisplay = this.dataDoc.accelerationYDisplay ?? 0;
+ this.dataDoc.coefficientOfKineticFriction = this.dataDoc.coefficientOfKineticFriction ?? 0;
+ this.dataDoc.coefficientOfStaticFriction = this.dataDoc.coefficientOfStaticFriction ?? 0;
+ this.dataDoc.currentForceSketch = this.dataDoc.currentForceSketch ?? [];
+ this.dataDoc.elasticCollisions = this.dataDoc.elasticCollisions ?? false;
+ this.dataDoc.forceSketches = this.dataDoc.forceSketches ?? [];
+ this.dataDoc.pendulumAngle = this.dataDoc.pendulumAngle ?? 26;
+ this.dataDoc.pendulumLength = this.dataDoc.pendulumLength ?? 300;
+ this.dataDoc.positionXDisplay = this.dataDoc.positionXDisplay ?? 0;
+ this.dataDoc.positionYDisplay = this.dataDoc.positionYDisplay ?? 0;
+ this.dataDoc.showAcceleration = this.dataDoc.showAcceleration ?? false;
+ this.dataDoc.showForceMagnitudes = this.dataDoc.showForceMagnitudes ?? false;
+ this.dataDoc.showForces = this.dataDoc.showForces ?? false;
+ this.dataDoc.showVelocity = this.dataDoc.showVelocity ?? false;
+ this.dataDoc.startForces = this.dataDoc.startForces ?? [this.forceOfGravity];
+ this.dataDoc.startPendulumAngle = this.dataDoc.startPendulumAngle ?? 0;
+ this.dataDoc.startPosX = this.dataDoc.startPosX ?? 50;
+ this.dataDoc.startPosY = this.dataDoc.startPosY ?? 50;
+ this.dataDoc.stepNumber = this.dataDoc.stepNumber ?? 0;
+ this.dataDoc.updateDisplay = this.dataDoc.updateDisplay ?? false;
+ this.dataDoc.updatedForces = this.dataDoc.updatedForces ?? [this.forceOfGravity];
+ this.dataDoc.velocityXDisplay = this.dataDoc.velocityXDisplay ?? 0;
+ this.dataDoc.velocityYDisplay = this.dataDoc.velocityYDisplay ?? 0;
+ this.dataDoc.wallPositions = this.dataDoc.wallPositions ?? [];
+ this.dataDoc.wedgeAngle = this.dataDoc.wedgeAngle ?? 26;
+ this.dataDoc.wedgeHeight = this.dataDoc.wedgeHeight ?? Math.tan((26 * Math.PI) / 180) * this.xMax*0.6;
+ this.dataDoc.wedgeWidth = this.dataDoc.wedgeWidth ?? this.xMax*0.6;
+
+ this.dataDoc.adjustPendulumAngle = true;
+ this.dataDoc.simulationPaused = true;
+ this.dataDoc.simulationReset = false;
+
+ // Add listener for SHIFT key, which determines if sketch force arrow will be edited or deleted on click
+ document.addEventListener("keydown", (e) => {
+ if (e.shiftKey) {
+ this.dataDoc.deleteMode = true;
+ }
+ });
+ document.addEventListener("keyup", (e) => {
+ if (e.shiftKey) {
+ this.dataDoc.deleteMode = false;
+ }
+ });
+ }
+
+ componentDidUpdate() {
+ this.xMax = this.layoutDoc._width;
+ this.yMax = this.layoutDoc._height;
+ this.radius = 0.1*this.layoutDoc._height;
+ }
+
+ render () {
+ return (
+ <div>
+ <div className="mechanicsSimulationContainer">
+ <div className="mechanicsSimulationContentContainer">
+ <div className="mechanicsSimulationButtonsAndElements">
+ <div className="mechanicsSimulationElements">
+ {this.dataDoc.weight && (
+ <Weight
+ adjustPendulumAngle={this.dataDoc.adjustPendulumAngle}
+ color={"red"}
+ dataDoc={this.dataDoc}
+ mass={1}
+ radius={this.radius}
+ simulationReset={this.dataDoc.simulationReset}
+ startPosX={this.dataDoc.startPosX}
+ startPosY={this.dataDoc.startPosY}
+ timestepSize={0.002}
+ updateDisplay={this.dataDoc.updateDisplay}
+ walls={this.dataDoc.wallPositions}
+ wedge={this.dataDoc.wedge}
+ wedgeWidth={this.dataDoc.wedgeWidth}
+ wedgeHeight={this.dataDoc.wedgeHeight}
+ xMax={this.xMax}
+ xMin={this.xMin}
+ yMax={this.yMax}
+ yMin={this.yMin}
+ />
+ )}
+ {this.dataDoc.wedge && (
+ <Wedge
+ startWidth={this.dataDoc.wedgeWidth}
+ startHeight={this.dataDoc.wedgeHeight}
+ startLeft={this.xMax * 0.2}
+ xMax={this.xMax}
+ yMax={this.yMax}
+ />
+ )}
+ </div>
+ <div>
+ {(this.dataDoc.wallPositions ?? []).map((element: { length: number; xPos: number; yPos: number; angleInDegrees: number; }, index: React.Key | null | undefined) => {
+ return (
+ <div key={index}>
+ <Wall
+ length={element.length}
+ xPos={element.xPos}
+ yPos={element.yPos}
+ angleInDegrees={element.angleInDegrees}
+ />
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ </div>
+ <div style = {{width: this.layoutDoc._width+'px', height: this.layoutDoc._height+'px'}}>
+ {this.menuIsOpen && (
+ <div className="mechanicsSimulationSettingsMenu">
+ <div className="close-button" onClick={() => {this.menuIsOpen = false; this.dataDoc.simulationReset = !this.dataDoc.simulationReset;}}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
+ <h4>Simulation Settings</h4>
+ <div className="mechanicsSimulationSettingsMenuRow">
+ <div className="mechanicsSimulationSettingsMenuRowDescription"><p>Show forces</p></div>
+ <div><input type="checkbox" checked={this.dataDoc.showForces} onClick={() => {this.dataDoc.showForces = !this.dataDoc.showForces}}/></div>
+ </div>
+ <div className="mechanicsSimulationSettingsMenuRow">
+ <div className="mechanicsSimulationSettingsMenuRowDescription"><p>Show acceleration</p></div>
+ <div><input type="checkbox" checked={this.dataDoc.showAcceleration} onClick={() => {this.dataDoc.showAcceleration = !this.dataDoc.showAcceleration}}/></div>
+ </div>
+ <div className="mechanicsSimulationSettingsMenuRow">
+ <div className="mechanicsSimulationSettingsMenuRowDescription">
+ <p>Show velocity</p></div>
+ <div><input type="checkbox" checked={this.dataDoc.showVelocity} onClick={() => {this.dataDoc.showVelocity = !this.dataDoc.showVelocity}}/></div>
+ </div>
+ <hr/>
+ {this.dataDoc.simulationType == "Free Weight" && <div className="mechanicsSimulationSettingsMenuRow">
+ <div className="mechanicsSimulationSettingsMenuRowDescription"><p>Elastic collisions </p></div>
+ <div><input type="checkbox" checked={this.dataDoc.elasticCollisions} onClick={() => {this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions}}/></div>
+ </div>}
+ {this.dataDoc.simulationType == "Pendulum" && <div className="mechanicsSimulationSettingsMenuRow">
+ <div className="mechanicsSimulationSettingsMenuRowDescription"><p>Pendulum start angle</p></div>
+ <div>
+ <input
+ type="number"
+ value={this.dataDoc.startPendulumAngle}
+ max={35}
+ min={0}
+ step={1}
+ onInput={(e) => {
+ let angle = e.target.value;
+ if (angle > 35) {
+ angle = 35
+ }
+ if (angle < 0) {
+ angle = 0
+ }
+ let length = this.xMax*0.7;
+ let x = length * Math.cos(((90 - angle) * Math.PI) / 180);
+ let y = length * Math.sin(((90 - angle) * Math.PI) / 180);
+ let xPos = this.xMax / 2 - x - this.radius;
+ let yPos = y - this.radius;
+ this.dataDoc.startPosX = xPos;
+ this.dataDoc.startPosY = yPos;
+ let mag = 9.81 * Math.cos((angle * Math.PI) / 180);
+ this.dataDoc.pendulumAngle = angle;
+ this.dataDoc.pendulumLength = length;
+ this.dataDoc.startPendulumAngle = angle;
+ this.dataDoc.adjustPendulumAngle = !this.dataDoc.adjustPendulumAngle;
+ }}
+ />
+ </div>
+ </div>}
+ {this.dataDoc.simulationType == "Inclined Plane" && <div className="mechanicsSimulationSettingsMenuRow">
+ <div className="mechanicsSimulationSettingsMenuRowDescription"><p>Inclined plane angle</p></div>
+ <div>
+ <input
+ type="number"
+ value={this.dataDoc.wedgeAngle}
+ max={70}
+ min={0}
+ step={1}
+ onInput={(e) => {
+ let angle = e.target.value ?? 0
+ if (angle > 70) {
+ angle = 70
+ }
+ if (angle < 0) {
+ angle = 0
+ }
+ this.dataDoc.wedgeAngle = angle
+ this.changeWedgeBasedOnNewAngle(angle)
+ }}
+ />
+ </div>
+ </div>}
+ </div>
+ )}
+ </div>
+ <div className="mechanicsSimulationEquationContainer">
+ <div className="mechanicsSimulationControls">
+ <div>
+ {this.dataDoc.simulationPaused && (
+ <button onClick={() => {
+ this.dataDoc.simulationPaused = false}
+ } >START</button>
+ )}
+ {!this.dataDoc.simulationPaused && (
+ <button onClick={() => {
+ this.dataDoc.simulationPaused = true}
+ } >PAUSE</button>
+ )}
+ {this.dataDoc.simulationPaused && (
+ <button onClick={() => {
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset}
+ } >RESET</button>
+ )}
+ {this.dataDoc.simulationPaused && ( <button onClick={() => {
+ if (!this.dataDoc.pendulum && !this.dataDoc.wedge) {
+ this.addWedge()
+ this.setToWedgeDefault()
+ this.dataDoc.simulationType = "Inclined Plane"
+ this.dataDoc.elasticCollisions = false
+ }
+ else if (!this.dataDoc.pendulum && this.dataDoc.wedge) {
+ this.setToPendulumDefault()
+ this.addPendulum()
+ this.dataDoc.simulationType = "Pendulum"
+ this.dataDoc.elasticCollisions = false
+ }
+ else {
+ this.setToWeightDefault()
+ this.addWeight()
+ this.dataDoc.simulationType = "Free Weight"
+ }
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset
+ }} >TYPE</button>)}
+ <button onClick={() => {this.menuIsOpen=true; this.dataDoc.simulationReset = !this.dataDoc.simulationReset;}}>MENU</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+ } \ No newline at end of file
diff --git a/src/client/views/nodes/PhysicsSimulationWall.tsx b/src/client/views/nodes/PhysicsSimulationWall.tsx
new file mode 100644
index 000000000..9283e8d46
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationWall.tsx
@@ -0,0 +1,34 @@
+import React = require('react');
+
+export interface Force {
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWallProps {
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
+}
+
+export default class App extends React.Component<IWallProps> {
+
+ constructor(props: any) {
+ super(props)
+ }
+
+ wallStyle = {
+ width: this.props.angleInDegrees == 0 ? this.props.length + "%" : "3%",
+ height: this.props.angleInDegrees == 0 ? "3%" : this.props.length + "%",
+ position: "absolute" as "absolute",
+ left: this.props.xPos + "%",
+ top: this.props.yPos + "%",
+ backgroundColor: "#6c7b8b",
+ margin: 0,
+ padding: 0,
+ };
+
+ render () {
+ return (<div style={this.wallStyle}></div>);
+ }
+};
diff --git a/src/client/views/nodes/PhysicsSimulationWedge.tsx b/src/client/views/nodes/PhysicsSimulationWedge.tsx
new file mode 100644
index 000000000..6134a6bc0
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationWedge.tsx
@@ -0,0 +1,83 @@
+import React = require('react');
+import "./PhysicsSimulationBox.scss";
+
+export interface IWedgeProps {
+ startHeight: number;
+ startWidth: number;
+ startLeft: number;
+ xMax: number;
+ yMax: number;
+}
+
+interface IState {
+ angleInRadians: number,
+ left: number,
+ coordinates: string,
+}
+
+export default class Wedge extends React.Component<IWedgeProps, IState> {
+
+ constructor(props: any) {
+ super(props)
+ this.state = {
+ angleInRadians: Math.atan(this.props.startHeight / this.props.startWidth),
+ left: this.props.startLeft,
+ coordinates: "",
+ }
+ }
+
+ color = "#deb887";
+
+ updateCoordinates() {
+ const coordinatePair1 =
+ Math.round(this.state.left) + "," + Math.round(this.props.yMax) + " ";
+ const coordinatePair2 =
+ Math.round(this.state.left + this.props.startWidth) +
+ "," +
+ Math.round(this.props.yMax) +
+ " ";
+ const coordinatePair3 =
+ Math.round(this.state.left) +
+ "," +
+ Math.round(this.props.yMax - this.props.startHeight);
+ const coord = coordinatePair1 + coordinatePair2 + coordinatePair3;
+ this.setState({coordinates: coord});
+ }
+
+ componentDidMount() {
+ this.updateCoordinates()
+ }
+
+ componentDidUpdate(prevProps: Readonly<IWedgeProps>, prevState: Readonly<IState>, snapshot?: any): void {
+ if (prevProps.startHeight != this.props.startHeight || prevProps.startWidth != this.props.startWidth) {
+ this.setState({angleInRadians: Math.atan(this.props.startHeight / this.props.startWidth)});
+ this.updateCoordinates();
+ }
+ }
+
+ render() {
+ return (
+ <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",
+ zIndex: 500,
+ left: Math.round(this.state.left + this.props.startWidth - 80) + "px",
+ top: Math.round(this.props.yMax - 40) + "px",
+ }}
+ >
+ {Math.round(((this.state.angleInRadians * 180) / Math.PI) * 100) / 100}°
+ </p>
+ </div>
+ );
+ }
+};
diff --git a/src/client/views/nodes/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsSimulationWeight.tsx
new file mode 100644
index 000000000..39b3249e8
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationWeight.tsx
@@ -0,0 +1,860 @@
+import React = require('react');
+import { Doc } from '../../../fields/Doc';
+import { IWallProps } from "./PhysicsSimulationWall";
+
+export interface IForce {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWeightProps {
+ adjustPendulumAngle: boolean;
+ color: string;
+ dataDoc: Doc;
+ mass: number;
+ radius: number;
+ simulationReset: boolean;
+ startPosX: number;
+ startPosY: number;
+ startVelX?: number;
+ startVelY?: number;
+ timestepSize: number;
+ updateDisplay: boolean,
+ walls: IWallProps[];
+ wedge: boolean;
+ wedgeHeight: number;
+ wedgeWidth: number;
+ xMax: number;
+ xMin: number;
+ yMax: number;
+ yMin: number;
+}
+
+interface IState {
+ angleLabel: number,
+ clickPositionX: number,
+ clickPositionY: number,
+ dragging: boolean,
+ kineticFriction: boolean,
+ timer: number;
+ update: boolean,
+ updatedStartPosX: number,
+ updatedStartPosY: number,
+ xPosition: number,
+ xVelocity: number,
+ yPosition: number,
+ yVelocity: number,
+}
+export default class Weight extends React.Component<IWeightProps, IState> {
+
+ constructor(props: any) {
+ super(props)
+ this.state = {
+ clickPositionX: 0,
+ clickPositionY: 0,
+ dragging: false,
+ kineticFriction: false,
+ timer: 0,
+ angleLabel: 0,
+ updatedStartPosX: this.props.dataDoc['startPosX'],
+ updatedStartPosY: this.props.dataDoc['startPosY'],
+ xPosition: this.props.dataDoc['startPosX'],
+ xVelocity: this.props.startVelX ? this.props.startVelX: 0,
+ yPosition: this.props.dataDoc['startPosY'],
+ yVelocity: this.props.startVelY ? this.props.startVelY: 0,
+ }
+ }
+
+ // Constants
+ draggable = !this.props.dataDoc['wedge'] ;
+ epsilon = 0.0001;
+ forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: this.props.mass * 9.81,
+ directionInDegrees: 270,
+ };
+
+ // Var
+ 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.dataDoc['startPosX'] + "px",
+ position: "absolute" as "absolute",
+ top: this.props.dataDoc['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);
+ this.props.dataDoc['positionYDisplay'] = Math.round(displayPos * 100) / 100
+ };
+ setXPosDisplay = (xPos: number) => {
+ this.props.dataDoc['positionXDisplay'] = Math.round(xPos * 100) / 100;
+ };
+ setYVelDisplay = (yVel: number) => {
+ this.props.dataDoc['velocityYDisplay'] = (-1 * Math.round(yVel * 100)) / 100;
+ };
+ setXVelDisplay = (xVel: number) => {
+ this.props.dataDoc['velocityXDisplay'] = Math.round(xVel * 100) / 100;
+ };
+
+ 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);
+ this.props.dataDoc['accelerationYDisplay'] =
+ (-1 * Math.round(this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 100)) / 100
+ ;
+ this.props.dataDoc['accelerationXDisplay'] =
+ Math.round(this.getNewAccelerationX(this.props.dataDoc['updatedForces']) * 100) / 100
+ ;
+ };
+
+ componentDidMount() {
+ // Timer for animating the simulation
+ setInterval(() => {
+ this.setState({timer: this.state.timer + 1});
+ }, 60);
+ }
+
+ componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void {
+
+ // When display values updated by user, update real values
+ if (this.props.updateDisplay != prevProps.updateDisplay) {
+ if (this.props.dataDoc['positionXDisplay'] != this.state.xPosition) {
+ let x = this.props.dataDoc['positionXDisplay'];
+ x = Math.max(0, x);
+ x = Math.min(x, this.props.xMax - 2 * this.props.radius);
+ this.setState({updatedStartPosX: x})
+ this.setState({xPosition: x})
+ this.props.dataDoc['positionXDisplay'] = x;
+ }
+
+ if (this.props.dataDoc['positionYDisplay'] != this.getDisplayYPos(this.state.yPosition)) {
+ let y = this.props.dataDoc['positionYDisplay'];
+ y = Math.max(0, y);
+ y = Math.min(y, this.props.yMax - 2 * this.props.radius);
+ this.props.dataDoc['positionYDisplay'] = y;
+ let coordinatePosition = this.getYPosFromDisplay(y);
+ this.setState({updatedStartPosY: coordinatePosition})
+ this.setState({yPosition: coordinatePosition})
+ }
+
+ if (this.props.dataDoc['velocityXDisplay'] != this.state.xVelocity) {
+ let x = this.props.dataDoc['velocityXDisplay'];
+ this.setState({xVelocity: x})
+ this.props.dataDoc['velocityXDisplay'] = x;
+ }
+
+ if (this.props.dataDoc['velocityYDisplay'] != this.state.yVelocity) {
+ let y = this.props.dataDoc['velocityYDisplay'];
+ this.setState({yVelocity: -y})
+ this.props.dataDoc['velocityYDisplay']
+ }
+ }
+ // Update sim
+ if (this.state.timer != prevState.timer) {
+ if (!this.props.dataDoc['simulationPaused']) {
+ let collisions = false;
+ if (!this.props.dataDoc['pendulum']) {
+ const collisionsWithGround = this.checkForCollisionsWithGround();
+ const collisionsWithWalls = this.checkForCollisionsWithWall();
+ collisions = collisionsWithGround || collisionsWithWalls;
+ }
+ if (!collisions) {
+ this.update();
+ }
+ this.setDisplayValues();
+ }
+ }
+
+ if (this.props.simulationReset != prevProps.simulationReset) {
+ this.resetEverything();
+ }
+ if (this.props.adjustPendulumAngle != prevProps.adjustPendulumAngle) {
+ console.log('update angle')
+ // Change pendulum angle based on input field
+ let length = this.props.dataDoc['pendulumLength'] ?? 0;
+ const x =
+ length * Math.cos(((90 - this.props.dataDoc['pendulumAngle']) * Math.PI) / 180);
+ const y =
+ length * Math.sin(((90 - this.props.dataDoc['pendulumAngle']) * Math.PI) / 180);
+ const xPos = this.props.xMax / 2 - x - this.props.radius;
+ const yPos = y - this.props.radius - 5;
+ this.setState({xPosition: xPos})
+ this.setState({yPosition: yPos})
+ this.setState({updatedStartPosX: xPos})
+ this.setState({updatedStartPosY: yPos})
+ this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle'] * 100) / 100})
+ }
+ // Update x start position
+ if (this.props.startPosX != prevProps.startPosX) {
+ this.setState({updatedStartPosX: this.props.dataDoc['startPosX']})
+ this.setState({xPosition: this.props.dataDoc['startPosX']})
+ this.setXPosDisplay(this.props.dataDoc['startPosX']);
+ }
+ // Update y start position
+ if (this.props.startPosY != prevProps.startPosY) {
+ this.setState({updatedStartPosY: this.props.dataDoc['startPosY']})
+ this.setState({yPosition: this.props.dataDoc['startPosY']})
+ this.setYPosDisplay(this.props.dataDoc['startPosY']);
+ }
+ if (!this.props.dataDoc['simulationPaused']) {
+ if (this.state.xVelocity != prevState.xVelocity) {
+ if (this.props.dataDoc['wedge'] && this.state.xVelocity != 0 && !this.state.kineticFriction) {
+ this.setState({kineticFriction: true});
+ //switch from static to kinetic friction
+ const normalForce: IForce = {
+ description: "Normal Force",
+ magnitude:
+ this.forceOfGravity.magnitude *
+ Math.cos(Math.atan(this.props.dataDoc['wedgeHeight'] / this.props.dataDoc['wedgeWidth'] )),
+ directionInDegrees:
+ 180 - 90 - (Math.atan(this.props.dataDoc['wedgeHeight'] / this.props.dataDoc['wedgeWidth'] ) * 180) / Math.PI,
+ };
+ let frictionForce: IForce = {
+ description: "Kinetic Friction Force",
+ magnitude:
+ this.props.dataDoc['coefficientOfKineticFriction'] *
+ this.forceOfGravity.magnitude *
+ Math.cos(Math.atan(this.props.dataDoc['wedgeHeight'] / this.props.dataDoc['wedgeWidth'] )),
+ directionInDegrees:
+ 180 - (Math.atan(this.props.dataDoc['wedgeHeight'] / this.props.dataDoc['wedgeWidth'] ) * 180) / Math.PI,
+ };
+ // reduce magnitude of friction force if necessary such that block cannot slide up plane
+ let yForce = -this.forceOfGravity.magnitude;
+ yForce +=
+ normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+ yForce +=
+ frictionForce.magnitude *
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude =
+ (-normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
+ this.forceOfGravity.magnitude) /
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+ if (this.props.dataDoc['coefficientOfKineticFriction'] != 0) {
+ this.props.dataDoc['updatedForces'] = [this.forceOfGravity, normalForce, frictionForce];
+ } else {
+ this.props.dataDoc['updatedForces'] = ([this.forceOfGravity, normalForce]);
+ }
+ }
+ }
+ }
+
+ this.weightStyle = {
+ alignItems: "center",
+ backgroundColor: this.props.color,
+ borderColor: this.state.dragging ? "lightblue" : "black",
+ borderRadius: 50 + "%",
+ borderStyle: "solid",
+ display: "flex",
+ height: 2 * this.props.radius + "px",
+ justifyContent: "center",
+ left: this.state.xPosition + "px",
+ position: "absolute" as "absolute",
+ top: this.state.yPosition + "px",
+ touchAction: "none",
+ width: 2 * this.props.radius + "px",
+ zIndex: 5,
+ };
+ }
+
+ 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['updatedForces'] = (this.props.dataDoc['startForces'])
+ this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle']* 100) / 100})
+ this.setDisplayValues();
+ };
+
+ 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;
+ };
+
+ 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;
+ };
+
+ getNewForces = (
+ xPos: number,
+ yPos: number,
+ xVel: number,
+ yVel: number
+ ) => {
+ if (!this.props.dataDoc['pendulum']) {
+ return this.props.dataDoc['updatedForces'];
+ }
+ const x = this.props.xMax / 2 - xPos - this.props.radius;
+ const y = yPos + this.props.radius + 5;
+ let angle = (Math.atan(y / x) * 180) / Math.PI;
+ if (angle < 0) {
+ angle += 180;
+ }
+ let oppositeAngle = 90 - angle;
+ if (oppositeAngle < 0) {
+ oppositeAngle = 90 - (180 - angle);
+ }
+
+ const pendulumLength = Math.sqrt(x * x + y * y);
+ this.props.dataDoc['pendulumAngle'] = oppositeAngle;
+ this.props.dataDoc['pendulumLength'] = Math.sqrt(x * x + y * y);
+
+ const mag =
+ this.props.mass * 9.81 * Math.cos((oppositeAngle * Math.PI) / 180) +
+ (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength;
+
+ const forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: angle,
+ };
+ this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle']* 100) / 100})
+
+ return [this.forceOfGravity, forceOfTension];
+ };
+
+ getNewPosition = (pos: number, vel: number) => {
+ return pos + vel * this.props.timestepSize;
+ };
+
+ getNewVelocity = (vel: number, acc: number) => {
+ return vel + acc * this.props.timestepSize;
+ };
+
+ checkForCollisionsWithWall = () => {
+ let collision = false;
+ const minX = this.state.xPosition;
+ const maxX = this.state.xPosition + 2 * this.props.radius;
+ const containerWidth = 300;
+ if (this.state.xVelocity != 0) {
+ if (this.props.dataDoc.wallPositions) {
+ this.props.dataDoc['wallPositions'].forEach((wall) => {
+ if (wall.angleInDegrees == 90) {
+ const wallX = (wall.xPos / 100) * 300;
+ if (wall.xPos < 0.35) {
+ if (minX <= wallX) {
+ if (this.props.dataDoc['elasticCollisions']) {
+ this.setState({xVelocity: -this.state.xVelocity});
+ } else {
+ this.setState({xVelocity: 0});
+ this.setState({xPosition: wallX+5});
+ }
+ collision = true;
+ }
+ } else {
+ if (maxX >= wallX) {
+ if (this.props.dataDoc['elasticCollisions']) {
+ this.setState({xVelocity: -this.state.xVelocity});
+ } else {
+ this.setState({xVelocity: 0});
+ this.setState({xPosition: wallX - 2 * this.props.radius + 5});
+ }
+ collision = true;
+ }
+ }
+ }
+ });
+ }
+ }
+ return collision;
+ };
+
+ checkForCollisionsWithGround = () => {
+ let collision = false;
+ const maxY = this.state.yPosition + 2 * this.props.radius;
+ if (this.state.yVelocity > 0) {
+ if (this.props.dataDoc.wallPositions) {
+ this.props.dataDoc['wallPositions'].forEach((wall) => {
+ if (wall.angleInDegrees == 0) {
+ const groundY = (wall.yPos / 100) * this.props.yMax;
+ if (maxY >= groundY) {
+ if (this.props.dataDoc['elasticCollisions']) {
+ this.setState({yVelocity: -this.state.yVelocity})
+ } else {
+ this.setState({yVelocity: 0})
+ this.setState({yPosition: groundY - 2 * this.props.radius + 5})
+ const forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: 9.81 * this.props.mass,
+ directionInDegrees: 270,
+ };
+ const normalForce: IForce = {
+ description: "Normal force",
+ magnitude: 9.81 * this.props.mass,
+ directionInDegrees: wall.angleInDegrees + 90,
+ };
+ this.props.dataDoc['updatedForces'] = ([forceOfGravity, normalForce]);
+ }
+ collision = true;
+ }
+ }
+ });
+ }
+ }
+ return collision;
+ };
+
+ update = () => {
+ // RK4 update
+ let xPos = this.state.xPosition;
+ let yPos = this.state.yPosition;
+ let xVel = this.state.xVelocity;
+ let yVel = this.state.yVelocity;
+ for (let i = 0; i < 60; i++) {
+ let forces1 = this.getNewForces(xPos, yPos, xVel, yVel);
+ const xAcc1 = this.getNewAccelerationX(forces1);
+ const yAcc1 = this.getNewAccelerationY(forces1);
+ const xVel1 = this.getNewVelocity(xVel, xAcc1);
+ const yVel1 = this.getNewVelocity(yVel, yAcc1);
+
+ let xVel2 = this.getNewVelocity(xVel, xAcc1 / 2);
+ let yVel2 = this.getNewVelocity(yVel, yAcc1 / 2);
+ let xPos2 = this.getNewPosition(xPos, xVel1 / 2);
+ let yPos2 = this.getNewPosition(yPos, yVel1 / 2);
+ const forces2 = this.getNewForces(xPos2, yPos2, xVel2, yVel2);
+ const xAcc2 = this.getNewAccelerationX(forces2);
+ const yAcc2 = this.getNewAccelerationY(forces2);
+ xVel2 = this.getNewVelocity(xVel2, xAcc2);
+ yVel2 = this.getNewVelocity(yVel2, yAcc2);
+ xPos2 = this.getNewPosition(xPos2, xVel2);
+ yPos2 = this.getNewPosition(yPos2, yVel2);
+
+ let xVel3 = this.getNewVelocity(xVel, xAcc2 / 2);
+ let yVel3 = this.getNewVelocity(yVel, yAcc2 / 2);
+ let xPos3 = this.getNewPosition(xPos, xVel2 / 2);
+ let yPos3 = this.getNewPosition(yPos, yVel2 / 2);
+ const forces3 = this.getNewForces(xPos3, yPos3, xVel3, yVel3);
+ const xAcc3 = this.getNewAccelerationX(forces3);
+ const yAcc3 = this.getNewAccelerationY(forces3);
+ xVel3 = this.getNewVelocity(xVel3, xAcc3);
+ yVel3 = this.getNewVelocity(yVel3, yAcc3);
+ xPos3 = this.getNewPosition(xPos3, xVel3);
+ yPos3 = this.getNewPosition(yPos3, yVel3);
+
+ let xVel4 = this.getNewVelocity(xVel, xAcc3);
+ let yVel4 = this.getNewVelocity(yVel, yAcc3);
+ let xPos4 = this.getNewPosition(xPos, xVel3);
+ let yPos4 = this.getNewPosition(yPos, yVel3);
+ const forces4 = this.getNewForces(xPos4, yPos4, xVel4, yVel4);
+ const xAcc4 = this.getNewAccelerationX(forces4);
+ const yAcc4 = this.getNewAccelerationY(forces4);
+ xVel4 = this.getNewVelocity(xVel4, xAcc4);
+ yVel4 = this.getNewVelocity(yVel4, yAcc4);
+ xPos4 = this.getNewPosition(xPos4, xVel4);
+ yPos4 = this.getNewPosition(yPos4, yVel4);
+
+ xVel +=
+ this.props.timestepSize * (xAcc1 / 6.0 + xAcc2 / 3.0 + xAcc3 / 3.0 + xAcc4 / 6.0);
+ yVel +=
+ this.props.timestepSize * (yAcc1 / 6.0 + yAcc2 / 3.0 + yAcc3 / 3.0 + yAcc4 / 6.0);
+ xPos +=
+ this.props.timestepSize * (xVel1 / 6.0 + xVel2 / 3.0 + xVel3 / 3.0 + xVel4 / 6.0);
+ yPos +=
+ this.props.timestepSize * (yVel1 / 6.0 + yVel2 / 3.0 + yVel3 / 3.0 + yVel4 / 6.0);
+ }
+
+ this.setState({xVelocity: xVel});
+ this.setState({yVelocity: yVel});
+ this.setState({xPosition: xPos});
+ this.setState({yPosition: yPos});
+
+ this.props.dataDoc['updatedForces'] = (this.getNewForces(xPos, yPos, xVel, yVel));
+ };
+
+
+ labelBackgroundColor = `rgba(255,255,255,0.5)`;
+
+ render () {
+ return (
+ <div>
+ <div
+ className="weightContainer"
+ // onPointerDown={(e) => {
+ // if (this.draggable) {
+ // e.preventDefault();
+ // this.props.dataDoc['simulationPaused'] = true;
+ // this.setState({dragging: true});
+ // this.setState({clickPositionX: e.clientX})
+ // this.setState({clickPositionY: e.clientY})
+ // }
+ // }}
+ // onPointerMove={(e) => {
+ // e.preventDefault();
+ // if (this.state.dragging) {
+ // let newY = this.state.yPosition + e.clientY - this.state.clickPositionY;
+ // if (newY > this.props.yMax - 2 * this.props.radius) {
+ // newY = this.props.yMax - 2 * this.props.radius;
+ // }
+
+ // let newX = this.state.xPosition + e.clientX - this.state.clickPositionX;
+ // if (newX > this.props.xMax - 2 * this.props.radius) {
+ // newX = this.props.xMax - 2 * this.props.radius;
+ // } else if (newX < 0) {
+ // newX = 0;
+ // }
+ // this.setState({xPosition: newX})
+ // this.setState({yPosition: newY})
+ // this.setState({updatedStartPosX: newX})
+ // this.setState({updatedStartPosY: newY})
+ // this.props.dataDoc['positionYDisplay'] = Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100;
+ // this.setState({clickPositionX: e.clientX})
+ // this.setState({clickPositionY: e.clientY})
+ // this.setDisplayValues();
+ // }
+ // }}
+ // onPointerUp={(e) => {
+ // if (this.state.dragging) {
+ // e.preventDefault();
+ // if (!this.props.dataDoc['pendulum']) {
+ // this.resetEverything();
+ // }
+ // this.setState({dragging: false});
+ // let newY = this.state.yPosition + e.clientY - this.state.clickPositionY;
+ // if (newY > this.props.yMax - 2 * this.props.radius) {
+ // newY = this.props.yMax - 2 * this.props.radius;
+ // }
+
+ // let newX = this.state.xPosition + e.clientX - this.state.clickPositionX;
+ // if (newX > this.props.xMax - 2 * this.props.radius) {
+ // newX = this.props.xMax - 2 * this.props.radius;
+ // } else if (newX < 0) {
+ // newX = 0;
+ // }
+ // if (this.props.dataDoc['pendulum']) {
+ // const x = this.props.xMax / 2 - newX - this.props.radius;
+ // const y = newY + this.props.radius + 5;
+ // let angle = (Math.atan(y / x) * 180) / Math.PI;
+ // if (angle < 0) {
+ // angle += 180;
+ // }
+ // let oppositeAngle = 90 - angle;
+ // if (oppositeAngle < 0) {
+ // oppositeAngle = 90 - (180 - angle);
+ // }
+
+ // const pendulumLength = Math.sqrt(x * x + y * y);
+ // this.props.dataDoc['pendulumAngle'] = oppositeAngle;
+ // this.props.dataDoc['pendulumLength'] = Math.sqrt(x * x + y * y);
+ // const mag = 9.81 * Math.cos((oppositeAngle * Math.PI) / 180);
+ // const forceOfTension: IForce = {
+ // description: "Tension",
+ // magnitude: mag,
+ // directionInDegrees: angle,
+ // };
+ // this.setState({kineticFriction: false})
+ // this.setState({xVelocity: this.props.startVelX ?? 0})
+ // this.setState({yVelocity: this.props.startVelY ?? 0})
+ // this.setDisplayValues();
+ // this.props.dataDoc['updatedForces'] = ([this.forceOfGravity, forceOfTension]);
+ // }
+ // }
+ // }}
+ >
+ <div className="weight" style={this.weightStyle}>
+ <p className="weightLabel">{this.props.mass} kg</p>
+ </div>
+ </div>
+ {this.props.dataDoc['pendulum'] && (
+ <div
+ className="rod"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={300 + "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",
+ left: this.props.xMax / 2 + "px",
+ top: 30 + "px",
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {this.state.angleLabel}°
+ </p>
+ </div>
+ )}
+ </div>
+ )}
+ {!this.state.dragging && this.props.dataDoc['showAcceleration'] && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={300 + "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.dataDoc['updatedForces']) * 5}
+ y2={this.state.yPosition + this.props.radius + this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 5}
+ stroke={"green"}
+ strokeWidth="5"
+ markerEnd="url(#accArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left:
+ this.state.xPosition +
+ this.props.radius +
+ this.getNewAccelerationX(this.props.dataDoc['updatedForces']) * 5 +
+ 25 +
+ "px",
+ top:
+ this.state.yPosition +
+ this.props.radius +
+ this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 5 +
+ 25 +
+ "px",
+ lineHeight: 0.5,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 *
+ Math.sqrt(
+ Math.pow(this.getNewAccelerationX(this.props.dataDoc['updatedForces']) * 3, 2) +
+ Math.pow(this.getNewAccelerationY(this.props.dataDoc['updatedForces']) * 3, 2)
+ )
+ ) / 100}{" "}
+ m/s<sup>2</sup>
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!this.state.dragging && this.props.dataDoc['showVelocity'] && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={this.props.xMax + "px"} height={300 + "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 * 3}
+ y2={this.state.yPosition + this.props.radius + this.state.yVelocity * 3}
+ stroke={"blue"}
+ strokeWidth="5"
+ markerEnd="url(#velArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: this.state.xPosition + this.props.radius + this.state.xVelocity * 3 + 25 + "px",
+ top: this.state.yPosition + this.props.radius + this.state.yVelocity * 3 + "px",
+ lineHeight: 0.5,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 * Math.sqrt(this.state.xVelocity**2 + this.state.yVelocity**2)
+ ) / 100}{" "}
+ m/s
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!this.state.dragging &&
+ this.props.dataDoc['showForces'] &&
+ this.props.dataDoc['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) *
+ 10 *
+ Math.sin((force.directionInDegrees * Math.PI) / 180);
+ const arrowEndX: number =
+ arrowStartX +
+ Math.abs(force.magnitude) *
+ 10 *
+ 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={300 + "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>
+ <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: 0.5,
+ backgroundColor: this.labelBackgroundColor,
+ }}
+ >
+ {force.description && <p>{force.description}</p>}
+ {!force.description && <p>Force</p>}
+ {this.props.dataDoc['showForceMagnitudes'] && (
+ <p>{Math.round(100 * force.magnitude) / 100} N</p>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+ }
+};