diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/.DS_Store | bin | 10244 -> 10244 bytes | |||
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 1 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 14 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 2 | ||||
-rw-r--r-- | src/client/views/.DS_Store | bin | 10244 -> 10244 bytes | |||
-rw-r--r-- | src/client/views/collections/collectionLinear/CollectionLinearView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionSchema/CollectionSchemaView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss | 76 | ||||
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx | 2170 | ||||
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx | 179 | ||||
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json | 161 | ||||
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json | 600 | ||||
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx | 34 | ||||
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx | 1674 |
15 files changed, 4914 insertions, 6 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 8f0e384aa..be99aa5af 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index e0eb20faa..cdc8c275c 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 3be476e68..6f8582c2e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -49,6 +49,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/PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox'; import { ScreenshotBox } from '../views/nodes/ScreenshotBox'; import { ScriptingBox } from '../views/nodes/ScriptingBox'; @@ -684,12 +685,19 @@ export namespace Docs { options: { _layout_fitWidth: true, _fitHeight: true, nativeDimModifiable: true }, }, ], + [ + DocumentType.SIMULATION, + { + layout: { view: PhysicsSimulationBox, dataField: defaultDataKey }, + options: { _height: 100 }, + }, + ], ]); const suffix = 'Proto'; /** - * This function loads or initializes the prototype for each docment type. + * This function loads or initializes the prototype for each document type. * * This is an asynchronous function because it has to attempt * to fetch the prototype documents from the server. @@ -1208,6 +1216,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 99c63fc9d..fffb9c79b 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -266,6 +266,7 @@ export class CurrentUserUtils { {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_altContentUI: true}}, {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, }}, {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}}, + {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, _freeform_backgroundGrid: true, }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }}, {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }}, {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }}, @@ -292,6 +293,7 @@ export class CurrentUserUtils { { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)}, { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)}, { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)}, + { 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 note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)}, { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 5e39387b8..5fa69f28c 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index e0c294da4..3561844da 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -169,7 +169,7 @@ export class CollectionLinearView extends CollectionSubView() { switch (doc.layout) { case '<LinkingUI>': return this.getLinkUI(); case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI(); - case '<UndoStack>': return <UndoStack width={200} height={40} inline={true} />; + case '<UndoStack>': return <UndoStack key={doc[Id]} width={200} height={40} inline={true} />; } const nested = doc._type_collection === CollectionViewType.Linear; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 5962afa6b..36abad87e 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -8,7 +8,7 @@ import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; @@ -18,15 +18,12 @@ import { ContextMenu } from '../../ContextMenu'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView'; -import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { DefaultStyleProvider } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; -import { faHeartPulse } from '@fortawesome/free-solid-svg-icons'; -import { faGalacticSenate } from '@fortawesome/free-brands-svg-icons'; export enum ColumnType { Number, diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 348ef910a..05a3b56f7 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -35,6 +35,7 @@ import { LinkBox } from './LinkBox'; import { LoadingBox } from './LoadingBox'; import { MapBox } from './MapBox/MapBox'; import { PDFBox } from './PDFBox'; +import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from './RecordingBox'; import { ScreenshotBox } from './ScreenshotBox'; import { ScriptingBox } from './ScriptingBox'; @@ -265,6 +266,7 @@ export class DocumentContentsView extends React.Component< HTMLtag, ComparisonBox, LoadingBox, + PhysicsSimulationBox, SchemaRowBox, }} bindings={bindings} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss new file mode 100644 index 000000000..7193e3157 --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss @@ -0,0 +1,76 @@ +* { + box-sizing: border-box; + font-size: 14px; +} + +.mechanicsSimulationContainer { + background-color: white; + height: 100%; + width: 100%; + display: flex; + + .mechanicsSimulationEquationContainer { + position: fixed; + left: 60%; + 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%; +} + +.dropdownMenu { + z-index: 50; +}
\ No newline at end of file diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx new file mode 100644 index 000000000..ab93583df --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx @@ -0,0 +1,2170 @@ +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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CheckBox } from '../../search/CheckBox'; +import PauseIcon from '@mui/icons-material/Pause'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import ReplayIcon from '@mui/icons-material/Replay'; +import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; +import ArrowLeftIcon from '@mui/icons-material/ArrowLeft'; +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; +import EditIcon from '@mui/icons-material/Edit'; +import EditOffIcon from '@mui/icons-material/EditOff'; +import { Box, Button, Checkbox, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import './PhysicsSimulationBox.scss'; +import InputField from './PhysicsSimulationInputField'; +import * as questions from './PhysicsSimulationQuestions.json'; +import * as tutorials from './PhysicsSimulationTutorial.json'; +import Wall from './PhysicsSimulationWall'; +import Weight from './PhysicsSimulationWeight'; +import { NumCast } from '../../../../fields/Types'; +import { HeightSym, WidthSym } from '../../../../fields/Doc'; + +interface IWallProps { + length: number; + xPos: number; + yPos: number; + angleInDegrees: number; +} +interface IForce { + description: string; + magnitude: number; + directionInDegrees: number; + component: boolean; +} +interface VectorTemplate { + top: number; + left: number; + width: number; + height: number; + x1: number; + y1: number; + x2: number; + y2: number; + weightX: number; + weightY: number; +} +interface QuestionTemplate { + questionSetup: string[]; + variablesForQuestionSetup: string[]; + question: string; + answerParts: string[]; + answerSolutionDescriptions: string[]; + goal: string; + hints: { description: string; content: string }[]; +} + +interface TutorialTemplate { + question: string; + steps: { + description: string; + content: string; + forces: { + description: string; + magnitude: number; + directionInDegrees: number; + component: boolean; + }[]; + showMagnitude: boolean; + }[]; +} + +@observer +export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(PhysicsSimulationBox, fieldKey); + } + + constructor(props: any) { + super(props); + } + + // Constants + xMin = 0; + yMin = 0; + xMax = 100; + yMax = 100; + color = `rgba(0,0,0,0.5)`; + radius = 50; + wallPositions: IWallProps[] = []; + + componentDidMount() { + // Used throughout sims + this.layoutDoc._width = 1000; + this.layoutDoc._height = 800; + this.xMax = this.layoutDoc._width * 0.6; + this.yMax = this.layoutDoc._height * 0.9; + this.dataDoc.reviewCoefficient = this.dataDoc.reviewCoefficient ?? 0; + this.dataDoc.questionVariables = this.dataDoc.questionVariables ?? []; + this.dataDoc.accelerationXDisplay = this.dataDoc.accelerationXDisplay ?? 0; + this.dataDoc.accelerationYDisplay = this.dataDoc.accelerationYDisplay ?? 0; + this.dataDoc.componentForces = this.dataDoc.componentForces ?? []; + this.dataDoc.displayChange = this.dataDoc.displayChange ?? { xDisplay: 0, yDisplay: 0 }; + this.dataDoc.elasticCollisions = this.dataDoc.elasticCollisions ?? false; + this.dataDoc.gravity = this.dataDoc.gravity ?? -9.81; + this.dataDoc.mass = this.dataDoc.mass ?? 1; + this.dataDoc.mode = 'Freeform'; + this.dataDoc.positionXDisplay = this.dataDoc.positionXDisplay ?? 0; + this.dataDoc.positionYDisplay = this.dataDoc.positionYDisplay ?? 0; + this.dataDoc.showAcceleration = this.dataDoc.showAcceleration ?? false; + this.dataDoc.showComponentForces = this.dataDoc.showComponentForces ?? false; + this.dataDoc.showForces = this.dataDoc.showForces ?? true; + this.dataDoc.showForceMagnitudes = this.dataDoc.showForceMagnitudes ?? true; + this.dataDoc.showVelocity = this.dataDoc.showVelocity ?? false; + this.dataDoc.simulationPaused = this.dataDoc.simulationPaused ?? true; + this.dataDoc.simulationReset = this.dataDoc.simulationReset ?? false; + this.dataDoc.simulationSpeed = this.dataDoc.simulationSpeed ?? 2; + this.dataDoc.simulationType = this.dataDoc.simulationType ?? 'Inclined Plane'; + this.dataDoc.startForces = this.dataDoc.startForces ?? []; + this.dataDoc.startPosX = this.dataDoc.startPosX ?? Math.round((this.xMax * 0.5 - 200) * 10) / 10; + this.dataDoc.startPosY = this.dataDoc.startPosY ?? this.getDisplayYPos((400 - 0.08 * this.layoutDoc._height) * Math.tan((26 * Math.PI) / 180) + Math.sqrt(26)); + this.dataDoc.startVelX = this.dataDoc.startVelX ?? 0; + this.dataDoc.startVelY = this.dataDoc.startVelY ?? 0; + this.dataDoc.stepNumber = this.dataDoc.stepNumber ?? 0; + this.dataDoc.updatedForces = this.dataDoc.updatedForces ?? []; + this.dataDoc.velocityXDisplay = this.dataDoc.velocityXDisplay ?? 0; + this.dataDoc.velocityYDisplay = this.dataDoc.velocityYDisplay ?? 0; + + // Used for review mode + // this.dataDoc.currentForceSketch = this.dataDoc.currentForceSketch ?? null; + // this.dataDoc.deleteMode = this.dataDoc.deleteMode ?? false; + // this.dataDoc.forceSketches = this.dataDoc.forceSketches ?? []; + this.dataDoc.answers = []; + this.dataDoc.showIcon = false; + this.dataDoc.hintDialogueOpen = false; + this.dataDoc.noMovement = false; + this.dataDoc.questionNumber = 0; + this.dataDoc.questionPartOne = ''; + this.dataDoc.questionPartTwo = ''; + this.dataDoc.reviewGravityAngle = 0; + this.dataDoc.reviewGravityMagnitude = 0; + this.dataDoc.reviewNormalAngle = 0; + this.dataDoc.reviewNormalMagnitude = 0; + this.dataDoc.reviewStaticAngle = 0; + this.dataDoc.reviewStaticMagnitude = 0; + this.dataDoc.selectedSolutions = []; + this.dataDoc.selectedQuestion = this.dataDoc.selectedQuestion ?? questions.inclinePlane[0]; + // this.dataDoc.sketching = this.dataDoc.sketching ?? false; + + // Used for tutorial mode + this.dataDoc.selectedTutorial = this.dataDoc.selectedTutorial ?? tutorials.inclinePlane; + + // Used for uniform circular motion + this.dataDoc.circularMotionRadius = this.dataDoc.circularMotionRadius ?? 150; + + // Used for spring simulation + this.dataDoc.springConstant = this.dataDoc.springConstant ?? 0.5; + this.dataDoc.springRestLength = this.dataDoc.springRestLength ?? 200; + this.dataDoc.springStartLength = this.dataDoc.springStartLength ?? 200; + + // Used for pendulum simulation + this.dataDoc.adjustPendulumAngle = this.dataDoc.adjustPendulumAngle ?? { angle: 0, length: 0 }; + this.dataDoc.pendulumAngle = this.dataDoc.pendulumAngle ?? 0; + this.dataDoc.pendulumLength = this.dataDoc.pendulumLength ?? 300; + this.dataDoc.startPendulumAngle = this.dataDoc.startPendulumAngle ?? 0; + + // Used for wedge simulation + this.dataDoc.coefficientOfKineticFriction = this.dataDoc.coefficientOfKineticFriction ?? 0; + this.dataDoc.coefficientOfStaticFriction = this.dataDoc.coefficientOfStaticFriction ?? 0; + this.dataDoc.wedgeAngle = this.dataDoc.wedgeAngle ?? 26; + this.dataDoc.wedgeHeight = this.dataDoc.wedgeHeight ?? Math.tan((26 * Math.PI) / 180) * this.xMax * 0.5; + this.dataDoc.wedgeWidth = this.dataDoc.wedgeWidth ?? this.xMax * 0.5; + + // Used for pulley simulation + this.dataDoc.positionXDisplay2 = this.dataDoc.positionXDisplay2 ?? 0; + this.dataDoc.velocityXDisplay2 = this.dataDoc.velocityXDisplay2 ?? 0; + this.dataDoc.accelerationXDisplay2 = this.dataDoc.accelerationXDisplay2 ?? 0; + this.dataDoc.positionYDisplay2 = this.dataDoc.positionYDisplay2 ?? 0; + this.dataDoc.velocityYDisplay2 = this.dataDoc.velocityYDisplay2 ?? 0; + this.dataDoc.accelerationYDisplay2 = this.dataDoc.accelerationYDisplay2 ?? 0; + this.dataDoc.startPosX2 = this.dataDoc.startPosX2 ?? 0; + this.dataDoc.startPosY2 = this.dataDoc.startPosY2 ?? 0; + this.dataDoc.displayChange2 = this.dataDoc.displayChange2 ?? { xDisplay: 0, yDisplay: 0 }; + this.dataDoc.startForces2 = this.dataDoc.startForces2 ?? []; + this.dataDoc.updatedForces2 = this.dataDoc.updatedForces2 ?? []; + this.dataDoc.mass2 = this.dataDoc.mass2 ?? 1; + + // Setup simulation + this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode); + + // Create walls + let walls = []; + walls.push({ length: (this.xMax / this.layoutDoc._width) * 100, xPos: 0, yPos: 0, angleInDegrees: 0 }); + walls.push({ length: (this.xMax / this.layoutDoc._width) * 100, xPos: 0, yPos: (this.yMax / this.layoutDoc._height) * 100, angleInDegrees: 0 }); + walls.push({ length: (this.yMax / this.layoutDoc._height) * 100, xPos: 0, yPos: 0, angleInDegrees: 90 }); + walls.push({ length: (this.yMax / this.layoutDoc._height) * 100, xPos: (this.xMax / this.layoutDoc._width) * 100, yPos: 0, angleInDegrees: 90 }); + this.wallPositions = walls; + } + + componentDidUpdate() { + if (this.xMax !== this.layoutDoc[WidthSym]() * 0.6 || this.yMax != this.layoutDoc[HeightSym]() * 0.9) { + this.layoutDoc._width = Math.max(this.layoutDoc[WidthSym](), 800); + this.layoutDoc._height = Math.max(this.layoutDoc[HeightSym](), 600); + this.xMax = this.layoutDoc._width * 0.6; + this.yMax = this.layoutDoc._height * 0.9; + this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode); + } + } + + setupSimulation = (simulationType: string, mode: string) => { + this.dataDoc.simulationPaused = true; + if (simulationType != 'Circular Motion') { + this.dataDoc.startVelX = 0; + this.dataDoc.setStartVelY = 0; + this.dataDoc.velocityXDisplay = 0; + this.dataDoc.velocityYDisplay = 0; + } + if (mode == 'Freeform') { + this.dataDoc.showForceMagnitudes = true; + if (simulationType == 'One Weight') { + this.dataDoc.showComponentForces = false; + this.dataDoc.startPosY = this.yMin + 0.08 * this.layoutDoc[HeightSym](); + this.dataDoc.startPosX = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym](); + this.dataDoc.positionYDisplay = this.getDisplayYPos(this.yMin + 0.08 * this.layoutDoc[HeightSym]()); + this.dataDoc.positionXDisplay = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym](); + this.dataDoc.updatedForces = [ + { + description: 'Gravity', + magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass, + directionInDegrees: 270, + component: false, + }, + ]; + this.dataDoc.startForces = [ + { + description: 'Gravity', + magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass, + directionInDegrees: 270, + component: false, + }, + ]; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + } else if (simulationType == 'Inclined Plane') { + this.changeWedgeBasedOnNewAngle(26); + this.dataDoc.startForces = [ + { + description: 'Gravity', + magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass, + directionInDegrees: 270, + component: false, + }, + ]; + this.updateForcesWithFriction(Number(this.dataDoc.coefficientOfStaticFriction)); + } else if (simulationType == 'Pendulum') { + this.setupPendulum(); + } else if (simulationType == 'Spring') { + this.setupSpring(); + } else if (simulationType == 'Circular Motion') { + this.setupCircular(0); + } else if (simulationType == 'Pulley') { + this.setupPulley(); + } else if (simulationType == 'Suspension') { + this.setupSuspension(); + } + } else if (mode == 'Review') { + this.dataDoc.showComponentForces = false; + this.dataDoc.showForceMagnitudes = true; + this.dataDoc.showAcceleration = false; + this.dataDoc.showVelocity = false; + this.dataDoc.showForces = true; + this.generateNewQuestion(); + if (simulationType == 'One Weight') { + // TODO - one weight review problems + } else if (simulationType == 'Spring') { + this.setupSpring(); + // TODO - spring review problems + } else if (simulationType == 'Inclined Plane') { + this.dataDoc.updatedForces = []; + this.dataDoc.startForces = []; + } else if (simulationType == 'Pendulum') { + this.setupPendulum(); + // TODO - pendulum review problems + } else if (simulationType == 'Circular Motion') { + this.setupCircular(0); + // TODO - circular motion review problems + } else if (simulationType == 'Pulley') { + this.setupPulley(); + // TODO - pulley tutorial review problems + } else if (simulationType == 'Suspension') { + this.setupSuspension(); + // TODO - suspension tutorial review problems + } + } else if (mode == 'Tutorial') { + this.dataDoc.showComponentForces = false; + this.dataDoc.stepNumber = 0; + this.dataDoc.showAcceleration = false; + if (this.dataDoc.simulationType != 'Circular Motion') { + this.dataDoc.velocityXDisplay = 0; + this.dataDoc.velocityYDisplay = 0; + this.dataDoc.showVelocity = false; + } else { + this.dataDoc.velocityXDisplay = 20; + this.dataDoc.velocityYDisplay = 0; + this.dataDoc.showVelocity = true; + } + + if (this.dataDoc.simulationType == 'One Weight') { + this.dataDoc.showForces = true; + this.dataDoc.startPosY = this.yMax - 100; + this.dataDoc.startPosX = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym](); + this.dataDoc.selectedTutorial = tutorials.freeWeight; + this.dataDoc.startForces = this.getForceFromJSON(tutorials.freeWeight.steps[0].forces); + this.dataDoc.showForceMagnitudes = tutorials.freeWeight.steps[0].showMagnitude; + } else if (this.dataDoc.simulationType == 'Spring') { + this.dataDoc.showForces = true; + this.setupSpring(); + this.dataDoc.startPosY = this.yMin + 200 + 19.62; + this.dataDoc.startPosX = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym](); + this.dataDoc.selectedTutorial = tutorials.spring; + this.dataDoc.startForces = this.getForceFromJSON(tutorials.spring.steps[0].forces); + this.dataDoc.showForceMagnitudes = tutorials.spring.steps[0].showMagnitude; + } else if (this.dataDoc.simulationType == 'Pendulum') { + this.dataDoc.showForces = true; + const length = 300; + const angle = 30; + const x = length * Math.cos(((90 - angle) * Math.PI) / 180); + const y = length * Math.sin(((90 - angle) * Math.PI) / 180); + const xPos = this.xMax / 2 - x - 0.08 * this.layoutDoc[HeightSym](); + const yPos = y - 0.08 * this.layoutDoc[HeightSym]() - 5; + this.dataDoc.startPosX = xPos; + this.dataDoc.startPosY = yPos; + this.dataDoc.selectedTutorial = tutorials.pendulum; + this.dataDoc.startForces = this.getForceFromJSON(tutorials.pendulum.steps[0].forces); + this.dataDoc.showForceMagnitudes = tutorials.pendulum.steps[0].showMagnitude; + this.dataDoc.pendulumAngle = 30; + this.dataDoc.pendulumLength = 300; + this.dataDoc.adjustPendulumAngle = { angle: 30, length: 300 }; + } else if (this.dataDoc.simulationType == 'Inclined Plane') { + this.dataDoc.showForces = true; + this.dataDoc.wedgeAngle = 26; + this.changeWedgeBasedOnNewAngle(26); + this.dataDoc.selectedTutorial = tutorials.inclinePlane; + this.dataDoc.startForces = this.getForceFromJSON(tutorials.inclinePlane.steps[0].forces); + this.dataDoc.showForceMagnitudes = tutorials.inclinePlane.steps[0].showMagnitude; + } else if (this.dataDoc.simulationType == 'Circular Motion') { + this.dataDoc.showForces = true; + this.setupCircular(40); + this.dataDoc.selectedTutorial = tutorials.circular; + this.dataDoc.startForces = this.getForceFromJSON(tutorials.circular.steps[0].forces); + this.dataDoc.showForceMagnitudes = tutorials.circular.steps[0].showMagnitude; + } else if (this.dataDoc.simulationType == 'Pulley') { + this.dataDoc.showForces = true; + this.setupPulley(); + this.dataDoc.selectedTutorial = tutorials.pulley; + this.dataDoc.startForces = this.getForceFromJSON(tutorials.pulley.steps[0].forces); + this.dataDoc.showForceMagnitudes = tutorials.pulley.steps[0].showMagnitude; + } else if (this.dataDoc.simulationType == 'Suspension') { + this.dataDoc.showForces = true; + this.setupSuspension(); + this.dataDoc.selectedTutorial = tutorials.suspension; + this.dataDoc.startForces = this.getForceFromJSON(tutorials.suspension.steps[0].forces); + this.dataDoc.showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude; + } + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + } + }; + + // Helper function to go between display and real values + getDisplayYPos = (yPos: number) => { + return this.yMax - yPos - 2 * (0.08 * this.layoutDoc[HeightSym]()) + 5; + }; + getYPosFromDisplay = (yDisplay: number) => { + return this.yMax - yDisplay - 2 * (0.08 * this.layoutDoc[HeightSym]()) + 5; + }; + + // Update forces when coefficient of static friction changes in freeform mode + updateForcesWithFriction = (coefficient: number, width: number = this.dataDoc.wedgeWidth, height: number = this.dataDoc.wedgeHeight) => { + const normalForce: IForce = { + description: 'Normal Force', + magnitude: Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)) * this.dataDoc.mass, + directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI, + component: false, + }; + let frictionForce: IForce = { + description: 'Static Friction Force', + magnitude: coefficient * Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)) * this.dataDoc.mass, + directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI, + component: false, + }; + // reduce magnitude or friction force if necessary such that block cannot slide up plane + let yForce = -Math.abs(this.dataDoc.gravity) * this.dataDoc.mass; + yForce += normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180); + yForce += frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + if (yForce > 0) { + frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + Math.abs(this.dataDoc.gravity) * this.dataDoc.mass) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + } + const frictionForceComponent: IForce = { + description: 'Static Friction Force', + magnitude: coefficient * Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)), + directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI, + component: true, + }; + const normalForceComponent: IForce = { + description: 'Normal Force', + magnitude: Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)), + directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI, + component: true, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin(Math.PI / 2 - Math.atan(height / width)), + directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180, + component: true, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.cos(Math.PI / 2 - Math.atan(height / width)), + directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI, + component: true, + }; + const gravityForce: IForce = { + description: 'Gravity', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }; + if (coefficient != 0) { + this.dataDoc.startForces = [gravityForce, normalForce, frictionForce]; + this.dataDoc.updatedForces = [gravityForce, normalForce, frictionForce]; + this.dataDoc.componentForces = [frictionForceComponent, normalForceComponent, gravityParallel, gravityPerpendicular]; + } else { + this.dataDoc.startForces = [gravityForce, normalForce]; + this.dataDoc.updatedForces = [gravityForce, normalForce]; + this.dataDoc.componentForces = [normalForceComponent, gravityParallel, gravityPerpendicular]; + } + }; + + // Change wedge height and width and weight position to match new wedge angle + changeWedgeBasedOnNewAngle = (angle: number) => { + this.dataDoc.wedgeWidth = this.xMax * 0.5; + this.dataDoc.wedgeHeight = Math.tan((angle * Math.PI) / 180) * this.xMax * 0.5; + + // update weight position based on updated wedge width/height + let yPos = this.yMax - 0.08 * this.layoutDoc[HeightSym]() * 2 - Math.tan((angle * Math.PI) / 180) * this.xMax * 0.5; + + // adjust y position + if (angle >= 5 && angle < 10) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.1; + } else if (angle >= 10 && angle < 15) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.23; + } else if (angle >= 15 && angle < 20) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.26; + } else if (angle >= 20 && angle < 25) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.33; + } else if (angle >= 25 && angle < 30) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.35; + } else if (angle >= 30 && angle < 35) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.4; + } else if (angle >= 35 && angle < 40) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.45; + } else if (angle >= 40 && angle < 45) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.47; + } else if (angle >= 45) { + yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.52; + } + + this.dataDoc.startPosX = this.xMax * 0.25; + this.dataDoc.startPosY = yPos; + if (this.dataDoc.mode == 'Freeform') { + this.updateForcesWithFriction(Number(this.dataDoc.coefficientOfStaticFriction), this.xMax * 0.5, Math.tan((angle * Math.PI) / 180) * this.xMax * 0.5); + } + }; + + // In review mode, update forces when coefficient of static friction changed + updateReviewForcesBasedOnCoefficient = (coefficient: number) => { + let theta: number = Number(this.dataDoc.wedgeAngle); + let index = this.dataDoc.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45'); + if (index >= 0) { + theta = this.dataDoc.questionVariables[index]; + } + if (isNaN(theta)) { + return; + } + this.dataDoc.reviewGravityMagnitude = Math.abs(this.dataDoc.gravity); + this.dataDoc.reviewGravityAngle = 270; + this.dataDoc.reviewNormalMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180); + this.dataDoc.reviewNormalAngle = 90 - theta; + let yForce = -Math.abs(this.dataDoc.gravity); + yForce += Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((90 - theta) * Math.PI) / 180); + yForce += coefficient * Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((180 - theta) * Math.PI) / 180); + let friction = coefficient * Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180); + if (yForce > 0) { + friction = (-(Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180)) * Math.sin(((90 - theta) * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin(((180 - theta) * Math.PI) / 180); + } + this.dataDoc.reviewStaticMagnitude = friction; + this.dataDoc.reviewStaticAngle = 180 - theta; + }; + + // In review mode, update forces when wedge angle changed + updateReviewForcesBasedOnAngle = (angle: number) => { + this.dataDoc.reviewGravityMagnitude = Math.abs(this.dataDoc.gravity); + this.dataDoc.reviewGravityAngle = 270; + this.dataDoc.reviewNormalMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180); + this.dataDoc.reviewNormalAngle = 90 - angle; + let yForce = -Math.abs(this.dataDoc.gravity); + yForce += Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180) * Math.sin(((90 - Number(angle)) * Math.PI) / 180); + yForce += this.dataDoc.reviewCoefficient * Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180) * Math.sin(((180 - Number(angle)) * Math.PI) / 180); + let friction = this.dataDoc.reviewCoefficient * Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180); + if (yForce > 0) { + friction = (-(Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180)) * Math.sin(((90 - Number(angle)) * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin(((180 - Number(angle)) * Math.PI) / 180); + } + this.dataDoc.reviewStaticMagnitude = friction; + this.dataDoc.reviewStaticAngle = 180 - angle; + }; + + // Solve for the correct answers to the generated problem + getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => { + const solutions: number[] = []; + + let theta: number = Number(this.dataDoc.wedgeAngle); + let index = question.variablesForQuestionSetup.indexOf('theta - max 45'); + if (index >= 0) { + theta = questionVars[index]; + } + let muS: number = Number(this.dataDoc.coefficientOfStaticFriction); + index = question.variablesForQuestionSetup.indexOf('coefficient of static friction'); + if (index >= 0) { + muS = questionVars[index]; + } + + for (let i = 0; i < question.answerSolutionDescriptions.length; i++) { + const description = question.answerSolutionDescriptions[i]; + if (!isNaN(Number(description))) { + solutions.push(Number(description)); + } else if (description == 'solve normal force angle from wedge angle') { + solutions.push(90 - theta); + } else if (description == 'solve normal force magnitude from wedge angle') { + solutions.push(Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI)); + } else if (description == 'solve static force magnitude from wedge angle given equilibrium') { + let normalForceMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI); + let normalForceAngle = 90 - theta; + let frictionForceAngle = 180 - theta; + let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); + solutions.push(frictionForceMagnitude); + } else if (description == 'solve static force angle from wedge angle given equilibrium') { + solutions.push(180 - theta); + } else if (description == 'solve minimum static coefficient from wedge angle given equilibrium') { + let normalForceMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI); + let normalForceAngle = 90 - theta; + let frictionForceAngle = 180 - theta; + let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); + let frictionCoefficient = frictionForceMagnitude / normalForceMagnitude; + solutions.push(frictionCoefficient); + } else if (description == 'solve maximum wedge angle from coefficient of static friction given equilibrium') { + solutions.push((Math.atan(muS) * 180) / Math.PI); + } + } + this.dataDoc.selectedSolutions = solutions; + return solutions; + }; + + // In review mode, check if input answers match correct answers and optionally generate alert + checkAnswers = (showAlert: boolean = true) => { + let error: boolean = false; + let epsilon: number = 0.01; + if (this.dataDoc.selectedQuestion) { + for (let i = 0; i < this.dataDoc.selectedQuestion.answerParts.length; i++) { + if (this.dataDoc.selectedQuestion.answerParts[i] == 'force of gravity') { + if (Math.abs(this.dataDoc.reviewGravityMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'angle of gravity') { + if (Math.abs(this.dataDoc.reviewGravityAngle - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'normal force') { + if (Math.abs(this.dataDoc.reviewNormalMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'angle of normal force') { + if (Math.abs(this.dataDoc.reviewNormalAngle - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'force of static friction') { + if (Math.abs(this.dataDoc.reviewStaticMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'angle of static friction') { + if (Math.abs(this.dataDoc.reviewStaticAngle - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'coefficient of static friction') { + if (Math.abs(Number(this.dataDoc.coefficientOfStaticFriction) - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'wedge angle') { + if (Math.abs(Number(this.dataDoc.wedgeAngle) - this.dataDoc.selectedSolutions[i]) > epsilon) { + error = true; + } + } + } + } + if (showAlert) { + if (!error) { + this.dataDoc.simulationPaused = false; + setTimeout(() => { + this.dataDoc.simulationPaused = true; + }, 3000); + } else { + this.dataDoc.simulationPaused = false; + setTimeout(() => { + this.dataDoc.simulationPaused = true; + }, 3000); + } + } + if (this.dataDoc.selectedQuestion.goal == 'noMovement') { + if (!error) { + this.dataDoc.noMovement = true; + } else { + this.dataDoc.roMovement = false; + } + } + }; + + // Reset all review values to default + resetReviewValuesToDefault = () => { + this.dataDoc.reviewGravityMagnitude = 0; + this.dataDoc.reviewGravityAngle = 0; + this.dataDoc.reviewNormalMagnitude = 0; + this.dataDoc.reviewNormalAngle = 0; + this.dataDoc.reviewStaticMagnitude = 0; + this.dataDoc.reviewStaticAngle = 0; + this.dataDoc.coefficientOfKineticFriction = 0; + this.dataDoc.simulationPaused = true; + }; + + // In review mode, reset problem variables and generate a new question + generateNewQuestion = () => { + this.resetReviewValuesToDefault(); + + const vars: number[] = []; + let question: QuestionTemplate = questions.inclinePlane[0]; + + if (this.dataDoc.simulationType == 'Inclined Plane') { + if (this.dataDoc.questionNumber == questions.inclinePlane.length - 1) { + this.dataDoc.questionNumber = 0; + } else { + this.dataDoc.questionNumber = this.dataDoc.questionNumber + 1; + } + question = questions.inclinePlane[this.dataDoc.questionNumber]; + + let coefficient = 0; + let wedgeAngle = 0; + + for (let i = 0; i < question.variablesForQuestionSetup.length; i++) { + if (question.variablesForQuestionSetup[i] == 'theta - max 45') { + let randValue = Math.floor(Math.random() * 44 + 1); + vars.push(randValue); + wedgeAngle = randValue; + } else if (question.variablesForQuestionSetup[i] == 'coefficient of static friction') { + let randValue = Math.round(Math.random() * 1000) / 1000; + vars.push(randValue); + coefficient = randValue; + } + } + this.dataDoc.wedgeAngle = wedgeAngle; + this.changeWedgeBasedOnNewAngle(wedgeAngle); + this.dataDoc.coefficientOfStaticFriction = coefficient; + this.dataDoc.reviewCoefficient = coefficient; + } + let q = ''; + for (let i = 0; i < question.questionSetup.length; i++) { + q += question.questionSetup[i]; + if (i != question.questionSetup.length - 1) { + q += vars[i]; + if (question.variablesForQuestionSetup[i].includes('theta')) { + q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)'; + } + } + } + this.dataDoc.questionVariables = vars; + this.dataDoc.selectedQuestion = question; + this.dataDoc.questionPartOne = q; + this.dataDoc.questionPartTwo = question.question; + this.dataDoc.answers = this.getAnswersToQuestion(question, vars); + //this.dataDoc.simulationReset = (!this.dataDoc.simulationReset); + }; + + // Default setup for uniform circular motion simulation + setupCircular = (value: number) => { + this.dataDoc.showComponentForces = false; + this.dataDoc.startVelY = 0; + this.dataDoc.startVelX = value; + let xPos = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym](); + let yPos = (this.yMax + this.yMin) / 2 + this.dataDoc.circularMotionRadius - 0.08 * this.layoutDoc[HeightSym](); + this.dataDoc.startPosY = yPos; + this.dataDoc.startPosX = xPos; + const tensionForce: IForce = { + description: 'Centripetal Force', + magnitude: (this.dataDoc.startVelX ** 2 * this.dataDoc.mass) / this.dataDoc.circularMotionRadius, + directionInDegrees: 90, + component: false, + }; + this.dataDoc.updatedForces = [tensionForce]; + this.dataDoc.startForces = [tensionForce]; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }; + + // Default setup for pendulum simulation + setupPendulum = () => { + const length = 300; + const angle = 30; + const x = length * Math.cos(((90 - angle) * Math.PI) / 180); + const y = length * Math.sin(((90 - angle) * Math.PI) / 180); + const xPos = this.xMax / 2 - x - 0.08 * this.layoutDoc[HeightSym](); + const yPos = y - 0.08 * this.layoutDoc[HeightSym]() - 5; + this.dataDoc.startPosX = xPos; + this.dataDoc.startPosY = yPos; + const mag = this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin((60 * Math.PI) / 180); + const forceOfTension: IForce = { + description: 'Tension', + magnitude: mag, + directionInDegrees: 90 - angle, + component: false, + }; + + const tensionComponent: IForce = { + description: 'Tension', + magnitude: mag, + directionInDegrees: 90 - angle, + component: true, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin(((90 - angle) * Math.PI) / 180), + directionInDegrees: -angle - 90, + component: true, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.cos(((90 - angle) * Math.PI) / 180), + directionInDegrees: -angle, + component: true, + }; + + this.dataDoc.componentForces = [tensionComponent, gravityParallel, gravityPerpendicular]; + this.dataDoc.updatedForces = [ + { + description: 'Gravity', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }, + forceOfTension, + ]; + this.dataDoc.startForces = [ + { + description: 'Gravity', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }, + forceOfTension, + ]; + this.dataDoc.startPendulumAngle = 30; + this.dataDoc.pendulumAngle = 30; + this.dataDoc.pendulumLength = 300; + this.dataDoc.adjustPendulumAngle = { angle: 30, length: 300 }; + }; + + // Default setup for spring simulation + setupSpring = () => { + this.dataDoc.showComponentForces = false; + const gravityForce: IForce = { + description: 'Gravity', + magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass, + directionInDegrees: 270, + component: false, + }; + this.dataDoc.updatedForces = [gravityForce]; + this.dataDoc.startForces = [gravityForce]; + this.dataDoc.startPosX = this.xMax / 2 - 0.08 * this.layoutDoc[HeightSym](); + this.dataDoc.startPosY = 200; + this.dataDoc.springConstant = 0.5; + this.dataDoc.springRestLength = 200; + this.dataDoc.springStartLength = 200; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }; + + // Default setup for suspension simulation + setupSuspension = () => { + let xPos = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym](); + let yPos = this.yMin + 200; + this.dataDoc.startPosY = yPos; + this.dataDoc.startPosX = xPos; + this.dataDoc.positionYDisplay = this.getDisplayYPos(yPos); + this.dataDoc.positionXDisplay = xPos; + let tensionMag = (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) / (2 * Math.sin(Math.PI / 4)); + const tensionForce1: IForce = { + description: 'Tension', + magnitude: tensionMag, + directionInDegrees: 45, + component: false, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: tensionMag, + directionInDegrees: 135, + component: false, + }; + const grav: IForce = { + description: 'Gravity', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }; + this.dataDoc.updatedForces = [tensionForce1, tensionForce2, grav]; + this.dataDoc.startForces = [tensionForce1, tensionForce2, grav]; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }; + + // Default setup for pulley simulation + setupPulley = () => { + this.dataDoc.showComponentForces = false; + this.dataDoc.startPosY = (this.yMax + this.yMin) / 2; + this.dataDoc.startPosX = (this.xMin + this.xMax) / 2 - 2 * (0.08 * this.layoutDoc[HeightSym]()) - 5; + this.dataDoc.positionYDisplay = this.getDisplayYPos((this.yMax + this.yMin) / 2); + this.dataDoc.positionXDisplay = (this.xMin + this.xMax) / 2 - 2 * (0.08 * this.layoutDoc[HeightSym]()) - 5; + let a = (-1 * ((this.dataDoc.mass - this.dataDoc.mass2) * Math.abs(this.dataDoc.gravity))) / (this.dataDoc.mass + this.dataDoc.mass2); + const gravityForce1: IForce = { + description: 'Gravity', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }; + const tensionForce1: IForce = { + description: 'Tension', + magnitude: this.dataDoc.mass * a + this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 90, + component: false, + }; + a *= -1; + const gravityForce2: IForce = { + description: 'Gravity', + magnitude: this.dataDoc.mass2 * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: this.dataDoc.mass2 * a + this.dataDoc.mass2 * Math.abs(this.dataDoc.gravity), + directionInDegrees: 90, + component: false, + }; + + this.dataDoc.updatedForces = [gravityForce1, tensionForce1]; + this.dataDoc.startForces = [gravityForce1, tensionForce1]; + this.dataDoc.startPosY2 = (this.yMax + this.yMin) / 2; + this.dataDoc.startPosX2 = (this.xMin + this.xMax) / 2 + 5; + this.dataDoc.positionYDisplay2 = this.getDisplayYPos((this.yMax + this.yMin) / 2); + this.dataDoc.positionXDisplay2 = (this.xMin + this.xMax) / 2 + 5; + this.dataDoc.updatedForces2 = [gravityForce2, tensionForce2]; + this.dataDoc.startForces2 = [gravityForce2, tensionForce2]; + + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }; + + // Helper function used for tutorial and review mode + getForceFromJSON = ( + json: { + description: string; + magnitude: number; + directionInDegrees: number; + component: boolean; + }[] + ): IForce[] => { + const forces: IForce[] = []; + for (let i = 0; i < json.length; i++) { + const force: IForce = { + description: json[i].description, + magnitude: json[i].magnitude, + directionInDegrees: json[i].directionInDegrees, + component: json[i].component, + }; + forces.push(force); + } + return forces; + }; + + // Handle force change in review mode + updateReviewModeValues = () => { + const forceOfGravityReview: IForce = { + description: 'Gravity', + magnitude: this.dataDoc.reviewGravityMagnitude, + directionInDegrees: this.dataDoc.reviewGravityAngle, + component: false, + }; + const normalForceReview: IForce = { + description: 'Normal Force', + magnitude: this.dataDoc.reviewNormalMagnitude, + directionInDegrees: this.dataDoc.reviewNormalAngle, + component: false, + }; + const staticFrictionForceReview: IForce = { + description: 'Static Friction Force', + magnitude: this.dataDoc.reviewStaticMagnitude, + directionInDegrees: this.dataDoc.reviewStaticAngle, + component: false, + }; + this.dataDoc.startForces = [forceOfGravityReview, normalForceReview, staticFrictionForceReview]; + this.dataDoc.updatedForces = [forceOfGravityReview, normalForceReview, staticFrictionForceReview]; + }; + + render() { + return ( + <div className="physicsSimApp"> + <div className="mechanicsSimulationContainer"> + <div className="mechanicsSimulationContentContainer"> + <div className="mechanicsSimulationButtonsAndElements"> + <div className="mechanicsSimulationButtons"> + {!this.dataDoc.simulationPaused && ( + <div + style={{ + position: 'fixed', + left: 0.1 * this.layoutDoc[WidthSym]() + 'px', + top: 0.95 * this.layoutDoc[HeightSym]() + 'px', + width: 0.5 * this.layoutDoc[WidthSym]() + 'px', + }}> + <LinearProgress /> + </div> + )} + </div> + <div className="mechanicsSimulationElements"> + <Weight + dataDoc={this.dataDoc} + layoutDoc={this.layoutDoc} + wallPositions={this.wallPositions} + adjustPendulumAngle={this.dataDoc.adjustPendulumAngle} + gravity={this.dataDoc.gravity} + circularMotionRadius={this.dataDoc.circularMotionRadius} + componentForces={this.dataDoc.componentForces} + showComponentForces={this.dataDoc.showComponentForces} + color={'red'} + coefficientOfKineticFriction={Number(this.dataDoc.coefficientOfKineticFriction)} + displayXVelocity={this.dataDoc.velocityXDisplay} + displayYVelocity={this.dataDoc.velocityYDisplay} + elasticCollisions={this.dataDoc.elasticCollisions} + mass={this.dataDoc.mass} + mode={this.dataDoc.mode} + noMovement={this.dataDoc.noMovement} + paused={this.dataDoc.simulationPaused} + pendulumAngle={this.dataDoc.pendulumAngle} + pendulumLength={this.dataDoc.pendulumLength} + radius={0.08 * this.layoutDoc[HeightSym]()} + reset={this.dataDoc.simulationReset} + simulationSpeed={this.dataDoc.simulationSpeed} + startPendulumAngle={this.dataDoc.startPendulumAngle} + showAcceleration={this.dataDoc.showAcceleration} + showForceMagnitudes={this.dataDoc.showForceMagnitudes} + showForces={this.dataDoc.showForces} + showVelocity={this.dataDoc.showVelocity} + simulationType={this.dataDoc.simulationType} + springConstant={this.dataDoc.springConstant} + springStartLength={this.dataDoc.springStartLength} + springRestLength={this.dataDoc.springRestLength} + startForces={this.dataDoc.startForces} + startPosX={this.dataDoc.startPosX} + startPosY={this.dataDoc.startPosY ?? 0} + startVelX={this.dataDoc.startVelX} + startVelY={this.dataDoc.startVelY} + timestepSize={0.05} + updateDisplay={this.dataDoc.displayChange} + updatedForces={this.dataDoc.updatedForces} + wedgeHeight={this.dataDoc.wedgeHeight} + wedgeWidth={this.dataDoc.wedgeWidth} + xMax={this.xMax} + xMin={this.xMin} + yMax={this.yMax} + yMin={this.yMin} + /> + {this.dataDoc.simulationType == 'Pulley' && ( + <Weight + dataDoc={this.dataDoc} + layoutDoc={this.layoutDoc} + wallPositions={this.wallPositions} + adjustPendulumAngle={this.dataDoc.adjustPendulumAngle} + circularMotionRadius={this.dataDoc.circularMotionRadius} + gravity={this.dataDoc.gravity} + componentForces={this.dataDoc.componentForces} + showComponentForces={this.dataDoc.showComponentForces} + color={'blue'} + coefficientOfKineticFriction={Number(this.dataDoc.coefficientOfKineticFriction)} + displayXVelocity={this.dataDoc.velocityXDisplay2} + displayYVelocity={this.dataDoc.velocityYDisplay2} + elasticCollisions={this.dataDoc.elasticCollisions} + mass={this.dataDoc.mass2} + mode={this.dataDoc.mode} + noMovement={this.dataDoc.noMovement} + paused={this.dataDoc.simulationPaused} + pendulumAngle={this.dataDoc.pendulumAngle} + pendulumLength={this.dataDoc.pendulumLength} + radius={0.08 * this.layoutDoc[HeightSym]()} + reset={this.dataDoc.simulationReset} + simulationSpeed={this.dataDoc.simulationSpeed} + startPendulumAngle={this.dataDoc.startPendulumAngle} + showAcceleration={this.dataDoc.showAcceleration} + showForceMagnitudes={this.dataDoc.showForceMagnitudes} + showForces={this.dataDoc.showForces} + showVelocity={this.dataDoc.showVelocity} + simulationType={this.dataDoc.simulationType} + springConstant={this.dataDoc.springConstant} + springStartLength={this.dataDoc.springStartLength} + springRestLength={this.dataDoc.springRestLength} + startForces={this.dataDoc.startForces2} + startPosX={this.dataDoc.startPosX2} + startPosY={this.dataDoc.startPosY2} + startVelX={this.dataDoc.startVelX} + startVelY={this.dataDoc.startVelY} + timestepSize={0.05} + updateDisplay={this.dataDoc.displayChange2} + updatedForces={this.dataDoc.updatedForces2} + wedgeHeight={this.dataDoc.wedgeHeight} + wedgeWidth={this.dataDoc.wedgeWidth} + xMax={this.xMax} + xMin={this.xMin} + yMax={this.yMax} + yMin={this.yMin} + /> + )} + </div> + <div> + {(this.dataDoc.simulationType == 'One Weight' || this.dataDoc.simulationType == 'Inclined Plane') && + this.wallPositions && + this.wallPositions.map((element, index) => { + return <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />; + })} + </div> + </div> + </div> + <div className="mechanicsSimulationEquationContainer"> + <div className="mechanicsSimulationControls"> + <Stack direction="row" spacing={1}> + {this.dataDoc.simulationPaused && this.dataDoc.mode != 'Tutorial' && ( + <IconButton + onClick={() => { + this.dataDoc.simulationPaused = false; + }}> + <PlayArrowIcon /> + </IconButton> + )} + {!this.dataDoc.simulationPaused && this.dataDoc.mode != 'Tutorial' && ( + <IconButton + onClick={() => { + this.dataDoc.simulationPaused = true; + }}> + <PauseIcon /> + </IconButton> + )} + {this.dataDoc.simulationPaused && this.dataDoc.mode != 'Tutorial' && ( + <IconButton + onClick={() => { + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }}> + <ReplayIcon /> + </IconButton> + )} + </Stack> + <div className="dropdownMenu"> + <select + value={this.dataDoc.simulationType} + onChange={event => { + this.dataDoc.simulationType = event.target.value; + this.setupSimulation(event.target.value, this.dataDoc.mode); + }} + style={{ height: '2em', width: '100%', fontSize: '16px' }}> + <option value="One Weight">Projectile</option> + <option value="Inclined Plane">Inclined Plane</option> + <option value="Pendulum">Pendulum</option> + <option value="Spring">Spring</option> + <option value="Circular Motion">Circular Motion</option> + <option value="Pulley">Pulley</option> + <option value="Suspension">Suspension</option> + </select> + </div> + <div className="dropdownMenu"> + <select + value={this.dataDoc.mode} + onChange={event => { + this.dataDoc.mode = event.target.value; + this.setupSimulation(this.dataDoc.simulationType, event.target.value); + }} + style={{ height: '2em', width: '100%', fontSize: '16px' }}> + <option value="Tutorial">Tutorial Mode</option> + <option value="Freeform">Freeform Mode</option> + <option value="Review">Review Mode</option> + </select> + </div> + </div> + {this.dataDoc.mode == 'Review' && this.dataDoc.simulationType != 'Inclined Plane' && ( + <div className="wordProblemBox"> + <p>{this.dataDoc.simulationType} review problems in progress!</p> + <hr /> + </div> + )} + {this.dataDoc.mode == 'Review' && this.dataDoc.simulationType == 'Inclined Plane' && ( + <div> + {!this.dataDoc.hintDialogueOpen && ( + <IconButton + onClick={() => { + this.dataDoc.hintDialogueOpen = true; + }} + sx={{ + position: 'fixed', + left: this.xMax - 50 + 'px', + top: this.yMin + 14 + 'px', + }}> + <QuestionMarkIcon /> + </IconButton> + )} + <Dialog maxWidth={'sm'} fullWidth={true} open={this.dataDoc.hintDialogueOpen} onClose={() => (this.dataDoc.hintDialogueOpen = false)}> + <DialogTitle>Hints</DialogTitle> + <DialogContent> + {this.dataDoc.selectedQuestion.hints && + this.dataDoc.selectedQuestion.hints.map((hint: any, index: number) => { + return ( + <div key={index}> + <DialogContentText> + <details> + <summary> + <b> + Hint {index + 1}: {hint.description} + </b> + </summary> + {hint.content} + </details> + </DialogContentText> + </div> + ); + })} + </DialogContent> + <DialogActions> + <Button + onClick={() => { + this.dataDoc.hintDialogueOpen = false; + }}> + Close + </Button> + </DialogActions> + </Dialog> + <div className="wordProblemBox"> + <div className="question"> + <p>{this.dataDoc.questionPartOne}</p> + <p>{this.dataDoc.questionPartTwo}</p> + </div> + <div className="answers"> + {this.dataDoc.selectedQuestion.answerParts.includes('force of gravity') && ( + <InputField + label={<p>Gravity magnitude</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'reviewGravityMagnitude'} + step={0.1} + unit={'N'} + upperBound={50} + value={this.dataDoc.reviewGravityMagnitude} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('force of gravity')]} + labelWidth={'7em'} + /> + )} + {this.dataDoc.selectedQuestion.answerParts.includes('angle of gravity') && ( + <InputField + label={<p>Gravity angle</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'reviewGravityAngle'} + step={1} + unit={'°'} + upperBound={360} + value={this.dataDoc.reviewGravityAngle} + radianEquivalent={true} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('angle of gravity')]} + labelWidth={'7em'} + /> + )} + {this.dataDoc.selectedQuestion.answerParts.includes('normal force') && ( + <InputField + label={<p>Normal force magnitude</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'reviewNormalMagnitude'} + step={0.1} + unit={'N'} + upperBound={50} + value={this.dataDoc.reviewNormalMagnitude} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('normal force')]} + labelWidth={'7em'} + /> + )} + {this.dataDoc.selectedQuestion.answerParts.includes('angle of normal force') && ( + <InputField + label={<p>Normal force angle</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'reviewNormalAngle'} + step={1} + unit={'°'} + upperBound={360} + value={this.dataDoc.reviewNormalAngle} + radianEquivalent={true} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('angle of normal force')]} + labelWidth={'7em'} + /> + )} + {this.dataDoc.selectedQuestion.answerParts.includes('force of static friction') && ( + <InputField + label={<p>Static friction magnitude</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'reviewStaticMagnitude'} + step={0.1} + unit={'N'} + upperBound={50} + value={this.dataDoc.reviewStaticMagnitude} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('force of static friction')]} + labelWidth={'7em'} + /> + )} + {this.dataDoc.selectedQuestion.answerParts.includes('angle of static friction') && ( + <InputField + label={<p>Static friction angle</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'reviewStaticAngle'} + step={1} + unit={'°'} + upperBound={360} + value={this.dataDoc.reviewStaticAngle} + radianEquivalent={true} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('angle of static friction')]} + labelWidth={'7em'} + /> + )} + {this.dataDoc.selectedQuestion.answerParts.includes('coefficient of static friction') && ( + <InputField + label={ + <Box> + μ<sub>s</sub> + </Box> + } + lowerBound={0} + dataDoc={this.dataDoc} + prop={'coefficientOfStaticFriction'} + step={0.1} + unit={''} + upperBound={1} + value={this.dataDoc.coefficientOfStaticFriction} + effect={this.updateReviewForcesBasedOnCoefficient} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('coefficient of static friction')]} + /> + )} + {this.dataDoc.selectedQuestion.answerParts.includes('wedge angle') && ( + <InputField + label={<Box>θ</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'wedgeAngle'} + step={1} + unit={'°'} + upperBound={49} + value={this.dataDoc.wedgeAngle ?? 26} + effect={(val: number) => { + this.changeWedgeBasedOnNewAngle(val); + this.updateReviewForcesBasedOnAngle(val); + }} + radianEquivalent={true} + showIcon={this.dataDoc.showIcon} + correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('wedge angle')]} + /> + )} + </div> + </div> + </div> + )} + {this.dataDoc.mode == 'Tutorial' && ( + <div className="wordProblemBox"> + <div className="question"> + <h2>Problem</h2> + <p>{this.dataDoc.selectedTutorial.question}</p> + </div> + <div + style={{ + display: 'flex', + justifyContent: 'spaceBetween', + width: '100%', + }}> + <IconButton + onClick={() => { + let step = this.dataDoc.stepNumber - 1; + step = Math.max(step, 0); + step = Math.min(step, this.dataDoc.selectedTutorial.steps.length - 1); + this.dataDoc.stepNumber = step; + this.dataDoc.startForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces); + this.dataDoc.updatedForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces); + this.dataDoc.showForceMagnitudes = this.dataDoc.selectedTutorial.steps[step].showMagnitude; + }} + disabled={this.dataDoc.stepNumber == 0}> + <ArrowLeftIcon /> + </IconButton> + <div> + <h3> + Step {this.dataDoc.stepNumber + 1}: {this.dataDoc.selectedTutorial.steps[this.dataDoc.stepNumber].description} + </h3> + <p>{this.dataDoc.selectedTutorial.steps[this.dataDoc.stepNumber].content}</p> + </div> + <IconButton + onClick={() => { + let step = this.dataDoc.stepNumber + 1; + step = Math.max(step, 0); + step = Math.min(step, this.dataDoc.selectedTutorial.steps.length - 1); + this.dataDoc.stepNumber = step; + this.dataDoc.startForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces); + this.dataDoc.updatedForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces); + this.dataDoc.showForceMagnitudes = this.dataDoc.selectedTutorial.steps[step].showMagnitude; + }} + disabled={this.dataDoc.stepNumber == this.dataDoc.selectedTutorial.steps.length - 1}> + <ArrowRightIcon /> + </IconButton> + </div> + <div> + {(this.dataDoc.simulationType == 'One Weight' || this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Pendulum') && <p>Resources</p>} + {this.dataDoc.simulationType == 'One Weight' && ( + <ul> + <li> + <a + href="https://www.khanacademy.org/science/physics/one-dimensional-motion" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - One Dimensional Motion + </a> + </li> + <li> + <a + href="https://www.khanacademy.org/science/physics/two-dimensional-motion" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Two Dimensional Motion + </a> + </li> + </ul> + )} + {this.dataDoc.simulationType == 'Inclined Plane' && ( + <ul> + <li> + <a + href="https://www.khanacademy.org/science/physics/forces-newtons-laws#normal-contact-force" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Normal Force + </a> + </li> + <li> + <a + href="https://www.khanacademy.org/science/physics/forces-newtons-laws#inclined-planes-friction" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Inclined Planes + </a> + </li> + </ul> + )} + {this.dataDoc.simulationType == 'Pendulum' && ( + <ul> + <li> + <a + href="https://www.khanacademy.org/science/physics/forces-newtons-laws#tension-tutorial" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Tension + </a> + </li> + </ul> + )} + </div> + </div> + )} + {this.dataDoc.mode == 'Review' && this.dataDoc.simulationType == 'Inclined Plane' && ( + <div + style={{ + display: 'flex', + justifyContent: 'space-between', + marginTop: '10px', + }}> + <p + style={{ + color: 'blue', + textDecoration: 'underline', + cursor: 'pointer', + }} + onClick={() => (this.dataDoc.mode = 'Tutorial')}> + {' '} + Go to walkthrough{' '} + </p> + <div style={{ display: 'flex', flexDirection: 'column' }}> + <Button + onClick={() => { + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + this.checkAnswers(); + this.dataDoc.showIcon = true; + }} + variant="outlined"> + <p>Submit</p> + </Button> + <Button + onClick={() => { + this.generateNewQuestion(); + this.dataDoc.showIcon = false; + }} + variant="outlined"> + <p>New question</p> + </Button> + </div> + </div> + )} + {this.dataDoc.mode == 'Freeform' && ( + <div className="vars"> + <FormControl component="fieldset"> + <FormGroup> + {this.dataDoc.simulationType == 'One Weight' && ( + <FormControlLabel + control={<Checkbox checked={this.dataDoc.elasticCollisions} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />} + label="Make collisions elastic" + labelPlacement="start" + /> + )} + <FormControlLabel control={<Checkbox checked={this.dataDoc.showForces} onChange={() => (this.dataDoc.showForces = !this.dataDoc.showForces)} />} label="Show force vectors" labelPlacement="start" /> + {(this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Pendulum') && ( + <FormControlLabel + control={<Checkbox checked={this.dataDoc.showForces} onChange={() => (this.dataDoc.showComponentForces = !this.dataDoc.showComponentForces)} />} + label="Show component force vectors" + labelPlacement="start" + /> + )} + <FormControlLabel + control={<Checkbox checked={this.dataDoc.showAcceleration} onChange={() => (this.dataDoc.showAcceleration = !this.dataDoc.showAcceleration)} />} + label="Show acceleration vector" + labelPlacement="start" + /> + <FormControlLabel control={<Checkbox checked={this.dataDoc.showVelocity} onChange={() => (this.dataDoc.showVelocity = !this.dataDoc.showVelocity)} />} label="Show velocity vector" labelPlacement="start" /> + <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop={'simulationSpeed'} step={1} unit={'x'} upperBound={10} value={this.dataDoc.simulationSpeed ?? 2} labelWidth={'5em'} /> + {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Circular Motion' && ( + <InputField + label={<Box>Gravity</Box>} + lowerBound={-30} + dataDoc={this.dataDoc} + prop={'gravity'} + step={0.01} + unit={'m/s2'} + upperBound={0} + value={this.dataDoc.gravity ?? -9.81} + effect={(val: number) => { + this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode); + }} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Pulley' && ( + <InputField + label={<Box>Mass</Box>} + lowerBound={1} + dataDoc={this.dataDoc} + prop={'mass'} + step={0.1} + unit={'kg'} + upperBound={5} + value={this.dataDoc.mass ?? 1} + effect={(val: number) => { + this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode); + }} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'Pulley' && ( + <InputField + label={<Box>Red mass</Box>} + lowerBound={1} + dataDoc={this.dataDoc} + prop={'mass'} + step={0.1} + unit={'kg'} + upperBound={5} + value={this.dataDoc.mass ?? 1} + effect={(val: number) => { + this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode); + }} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'Pulley' && ( + <InputField + label={<Box>Blue mass</Box>} + lowerBound={1} + dataDoc={this.dataDoc} + prop={'mass2'} + step={0.1} + unit={'kg'} + upperBound={5} + value={this.dataDoc.mass2 ?? 1} + effect={(val: number) => { + this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode); + }} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'Circular Motion' && ( + <InputField + label={<Box>Rod length</Box>} + lowerBound={100} + dataDoc={this.dataDoc} + prop={'circularMotionRadius'} + step={5} + unit={'m'} + upperBound={250} + value={this.dataDoc.circularMotionRadius ?? 100} + effect={(val: number) => { + this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode); + }} + labelWidth={'5em'} + /> + )} + </FormGroup> + </FormControl> + {this.dataDoc.simulationType == 'Spring' && this.dataDoc.simulationPaused && ( + <div> + <InputField + label={<Typography color="inherit">Spring stiffness</Typography>} + lowerBound={0.1} + dataDoc={this.dataDoc} + prop={'springConstant'} + step={1} + unit={'N/m'} + upperBound={500} + value={this.dataDoc.springConstant ?? 0.5} + effect={(val: number) => { + this.dataDoc.simulationReset(!this.dataDoc.simulationReset); + }} + radianEquivalent={false} + mode={'Freeform'} + labelWidth={'7em'} + /> + <InputField + label={<Typography color="inherit">Rest length</Typography>} + lowerBound={10} + dataDoc={this.dataDoc} + prop={'springRestLength'} + step={100} + unit={''} + upperBound={500} + value={this.dataDoc.springRestLength ?? 200} + effect={(val: number) => { + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }} + radianEquivalent={false} + mode={'Freeform'} + labelWidth={'7em'} + /> + <InputField + label={<Typography color="inherit">Starting displacement</Typography>} + lowerBound={-(this.dataDoc.springRestLength - 10)} + dataDoc={this.dataDoc} + prop={''} + step={10} + unit={''} + upperBound={this.dataDoc.springRestLength} + value={this.dataDoc.springStartLength - this.dataDoc.springRestLength ?? 0} + effect={(val: number) => { + this.dataDoc.startPosY = this.dataDoc.springRestLength + val; + this.dataDoc.springStartLength = this.dataDoc.springRestLength + val; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }} + radianEquivalent={false} + mode={'Freeform'} + labelWidth={'7em'} + /> + </div> + )} + {this.dataDoc.simulationType == 'Inclined Plane' && this.dataDoc.simulationPaused && ( + <div> + <InputField + label={<Box>θ</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'wedgeAngle'} + step={1} + unit={'°'} + upperBound={49} + value={this.dataDoc.wedgeAngle ?? 26} + effect={(val: number) => { + this.changeWedgeBasedOnNewAngle(val); + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }} + radianEquivalent={true} + mode={'Freeform'} + labelWidth={'2em'} + /> + <InputField + label={ + <Box> + μ<sub>s</sub> + </Box> + } + lowerBound={0} + dataDoc={this.dataDoc} + prop={'coefficientOfStaticFriction'} + step={0.1} + unit={''} + upperBound={1} + value={this.dataDoc.coefficientOfStaticFriction ?? 0} + effect={(val: number) => { + this.updateForcesWithFriction(val); + if (val < Number(this.dataDoc.coefficientOfKineticFriction)) { + this.dataDoc.soefficientOfKineticFriction = val; + } + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }} + mode={'Freeform'} + labelWidth={'2em'} + /> + <InputField + label={ + <Box> + μ<sub>k</sub> + </Box> + } + lowerBound={0} + dataDoc={this.dataDoc} + prop={'coefficientOfKineticFriction'} + step={0.1} + unit={''} + upperBound={Number(this.dataDoc.coefficientOfStaticFriction)} + value={this.dataDoc.coefficientOfKineticFriction ?? 0} + effect={(val: number) => { + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }} + mode={'Freeform'} + labelWidth={'2em'} + /> + </div> + )} + {this.dataDoc.simulationType == 'Inclined Plane' && !this.dataDoc.simulationPaused && ( + <Typography> + θ: {Math.round(Number(this.dataDoc.wedgeAngle) * 100) / 100}° ≈ {Math.round(((Number(this.dataDoc.wedgeAngle) * Math.PI) / 180) * 100) / 100} rad + <br /> + μ <sub>s</sub>: {this.dataDoc.coefficientOfStaticFriction} + <br /> + μ <sub>k</sub>: {this.dataDoc.coefficientOfKineticFriction} + </Typography> + )} + {this.dataDoc.simulationType == 'Pendulum' && !this.dataDoc.simulationPaused && ( + <Typography> + θ: {Math.round(this.dataDoc.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.dataDoc.pendulumAngle * Math.PI) / 180) * 100) / 100} rad + </Typography> + )} + {this.dataDoc.simulationType == 'Pendulum' && this.dataDoc.simulationPaused && ( + <div> + <InputField + label={<Box>Angle</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'pendulumAngle'} + step={1} + unit={'°'} + upperBound={59} + value={this.dataDoc.pendulumAngle ?? 30} + effect={value => { + this.dataDoc.startPendulumAngle = value; + if (this.dataDoc.simulationType == 'Pendulum') { + const mag = this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.cos((value * Math.PI) / 180); + + const forceOfTension: IForce = { + description: 'Tension', + magnitude: mag, + directionInDegrees: 90 - value, + component: false, + }; + + const tensionComponent: IForce = { + description: 'Tension', + magnitude: mag, + directionInDegrees: 90 - value, + component: true, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: Math.abs(this.dataDoc.gravity) * Math.cos((value * Math.PI) / 180), + directionInDegrees: 270 - value, + component: true, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: Math.abs(this.dataDoc.gravity) * Math.sin((value * Math.PI) / 180), + directionInDegrees: -value, + component: true, + }; + + const length = this.dataDoc.pendulumLength; + const x = length * Math.cos(((90 - value) * Math.PI) / 180); + const y = length * Math.sin(((90 - value) * Math.PI) / 180); + const xPos = this.xMax / 2 - x - this.dataDoc.radius; + const yPos = y - this.dataDoc.radius - 5; + this.dataDoc.startPosX = xPos; + this.dataDoc.startPosY = yPos; + + this.dataDoc.startForces = [ + { + description: 'Gravity', + magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass, + directionInDegrees: 270, + component: false, + }, + forceOfTension, + ]; + this.dataDoc.updatedForces = [ + { + description: 'Gravity', + magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass, + directionInDegrees: 270, + component: false, + }, + forceOfTension, + ]; + this.dataDoc.componentForces = [tensionComponent, gravityParallel, gravityPerpendicular]; + this.dataDoc.adjustPendulumAngle = { + angle: value, + length: this.dataDoc.pendulumLength, + }; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + } + }} + radianEquivalent={true} + mode={'Freeform'} + labelWidth={'5em'} + /> + <InputField + label={<Box>Rod length</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop={'pendulumLength'} + step={1} + unit={'m'} + upperBound={400} + value={Math.round(this.dataDoc.pendulumLength)} + effect={value => { + if (this.dataDoc.simulationType == 'Pendulum') { + this.dataDoc.adjustPendulumAngle = { + angle: this.dataDoc.pendulumAngle, + length: value, + }; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + } + }} + radianEquivalent={false} + mode={'Freeform'} + labelWidth={'5em'} + /> + </div> + )} + </div> + )} + <div className="mechanicsSimulationEquation"> + {this.dataDoc.mode == 'Freeform' && ( + <table> + <tbody> + <tr> + <td>{this.dataDoc.simulationType == 'Pulley' ? 'Red Weight' : ''}</td> + <td>X</td> + <td>Y</td> + </tr> + <tr> + <td + style={{ cursor: 'help' }} + // onClick={() => { + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + <Box>Position</Box> + </td> + {(!this.dataDoc.simulationPaused || this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Circular Motion' || this.dataDoc.simulationType == 'Pulley') && ( + <td style={{ cursor: 'default' }}>{this.dataDoc.positionXDisplay} m</td> + )}{' '} + {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Inclined Plane' && this.dataDoc.simulationType != 'Circular Motion' && this.dataDoc.simulationType != 'Pulley' && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={this.dataDoc.simulationType == 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15} + dataDoc={this.dataDoc} + prop={'positionXDisplay'} + step={1} + unit={'m'} + upperBound={this.dataDoc.simulationType == 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15} + value={this.dataDoc.positionXDisplay} + effect={value => { + this.dataDoc.displayChange = { + xDisplay: value, + yDisplay: this.dataDoc.positionYDisplay, + }; + if (this.dataDoc['simulationType'] == 'Suspension') { + let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; + let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; + let deltaX1 = value + this.radius - x1rod; + let deltaX2 = x2rod - (value + this.radius); + let deltaY = this.getYPosFromDisplay(this.dataDoc.positionYDisplay) + this.radius; + let dir1T = Math.PI - Math.atan(deltaY / deltaX1); + let dir2T = Math.atan(deltaY / deltaX2); + let tensionMag2 = (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); + let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + dir1T = (dir1T * 180) / Math.PI; + dir2T = (dir2T * 180) / Math.PI; + const tensionForce1: IForce = { + description: 'Tension', + magnitude: tensionMag1, + directionInDegrees: dir1T, + component: false, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: tensionMag2, + directionInDegrees: dir2T, + component: false, + }; + const grav: IForce = { + description: 'Gravity', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }; + this.dataDoc['updatedForces'] = [tensionForce1, tensionForce2, grav]; + } + }} + small={true} + mode={'Freeform'} + /> + </td> + )}{' '} + {(!this.dataDoc.simulationPaused || this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Circular Motion' || this.dataDoc.simulationType == 'Pulley') && ( + <td style={{ cursor: 'default' }}>{this.dataDoc.positionYDisplay} m</td> + )}{' '} + {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Inclined Plane' && this.dataDoc.simulationType != 'Circular Motion' && this.dataDoc.simulationType != 'Pulley' && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={1} + dataDoc={this.dataDoc} + prop={'positionYDisplay'} + step={1} + unit={'m'} + upperBound={this.yMax - 110} + value={this.dataDoc.positionYDisplay} + effect={value => { + this.dataDoc.displayChange = { + xDisplay: this.dataDoc.positionXDisplay, + yDisplay: value, + }; + if (this.dataDoc['simulationType'] == 'Suspension') { + let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; + let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; + let deltaX1 = this.dataDoc.positionXDisplay + this.radius - x1rod; + let deltaX2 = x2rod - (this.dataDoc.positionXDisplay + this.radius); + let deltaY = this.getYPosFromDisplay(value) + this.radius; + let dir1T = Math.PI - Math.atan(deltaY / deltaX1); + let dir2T = Math.atan(deltaY / deltaX2); + let tensionMag2 = (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); + let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + dir1T = (dir1T * 180) / Math.PI; + dir2T = (dir2T * 180) / Math.PI; + const tensionForce1: IForce = { + description: 'Tension', + magnitude: tensionMag1, + directionInDegrees: dir1T, + component: false, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: tensionMag2, + directionInDegrees: dir2T, + component: false, + }; + const grav: IForce = { + description: 'Gravity', + magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity), + directionInDegrees: 270, + component: false, + }; + this.dataDoc['updatedForces'] = [tensionForce1, tensionForce2, grav]; + } + }} + small={true} + mode={'Freeform'} + /> + </td> + )}{' '} + </tr> + <tr> + <td + style={{ cursor: 'help' }} + // onClick={() => { + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + <Box>Velocity</Box> + </td> + {(!this.dataDoc.simulationPaused || (this.dataDoc.simulationType != 'One Weight' && this.dataDoc.simulationType != 'Circular Motion')) && ( + <td style={{ cursor: 'default' }}>{this.dataDoc.velocityXDisplay} m/s</td> + )}{' '} + {this.dataDoc.simulationPaused && (this.dataDoc.simulationType == 'One Weight' || this.dataDoc.simulationType == 'Circular Motion') && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={-50} + dataDoc={this.dataDoc} + prop={'velocityXDisplay'} + step={1} + unit={'m/s'} + upperBound={50} + value={this.dataDoc.velocityXDisplay} + effect={value => { + this.dataDoc.startVelX = value; + this.dataDoc.simulationReset = !this.dataDoc.simulationReset; + }} + small={true} + mode={'Freeform'} + /> + </td> + )}{' '} + {(!this.dataDoc.simulationPaused || this.dataDoc.simulationType != 'One Weight') && <td style={{ cursor: 'default' }}>{this.dataDoc.velocityYDisplay} m/s</td>}{' '} + {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'One Weight' && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={-50} + dataDoc={this.dataDoc} + prop={'velocityYDisplay'} + step={1} + unit={'m/s'} + upperBound={50} + value={this.dataDoc.velocityYDisplay} + effect={value => { + this.dataDoc.startVelY = -value; + this.dataDoc.displayChange = { + xDisplay: this.dataDoc.positionXDisplay, + yDisplay: this.dataDoc.positionYDisplay, + }; + }} + small={true} + mode={'Freeform'} + /> + </td> + )}{' '} + </tr> + <tr> + <td + style={{ cursor: 'help' }} + // onClick={() => { + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + <Box>Acceleration</Box> + </td> + <td style={{ cursor: 'default' }}> + {this.dataDoc.accelerationXDisplay} m/s<sup>2</sup> + </td> + <td style={{ cursor: 'default' }}> + {this.dataDoc.accelerationYDisplay} m/s<sup>2</sup> + </td> + </tr> + <tr> + <td> + <Box>Momentum</Box> + </td> + <td>{Math.round(this.dataDoc.velocityXDisplay * this.dataDoc.mass * 10) / 10} kg*m/s</td> + <td>{Math.round(this.dataDoc.velocityYDisplay * this.dataDoc.mass * 10) / 10} kg*m/s</td> + </tr> + </tbody> + </table> + )} + {this.dataDoc.mode == 'Freeform' && this.dataDoc.simulationType == 'Pulley' && ( + <table> + <tbody> + <tr> + <td>Blue Weight</td> + <td>X</td> + <td>Y</td> + </tr> + <tr> + <td> + <Box>Position</Box> + </td> + <td style={{ cursor: 'default' }}>{this.dataDoc.positionXDisplay2} m</td> + <td style={{ cursor: 'default' }}>{this.dataDoc.positionYDisplay2} m</td> + </tr> + <tr> + <td> + <Box>Velocity</Box> + </td> + <td style={{ cursor: 'default' }}>{this.dataDoc.velocityXDisplay2} m/s</td> + + <td style={{ cursor: 'default' }}>{this.dataDoc.velocityYDisplay2} m/s</td> + </tr> + <tr> + <td> + <Box>Acceleration</Box> + </td> + <td style={{ cursor: 'default' }}> + {this.dataDoc.accelerationXDisplay2} m/s<sup>2</sup> + </td> + <td style={{ cursor: 'default' }}> + {this.dataDoc.accelerationYDisplay2} m/s<sup>2</sup> + </td> + </tr> + <tr> + <td> + <Box>Momentum</Box> + </td> + <td>{Math.round(this.dataDoc.velocityXDisplay2 * this.dataDoc.mass * 10) / 10} kg*m/s</td> + <td>{Math.round(this.dataDoc.velocityYDisplay2 * this.dataDoc.mass * 10) / 10} kg*m/s</td> + </tr> + </tbody> + </table> + )} + </div> + {this.dataDoc.simulationType != 'Pendulum' && this.dataDoc.simulationType != 'Spring' && ( + <div> + <p>Kinematic Equations</p> + <ul> + <li> + Position: x<sub>1</sub>=x<sub>0</sub>+v<sub>0</sub>t+ + <sup>1</sup>⁄ + <sub>2</sub>at + <sup>2</sup> + </li> + <li> + Velocity: v<sub>1</sub>=v<sub>0</sub>+at + </li> + <li>Acceleration: a = F/m</li> + </ul> + </div> + )} + {this.dataDoc.simulationType == 'Spring' && ( + <div> + <p>Harmonic Motion Equations: Spring</p> + <ul> + <li> + Spring force: F<sub>s</sub>=kd + </li> + <li> + Spring period: T<sub>s</sub>=2π√<sup>m</sup>⁄ + <sub>k</sub> + </li> + <li>Equilibrium displacement for vertical spring: d = mg/k</li> + <li> + Elastic potential energy: U<sub>s</sub>=<sup>1</sup>⁄ + <sub>2</sub>kd<sup>2</sup> + </li> + <ul> + <li>Maximum when system is at maximum displacement, 0 when system is at 0 displacement</li> + </ul> + <li> + Translational kinetic energy: K=<sup>1</sup>⁄ + <sub>2</sub>mv<sup>2</sup> + </li> + <ul> + <li>Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)</li> + </ul> + </ul> + </div> + )} + {this.dataDoc.simulationType == 'Pendulum' && ( + <div> + <p>Harmonic Motion Equations: Pendulum</p> + <ul> + <li> + Pendulum period: T<sub>p</sub>=2π√<sup>l</sup>⁄ + <sub>g</sub> + </li> + </ul> + </div> + )} + </div> + </div> + <div + style={{ + position: 'fixed', + top: this.yMax - 120 + 20 + 'px', + left: this.xMin + 90 - 80 + 'px', + zIndex: -10000, + }}> + <svg width={100 + 'px'} height={100 + 'px'}> + <defs> + <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth"> + <path d="M0,0 L0,6 L9,3 z" fill={'#000000'} /> + </marker> + </defs> + <line x1={20} y1={70} x2={70} y2={70} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" /> + <line x1={20} y1={70} x2={20} y2={20} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" /> + </svg> + <p + style={{ + position: 'fixed', + top: this.yMax - 120 + 40 + 'px', + left: this.xMin + 90 - 80 + 'px', + }}> + {this.dataDoc.simulationType == 'Circular Motion' ? 'Z' : 'Y'} + </p> + <p + style={{ + position: 'fixed', + top: this.yMax - 120 + 80 + 'px', + left: this.xMin + 90 - 40 + 'px', + }}> + X + </p> + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx new file mode 100644 index 000000000..d595a499e --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx @@ -0,0 +1,179 @@ +import { TextField, InputAdornment } from '@mui/material'; +import { Doc } from '../../../../fields/Doc'; +import React = require('react'); +import TaskAltIcon from '@mui/icons-material/TaskAlt'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { isNumber } from 'lodash'; +export interface IInputProps { + label?: JSX.Element; + lowerBound: number; + dataDoc: Doc; + prop: string; + step: number; + unit: string; + upperBound: number; + value: number | string | Array<number | string>; + correctValue?: number; + showIcon?: boolean; + effect?: (val: number) => any; + radianEquivalent?: boolean; + small?: boolean; + mode?: string; + update?: boolean; + labelWidth?: string; +} + +interface IState { + tempValue: string | number | (string | number)[]; + tempRadianValue: number; + width: string; + margin: string; +} + +export default class InputField extends React.Component<IInputProps, IState> { + constructor(props: any) { + super(props); + this.state = { + tempValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : this.props.value, + tempRadianValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : (Number(this.props.value) * Math.PI) / 180, + width: this.props.small ? '6em' : '7em', + margin: this.props.small ? '0px' : '10px', + }; + } + + epsilon: number = 0.01; + + componentDidMount(): void { + this.setState({ tempValue: Number(this.props.value) }); + } + + componentDidUpdate(prevProps: Readonly<IInputProps>, prevState: Readonly<IState>, snapshot?: any): void { + if (prevProps.value != this.props.value && isNumber(this.props.value) && !isNaN(this.props.value)) { + if (this.props.mode == 'Freeform') { + if (isNumber(this.state.tempValue) && Math.abs(this.state.tempValue - Number(this.props.value)) > 1) { + this.setState({ tempValue: Number(this.props.value) }); + } + } + } + if (prevProps.update != this.props.update) { + this.externalUpdate(); + } + } + + externalUpdate = () => { + this.setState({ tempValue: Number(this.props.value) }); + this.setState({ tempRadianValue: (Number(this.props.value) * Math.PI) / 180 }); + }; + + onChange = (event: React.ChangeEvent<HTMLInputElement>) => { + let value = event.target.value == '' ? 0 : Number(event.target.value); + if (value > this.props.upperBound) { + value = this.props.upperBound; + } else if (value < this.props.lowerBound) { + value = this.props.lowerBound; + } + if (this.props.prop != '') { + this.props.dataDoc[this.props.prop] = value; + } + this.setState({ tempValue: event.target.value == '' ? event.target.value : value }); + this.setState({ tempRadianValue: (value * Math.PI) / 180 }); + if (this.props.effect) { + this.props.effect(value); + } + }; + + onChangeRadianValue = (event: React.ChangeEvent<HTMLInputElement>) => { + let value = event.target.value === '' ? 0 : Number(event.target.value); + if (value > 2 * Math.PI) { + value = 2 * Math.PI; + } else if (value < 0) { + value = 0; + } + if (this.props.prop != '') { + this.props.dataDoc[this.props.prop] = (value * 180) / Math.PI; + } + this.setState({ tempValue: (value * 180) / Math.PI }); + this.setState({ tempRadianValue: value }); + if (this.props.effect) { + this.props.effect((value * 180) / Math.PI); + } + }; + + render() { + return ( + <div + style={{ + display: 'flex', + lineHeight: '1.5', + textAlign: 'right', + alignItems: 'center', + }}> + {this.props.label && ( + <div + style={{ + marginTop: '0.3em', + marginBottom: '-0.5em', + width: this.props.labelWidth ?? '2em', + }}> + {this.props.label} + </div> + )} + <TextField + type="number" + variant="standard" + value={this.state.tempValue} + onChange={this.onChange} + sx={{ + height: '1em', + width: this.state.width, + marginLeft: this.state.margin, + zIndex: 'modal', + }} + inputProps={{ + step: this.props.step, + min: this.props.lowerBound, + max: this.props.upperBound, + type: 'number', + }} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) < this.epsilon && this.props.showIcon && <TaskAltIcon color={'success'} />} + {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) >= this.epsilon && this.props.showIcon && <ErrorOutlineIcon color={'error'} />} + </InputAdornment> + ), + endAdornment: <InputAdornment position="end">{this.props.unit}</InputAdornment>, + }} + /> + {this.props.radianEquivalent && ( + <div style={{ marginTop: '0.3em', marginBottom: '-0.5em', width: '1em' }}> + <p>≈</p> + </div> + )} + {this.props.radianEquivalent && ( + <TextField + type="number" + variant="standard" + value={this.state.tempRadianValue} + onChange={this.onChangeRadianValue} + sx={{ + height: '1em', + width: this.state.width, + marginLeft: this.state.margin, + zIndex: 'modal', + }} + inputProps={{ + step: Math.PI / 8, + min: 0, + max: 2 * Math.PI, + type: 'number', + }} + InputProps={{ + endAdornment: <InputAdornment position="end">rad</InputAdornment>, + }} + /> + )} + </div> + ); + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json b/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json new file mode 100644 index 000000000..cc79f7aad --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json @@ -0,0 +1,161 @@ +{ + "inclinePlane": [ + { + "questionSetup": [ + "There is a 1kg weight on an inclined plane. The plane is at a ", + " angle from the ground. The system is in equilibrium (the net force on the weight is 0)." + ], + "variablesForQuestionSetup": ["theta - max 45"], + "question": "What are the magnitudes and directions of the forces acting on the weight?", + "answerParts": [ + "force of gravity", + "angle of gravity", + "normal force", + "angle of normal force", + "force of static friction", + "angle of static friction" + ], + "answerSolutionDescriptions": [ + "9.81", + "270", + "solve normal force magnitude from wedge angle", + "solve normal force angle from wedge angle", + "solve static force magnitude from wedge angle given equilibrium", + "solve static force angle from wedge angle given equilibrium" + ], + "goal": "noMovement", + "hints": [ + { + "description": "Direction of Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3π/2 rad." + }, + { + "description": "Direction of Normal Force", + "content": "The normal force acts in the direction perpendicular to the incline plane: π/2-θ rad, where θ is the angle of the incline plane." + }, + { + "description": "Direction of Force of Friction", + "content": "The force of friction acts in the direction along the incline plane: π-θ rad, where θ is the angle of the incline plane." + }, + { + "description": "Magnitude of Force of Gravity", + "content": "The magnitude of the force of gravity is approximately 9.81." + }, + { + "description": "Magnitude of Normal Force", + "content": "The magnitude of the normal force is equal to m*g*cos(θ), where θ is the angle of the incline plane." + }, + { + "description": "Net Force in Equilibrium", + "content": "For the system to be in equilibrium, the sum of the x components of all forces must equal 0, and the sum of the y components of all forces must equal 0." + }, + { + "description": "X Component of Normal Force", + "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane." + }, + { + "description": "X Component of Force of Friction", + "content": "Since the net force in the x direction must be 0, we know the magnitude of the x component of the friction force is m*g*cos(θ)*cos(π/2-θ)." + }, + { + "description": "Y Component of Normal Force", + "content": "The y component of the normal force is equal to m*g*cos(θ)*sin(π/2-θ), where θ is the angle of the incline plane. The y component of gravity is equal to m*g" + }, + { + "description": "Y Component of Force of Friction", + "content": "Since the net force in the x direction must be 0, we know the magnitude of the y component of the friction force is m*g-m*g*cos(θ)*sin(π/2-θ)." + }, + { + "description": "Magnitude of Force of Friction", + "content": "Combining the x and y components of the friction force, we get the magnitude of the friction force is equal to sqrt((m*g*cos(θ)*cos(π/2-θ))^2 + (m*g-m*g*cos(θ)*sin(π/2-θ))^2)." + } + ] + }, + { + "questionSetup": [ + "There is a 1kg weight on an inclined plane. The plane is at a ", + " angle from the ground. The system is in equilibrium (the net force on the weight is 0)." + ], + "variablesForQuestionSetup": ["theta - max 45"], + "question": "What is the minimum coefficient of static friction?", + "answerParts": ["coefficient of static friction"], + "answerSolutionDescriptions": [ + "solve minimum static coefficient from wedge angle given equilibrium" + ], + "goal": "noMovement", + "hints": [ + { + "description": "Net Force in Equilibrium", + "content": "If the system is in equilibrium, the sum of the x components of all forces must equal 0. In this system, the normal force and force of static friction have non-zero x components." + }, + { + "description": "X Component of Normal Force", + "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane." + }, + { + "description": "X Component of Force of Friction", + "content": "The x component of the force of static friction is equal to μ*m*g*cos(θ)*cos(π-θ), where θ is the angle of the incline plane." + }, + { + "description": "Equation to Solve for Minimum Coefficient of Static Friction", + "content": "Since the net force in the x direction must be 0, we can solve the equation 0=m*g*cos(θ)*cos(π/2-θ)+μ*m*g*cos(θ)*cos(π-θ) for μ to find the minimum coefficient of static friction such that the system stays in equilibrium." + } + ] + }, + { + "questionSetup": [ + "There is a 1kg weight on an inclined plane. The coefficient of static friction is ", + ". The system is in equilibrium (the net force on the weight is 0)." + ], + "variablesForQuestionSetup": ["coefficient of static friction"], + "question": "What is the maximum angle of the plane from the ground?", + "answerParts": ["wedge angle"], + "answerSolutionDescriptions": [ + "solve maximum wedge angle from coefficient of static friction given equilibrium" + ], + "goal": "noMovement", + "hints": [ + { + "description": "Net Force in Equilibrium", + "content": "If the system is in equilibrium, the sum of the x components of all forces must equal 0. In this system, the normal force and force of static friction have non-zero x components." + }, + { + "description": "X Component of Normal Force", + "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane." + }, + { + "description": "X Component of Force of Friction", + "content": "The x component of the force of static friction is equal to μ*m*g*cos(θ)*cos(π-θ), where θ is the angle of the incline plane." + }, + { + "description": "Equation to Solve for Maximum Wedge Angle", + "content": "Since the net force in the x direction must be 0, we can solve the equation 0=m*g*cos(θ)*cos(π/2-θ)+μ*m*g*cos(θ)*cos(π-θ) for θ to find the maximum wedge angle such that the system stays in equilibrium." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Simplifying 0=m*g*cos(θ)*cos(π/2-θ)+μ*m*g*cos(θ)*cos(π-θ), we get cos(π/2-θ)=-μ*cos(π-θ)." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "The cosine subtraction formula states that cos(A-B)=cos(A)*cos(B)+sin(A)sin(B)." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Applying the cosine subtraction formula to cos(π/2-θ)=-μ*cos(π-θ), we get cos(π/2)*cos(θ)+sin(π/2)*sin(θ)=-μ*(cos(π)cos(θ)+sin(π)sin(θ))." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Simplifying cos(π/2)*cos(θ)-sin(π/2)*sin(θ)=-μ*(cos(π)cos(θ)-sin(π)sin(θ)), we get -sin(θ)=-μ*(-cos(θ))." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Simplifying -sin(θ)=-μ*(-cos(θ)), we get tan(θ)=-μ." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Solving for θ, we get θ = atan(μ)." + } + ] + } + ] +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json b/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json new file mode 100644 index 000000000..3015deaa4 --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json @@ -0,0 +1,600 @@ +{ + "freeWeight": { + "question": "A 1kg weight is at rest on the ground. What are the magnitude and directions of the forces acting on the weight?", + "steps": [ + { + "description": "Forces", + "content": "There are two forces acting on the weight: the force of gravity and the normal force.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3π/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Normal Force", + "content": "The normal force acts in the positive y direction: π/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Normal Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "pendulum": { + "question": "A 1kg weight on a 300m rod of negligible mass is released from an angle 30 degrees offset from equilibrium. What are the magnitude and directions of the forces acting on the weight immediately after release? (Ignore air resistance)", + "steps": [ + { + "description": "Forces", + "content": "There are two force acting on the weight: the force of gravity and the force of tension.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 8.5, + "directionInDegrees": 60, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3π/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Tension", + "content": "The force of tension acts along the direction of the rod. The rod is 30 degrees offset from equilibrium, so the direction along the rod is 90-30=60 degrees. The tension force has two components—the component creating the centripetal force and the component canceling out the parallel component of gravity. The weight has just been released, so it has velocity 0, meaning the centripetal force is 0. Thus, the tension force only acts to cancel out the parallel component of gravity. Thus, the magnitude of tension is m*g*sin(60°)", + "forces": [ + { + "description": "Tension", + "magnitude": 8.5, + "directionInDegrees": 60, + "component": false + }, + { + "description": "Gravity - Parallel Component", + "magnitude": 8.5, + "directionInDegrees": 240, + "component": true + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 8.5, + "directionInDegrees": 60, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "inclinePlane": { + "question": "There is a 1kg weight on an inclined plane. The plane is at an angle θ from the ground, and has a coefficient of static friction μ. The system is in equilibrium (the net force on the weight is 0). What are the magnitudes and directions of the forces acting on the weight?", + "steps": [ + { + "description": "Forces", + "content": "There are three forces acting on the weight: the force of gravity, the normal force, and the force of static friction.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 8.817, + "directionInDegrees": 64, + "component": false + }, + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3π/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Normal Force", + "content": "The normal force acts in the direction perpendicular to the incline plane: π/2-θ rad, where θ is the angle of the incline plane. The magnitude of the normal force is equal to m*g*cos(θ).", + "forces": [ + { + "description": "Normal Force", + "magnitude": 8.817, + "directionInDegrees": 64, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Force of Static Friction", + "content": "The force of static friction acts in the direction along the incline plane: π-θ rad, where θ is the angle of the incline plane. We can use the knowledge that the system is in equilibrium to solve for the magnitude of the force of static friction.", + "forces": [ + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Net X Force in Equilibrium", + "content": "For the system to be in equilibrium, the sum of the x components of all forces must equal 0. The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane. The x component of gravity is equal to 0. Since the net force in the x direction must be 0, we know the magnitude of the x component of the friction force is m*g*cos(θ)*cos(π/2-θ).", + "forces": [ + { + "description": "Normal Force - X Component", + "magnitude": 3.87, + "directionInDegrees": 0, + "component": true + }, + { + "description": "Friction Force - X Component", + "magnitude": 3.87, + "directionInDegrees": 180, + "component": true + } + ], + "showMagnitude": true + }, + { + "description": "Net Y Force Normal Force", + "content": "For the system to be in equilibrium, the sum of the y components of all forces must equal 0. The y component of the normal force is equal to m*g*cos(θ)*sin(π/2-θ), where θ is the angle of the incline plane. The y component of gravity is equal to m*g. Since the net force in the x direction must be 0, we know the magnitude of the y component of the friction force is m*g-m*g*cos(θ)*sin(π/2-θ).", + "forces": [ + { + "description": "Normal Force - Y Component ", + "magnitude": 7.92, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Gravity - Y Component ", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": true + }, + { + "description": "Friction Force - Y Component ", + "magnitude": 1.89, + "directionInDegrees": 90, + "component": true + } + ], + "showMagnitude": true + }, + { + "description": "Magnitude of Force of Friction", + "content": "Combining the x and y components of the friction force, we get the magnitude of the friction force is equal to sqrt((m*g*cos(θ)*cos(π/2-θ))^2 + (m*g*cos(θ)*sin(π/2-θ)-m*g)^2).", + "forces": [ + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 8.817, + "directionInDegrees": 64, + "component": false + }, + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "spring": { + "question": "A 1kg weight is on a spring of negligible mass with rest length 200m and spring constant 0.5. What is the equilibrium spring length?", + "steps": [ + { + "description": "Forces", + "content": "We can start by solving for the forces acting on the weight at any given point in time. There are two forces potentially acting on the weight: the force of gravity and the spring force. In equilibrium, these forces will be perfectly balanced.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Spring Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3π/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Spring Force", + "content": "The spring force acts in the negative y direction (3π/2 rad) if the spring is compressed. The spring force acts in the positive y direction (π/2 rad) if the spring is extended. Because the forces are perfectly balanced and gravity acts in the negative y direction, the spring force must act in the positive y direction and have the same magnitude as the force og gravity, m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Spring Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Spring Force", + "content": "We can use the spring force equation, Fs=kd to solve for the displacement such that Fs=mg. Setting them equal, we get mg=kd. Plugging in for the known values of m,g, and k, we get 1*9.81=0.5d. Solving for d, we get d=19.62 as the equilibrium starting displacement", + "forces": [ + { + "description": "Spring Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "circular": { + "question": "A 1kg weight is attached to a 100m rod of negligible mass. The weight is undergoing uniform circular motion with tangential velocity 40 m/s. What are the magnitude and directions of the forces acting on the weight? (Ignore air resistance)", + "steps": [ + { + "description": "Forces", + "content": "There is one force acting on the weight: the centripetal force.", + "forces": [ + { + "description": "Centripetal Force", + "magnitude": 16, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Centripetal Force", + "content": "The centripetal force is always directed toward the center of the circle. The formula for solving for the magnitude of centripetal force for an object undergoing uniform circular motion is Fc=mv^2 / r. Plugging in for known values, we get Fc=1*(40^2)/100. Solving for this, we get Fc=16", + "forces": [ + { + "description": "Centripetal Force", + "magnitude": 16, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "pulley": { + "question": "A 1kg red weight is attached to a simple pulley with a rope of negligible mass. A 1.5kg blue weight is attached to the other end of the simple pulley. What are the forces acting on the red weight?", + "steps": [ + { + "description": "Forces", + "content": "There are two force acting on the red weight: the force of gravity and the force of tension.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 11.77, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Gravity", + "content": "The force of gravity acts in the negative y direction: 3π/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Tension", + "content": "The force of tension acts in the positive y direction: π/2 rad. We know that the acceleration in a simple pulley system is (mass 2 - mass 1) * acceleration due to gravity / (mass 1 + mass 2) = (1.5-1) * 9.81 / (1.5+1) = 1.962 m/s^2. Because the acceleration is caused by the force of gravity and force of tension, we can solve for the force of tension acting on the weight as mass 1 * (a + acceleration due to gravity) = 1 * (1.962+9.81) = 11.77.", + "forces": [ + { + "description": "Tension", + "magnitude": 11.77, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 11.77, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "suspension": { + "question": "A 1kg weight is attached to two rods hanging from 45° angles from the ceiling. The system is in equilibrium, i.e. the weight does not move. What are the magnitudes and directions of the forces acting on the weight?", + "steps": [ + { + "description": "Forces", + "content": "There are three force acting on the red weight: the force of gravity, the force of tension from the left rod, and the force of tension from the right rod.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Left Tension", + "magnitude": 6.94, + "directionInDegrees": 135, + "component": false + }, + { + "description": "Right Tension", + "magnitude": 6.94, + "directionInDegrees": 45, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force X Components", + "content": "There are two forces with x components to consider: the tension from the left rod and the tension from the right rod. These must cancel each other out so that the net x force is 0.", + "forces": [ + { + "description": "Left Tension X Component", + "magnitude": 4.907, + "directionInDegrees": 180, + "component": true + }, + { + "description": "Right Tension X Component", + "magnitude": 4.907, + "directionInDegrees": 0, + "component": true + } + ], + "showMagnitude": false + }, { + "description": "Force Y Components", + "content": "There are three forces with y components to consider: the tension from the left rod, the tension from the right rod, and the force of gravity.", + "forces": [ + { + "description": "Left Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Right Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Gravity Y Component", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": true + } + ], + "showMagnitude": false + }, { + "description": "Force Y Components", + "content": "The y components of forces must cancel each other out so that the net y force is 0. Thus, gravity = left tension y component + right tension y component. Because the x components of tension are the same and the angles of each rod are the same, the y components must be the same. Thus, the y component for each force of tension must be 9.81/2.", + "forces": [ + { + "description": "Left Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Right Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Gravity Y Component", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": true + } + ], + "showMagnitude": true + }, { + "description": "Tension", + "content": "Now that we know the y component of tension for each rod is 4.907, we can solve for the full force of tension as 4.907 = T * sin(45°) -> T = 6.94.", + "forces": [ + { + "description": "Left Tension", + "magnitude": 6.94, + "directionInDegrees": 135, + "component": false + }, + { + "description": "Right Tension", + "magnitude": 6.94, + "directionInDegrees": 45, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Left Tension", + "magnitude": 6.94, + "directionInDegrees": 135, + "component": false + }, + { + "description": "Right Tension", + "magnitude": 6.94, + "directionInDegrees": 45, + "component": false + } + ], + "showMagnitude": true + } + ] + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx new file mode 100644 index 000000000..8cc1d0fbf --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/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 Wall extends React.Component<IWallProps> { + + constructor(props: any) { + super(props) + } + + wallStyle = { + width: this.props.angleInDegrees == 0 ? this.props.length + "%" : "5px", + height: this.props.angleInDegrees == 0 ? "5px" : 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/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx new file mode 100644 index 000000000..88af37791 --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx @@ -0,0 +1,1674 @@ +import { Doc } from '../../../../fields/Doc'; +import React = require('react'); +import "./PhysicsSimulationBox.scss"; + +interface IWallProps { + length: number; + xPos: number; + yPos: number; + angleInDegrees: number; +} +interface IForce { + description: string; + magnitude: number; + directionInDegrees: number; + component: boolean; +} +export interface IWeightProps { + dataDoc: Doc; + layoutDoc: Doc; + adjustPendulumAngle: { angle: number; length: number }; + circularMotionRadius: number; + coefficientOfKineticFriction: number; + color: string; + componentForces: IForce[]; + displayXVelocity: number; + displayYVelocity: number; + elasticCollisions: boolean; + gravity: number; + mass: number; + mode: string; + noMovement: boolean; + paused: boolean; + pendulumAngle: number; + pendulumLength: number; + radius: number; + reset: boolean; + showAcceleration: boolean; + showComponentForces: boolean; + showForceMagnitudes: boolean; + showForces: boolean; + showVelocity: boolean; + simulationSpeed: number; + simulationType: string; + springConstant: number; + springRestLength: number; + springStartLength: number; + startForces: IForce[]; + startPendulumAngle: number; + startPosX: number; + startPosY: number; + startVelX: number; + startVelY: number; + timestepSize: number; + updateDisplay: { xDisplay: number; yDisplay: number }; + updatedForces: IForce[]; + wallPositions: IWallProps[]; + wedgeHeight: number; + wedgeWidth: number; + xMax: number; + xMin: number; + yMax: number; + yMin: number; +} + +interface IState { + angleLabel: number, + clickPositionX: number, + clickPositionY: number, + coordinates: string, + dragging: boolean, + kineticFriction: boolean, + maxPosYConservation: number, + timer: number, + updatedStartPosX: any, + updatedStartPosY: any, + walls: IWallProps[], + xPosition: any, + xVelocity: number, + yPosition: any, + yVelocity: number, + xAccel: number, + yAccel: number, +} + +export default class Weight extends React.Component<IWeightProps, IState> { + + constructor(props: any) { + super(props) + this.state = { + angleLabel: 0, + clickPositionX: 0, + clickPositionY: 0, + coordinates: "", + dragging: false, + kineticFriction: false, + maxPosYConservation: 0, + timer: 0, + updatedStartPosX: this.props.startPosX ?? 0, + updatedStartPosY: this.props.startPosY ?? 0, + walls: [], + xPosition: this.props.startPosX ?? 0, + xVelocity: this.props.startVelX ? this.props.startVelX: 0, + yPosition: this.props.startPosY ?? 0, + yVelocity: this.props.startVelY ? this.props.startVelY: 0, + xAccel: 0, + yAccel: 0, + } + } + + componentDidMount() { + // Timer for animating the simulation + setInterval(() => { + this.setState({timer: this.state.timer + 1}); + }, 50); + } + + // Constants + draggable = + this.props.dataDoc['simulationType'] != "Inclined Plane" && + this.props.dataDoc['simulationType'] != "Pendulum" && + this.props.dataDoc['mode'] == "Freeform"; + epsilon = 0.0001; + labelBackgroundColor = `rgba(255,255,255,0.5)`; + + // Variables + weightStyle = { + alignItems: "center", + backgroundColor: this.props.color, + borderColor: "black", + borderRadius: 50 + "%", + borderStyle: "solid", + display: "flex", + height: 2 * this.props.radius + "px", + justifyContent: "center", + left: this.props.startPosX + "px", + position: "absolute" as "absolute", + top: this.props.startPosY + "px", + touchAction: "none", + width: 2 * this.props.radius + "px", + zIndex: 5, + }; + + // Helper function to go between display and real values + getDisplayYPos = (yPos: number) => { + return this.props.yMax - yPos - 2 * this.props.radius + 5; + }; + getYPosFromDisplay = (yDisplay: number) => { + return this.props.yMax - yDisplay - 2 * this.props.radius + 5; + }; + + // Set display values based on real values + setYPosDisplay = (yPos: number) => { + const displayPos = this.getDisplayYPos(yPos); + if (this.props.color == 'red') { + this.props.dataDoc['positionYDisplay'] = Math.round(displayPos * 100) / 100 + } else { + this.props.dataDoc['positionYDisplay2'] = Math.round(displayPos * 100) / 100 + } + }; + setXPosDisplay = (xPos: number) => { + if (this.props.color == 'red') { + this.props.dataDoc['positionXDisplay'] = Math.round(xPos * 100) / 100; + } else { + this.props.dataDoc['positionXDisplay2'] = Math.round(xPos * 100) / 100;} + }; + setYVelDisplay = (yVel: number) => { + if (this.props.color == 'red') { + this.props.dataDoc['velocityYDisplay'] = (-1 * Math.round(yVel * 100)) / 100; + } else { + this.props.dataDoc['velocityYDisplay2'] = (-1 * Math.round(yVel * 100)) / 100;} + }; + setXVelDisplay = (xVel: number) => { + if (this.props.color == 'red') { + this.props.dataDoc['velocityXDisplay'] = Math.round(xVel * 100) / 100; + } else { + this.props.dataDoc['velocityXDisplay2'] = Math.round(xVel * 100) / 100;} + }; + + // Update display values when simulation updates + setDisplayValues = ( + xPos: number = this.state.xPosition, + yPos: number = this.state.yPosition, + xVel: number = this.state.xVelocity, + yVel: number = this.state.yVelocity + ) => { + this.setYPosDisplay(yPos); + this.setXPosDisplay(xPos); + this.setYVelDisplay(yVel); + this.setXVelDisplay(xVel); + if (this.props.color == 'red') { + this.props.dataDoc['accelerationYDisplay'] = + (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100 + ; + this.props.dataDoc['accelerationXDisplay'] = + Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100 + ; + } else { + this.props.dataDoc['accelerationYDisplay2'] = + (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100 + ; + this.props.dataDoc['accelerationXDisplay2'] = + Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100 + ;} + + this.setState({xAccel : (Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100)}) + this.setState({yAccel : + (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100 + }); + }; + + componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void { + // Change pendulum angle from input field + if (prevProps.adjustPendulumAngle != this.props.adjustPendulumAngle) { + let length = this.props.adjustPendulumAngle.length; + const x = + length * Math.cos(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180); + const y = + length * Math.sin(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180); + const xPos = this.props.xMax / 2 - x - this.props.radius; + const yPos = y - this.props.radius - 5; + this.setState({xPosition: xPos}) + this.setState({yPosition: yPos}) + this.setState({updatedStartPosX: xPos}) + this.setState({updatedStartPosY: yPos}) + this.props.dataDoc['pendulumAngle'] = this.props.adjustPendulumAngle.angle + this.props.dataDoc['pendulumLength'] = this.props.adjustPendulumAngle.length + } + + // When display values updated by user, update real values + if (prevProps.updateDisplay != this.props.updateDisplay) { + if (this.props.updateDisplay.xDisplay != this.state.xPosition) { + let x = this.props.updateDisplay.xDisplay; + x = Math.max(0, x); + x = Math.min(x, this.props.xMax - 2 * this.props.radius); + this.setState({updatedStartPosX: x}) + this.setState({xPosition: x}) + if (this.props.color == 'red') { + this.props.dataDoc['positionXDisplay'] = x + } else { + this.props.dataDoc['positionXDisplay2'] = x + } + } + + if (this.props.updateDisplay.yDisplay != this.getDisplayYPos(this.state.yPosition)) { + let y = this.props.updateDisplay.yDisplay; + y = Math.max(0, y); + y = Math.min(y, this.props.yMax - 2 * this.props.radius); + let coordinatePosition = this.getYPosFromDisplay(y); + this.setState({updatedStartPosY: coordinatePosition}) + this.setState({yPosition: coordinatePosition}) + if (this.props.color == 'red') { + this.props.dataDoc['positionYDisplay'] = y + } else { + this.props.dataDoc['positionYDisplay2'] = y + } + } + + if (this.props.displayXVelocity != this.state.xVelocity) { + let x = this.props.displayXVelocity; + this.setState({xVelocity: x}) + if (this.props.color == 'red') { + this.props.dataDoc['velocityXDisplay'] = x + } else { + this.props.dataDoc['velocityXDisplay2'] = x + } + } + + if (this.props.displayYVelocity != -this.state.yVelocity) { + let y = this.props.displayYVelocity; + this.setState({yVelocity: -y}) + if (this.props.color == 'red') { + this.props.dataDoc['velocityYDisplay'] = y + } else { + this.props.dataDoc['velocityYDisplay2'] = y + } + } + } + + // Prevent bug when switching between sims + if (prevProps.startForces != this.props.startForces) { + this.setState({xVelocity: this.props.startVelX}) + this.setState({yVelocity: this.props.startVelY}) + this.setDisplayValues(); + } + + // Make sure weight doesn't go above max height + if ((prevState.updatedStartPosY != this.state.updatedStartPosY || prevProps.startVelY != this.props.startVelY) && !isNaN(this.state.updatedStartPosY) && !isNaN(this.props.startVelY)){ + if (this.props.dataDoc['simulationType'] == "One Weight") { + let maxYPos = this.state.updatedStartPosY; + if (this.props.startVelY != 0) { + maxYPos -= (this.props.startVelY * this.props.startVelY) / (2 * Math.abs(this.props.gravity)); + } + if (maxYPos < 0) { + maxYPos = 0; + } + this.setState({maxPosYConservation: maxYPos}) + } + } + + // Check for collisions and update + if (!this.props.paused) { + if (prevState.timer != this.state.timer) { + if (!this.props.noMovement) { + let collisions = false; + if ( + this.props.dataDoc['simulationType'] == "One Weight" || + this.props.dataDoc['simulationType'] == "Inclined Plane" + ) { + const collisionsWithGround = this.checkForCollisionsWithGround(); + const collisionsWithWalls = this.checkForCollisionsWithWall(); + collisions = collisionsWithGround || collisionsWithWalls; + } + if (this.props.dataDoc['simulationType'] == "Pulley") { + if (this.state.yPosition <= this.props.yMin + 100 || this.state.yPosition >= this.props.yMax - 100) { + collisions = true; + } + } + if (!collisions) { + this.update(); + } + this.setDisplayValues(); + } + } + } + + // Reset everything on reset button click + if (prevProps.reset != this.props.reset) { + this.resetEverything(); + } + + // Convert from static to kinetic friction if/when weight slips on inclined plane + if (prevState.xVelocity != this.state.xVelocity) { + if ( + this.props.dataDoc['simulationType'] == "Inclined Plane" && + Math.abs(this.state.xVelocity) > 0.1 && + this.props.dataDoc['mode'] != "Review" && + !this.state.kineticFriction + ) { + this.setState({kineticFriction: true}) + const normalForce: IForce = { + description: "Normal Force", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: false, + }; + let frictionForce: IForce = { + description: "Kinetic Friction Force", + magnitude: + this.props.mass * + this.props.coefficientOfKineticFriction * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: false, + }; + // reduce magnitude of friction force if necessary such that block cannot slide up plane + let yForce = -Math.abs(this.props.gravity); + yForce += + normalForce.magnitude * + Math.sin((normalForce.directionInDegrees * Math.PI) / 180); + yForce += + frictionForce.magnitude * + Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + if (yForce > 0) { + frictionForce.magnitude = + (-normalForce.magnitude * + Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + + Math.abs(this.props.gravity)) / + Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + } + + const frictionForceComponent: IForce = { + description: "Kinetic Friction Force", + + magnitude: + this.props.mass * + this.props.coefficientOfKineticFriction * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: true, + }; + const normalForceComponent: IForce = { + description: "Normal Force", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: true, + }; + const gravityParallel: IForce = { + description: "Gravity Parallel Component", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.sin(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - + 90 - + (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI + + 180, + component: true, + }; + const gravityPerpendicular: IForce = { + description: "Gravity Perpendicular Component", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.cos(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 360 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: true, + }; + const gravityForce: IForce = { + description: "Gravity", + magnitude: this.props.mass * Math.abs(this.props.gravity), + directionInDegrees: 270, + component: false, + }; + if (this.props.coefficientOfKineticFriction != 0) { + this.props.dataDoc['updatedForces'] = ([gravityForce, normalForce, frictionForce]); + this.props.dataDoc['componentForces'] = ([ + frictionForceComponent, + normalForceComponent, + gravityParallel, + gravityPerpendicular, + ]); + } else { + this.props.dataDoc['updatedForces'] = ([gravityForce, normalForce]); + this.props.dataDoc['componentForces'] = ([ + normalForceComponent, + gravityParallel, + gravityPerpendicular, + ]); + } + } + } + + // Add/remove walls when simulation type changes + if (prevProps.simulationType != this.props.simulationType) { + let w: IWallProps[] = []; + if (this.props.dataDoc['simulationType'] == "One Weight" || this.props.dataDoc['simulationType'] == "Inclined Plane") { + w = this.props.wallPositions + } + this.setState({walls: w}) + } + + // Update x position when start pos x changes + if (prevProps.startPosX != this.props.startPosX) { + if (this.props.paused && !isNaN(this.props.startPosX)) { + this.setState({xPosition: this.props.startPosX}) + this.setState({updatedStartPosX: this.props.startPosX}) + this.setXPosDisplay(this.props.startPosX) + } + } + + // Update y position when start pos y changes TODO debug + if (prevProps.startPosY != this.props.startPosY) { + if (this.props.paused && !isNaN(this.props.startPosY)) { + this.setState({yPosition: this.props.startPosY}) + this.setState({updatedStartPosY: this.props.startPosY ?? 0}) + this.setYPosDisplay(this.props.startPosY ?? 0) + } + } + + // Update wedge coordinates + if (prevProps.wedgeWidth != this.props.wedgeWidth || prevProps.wedgeHeight != this.props.wedgeHeight) { + const left = this.props.xMax * 0.25; + const coordinatePair1 = Math.round(left) + "," + this.props.yMax + " "; + const coordinatePair2 = Math.round(left + this.props.wedgeWidth) + "," + this.props.yMax + " "; + const coordinatePair3 = Math.round(left) + "," + (this.props.yMax - this.props.wedgeHeight); + const coord = coordinatePair1 + coordinatePair2 + coordinatePair3; + this.setState({coordinates: coord}) + } + + if (this.state.xPosition != prevState.xPosition || this.state.yPosition != prevState.yPosition) { + this.weightStyle = { + alignItems: "center", + backgroundColor: this.props.color, + borderColor: "black", + borderRadius: 50 + "%", + borderStyle: "solid", + display: "flex", + height: 2 * this.props.radius + "px", + justifyContent: "center", + left: this.state.xPosition + "px", + position: "absolute" as "absolute", + top: this.state.yPosition + "px", + touchAction: "none", + width: 2 * this.props.radius + "px", + zIndex: 5, + }; + } + } + + // Reset simulation on reset button click + resetEverything = () => { + this.setState({kineticFriction: false}) + this.setState({xPosition: this.state.updatedStartPosX}) + this.setState({yPosition: this.state.updatedStartPosY}) + this.setState({xVelocity: this.props.startVelX ?? 0}) + this.setState({yVelocity: this.props.startVelY ?? 0}) + this.props.dataDoc['pendulumAngle'] = this.props.dataDoc['startPendulumAngle'] + this.props.dataDoc['updatedForces'] = (this.props.dataDoc['startForces']) + this.props.dataDoc['updatedForces2'] = (this.props.dataDoc['startForces2']) + if (this.props.color == 'red') { + this.props.dataDoc['positionXDisplay'] = this.state.updatedStartPosX + this.props.dataDoc['positionYDisplay'] = this.state.updatedStartPosY + this.props.dataDoc['velocityXDisplay'] = this.props.startVelX ?? 0 + this.props.dataDoc['velocityYDisplay'] = this.props.startVelY ?? 0 + this.props.dataDoc['accelerationXDisplay'] = 0 + this.props.dataDoc['accelerationYDisplay'] = 0 + } else { + this.props.dataDoc['positionXDisplay2'] = this.state.updatedStartPosX + this.props.dataDoc['positionYDisplay2'] = this.state.updatedStartPosY + this.props.dataDoc['velocityXDisplay2'] = this.props.startVelX ?? 0 + this.props.dataDoc['velocityYDisplay2'] = this.props.startVelY ?? 0 + this.props.dataDoc['accelerationXDisplay2'] = 0 + this.props.dataDoc['accelerationYDisplay2'] = 0 + } + this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle'] ?? 0 * 100) / 100}) + }; + + // Compute x acceleration from forces, F=ma + getNewAccelerationX = (forceList: IForce[]) => { + let newXAcc = 0; + if (forceList) { + forceList.forEach((force) => { + newXAcc += + (force.magnitude * + Math.cos((force.directionInDegrees * Math.PI) / 180)) / + this.props.mass; + }); + } + return newXAcc; + }; + + + // Compute y acceleration from forces, F=ma + getNewAccelerationY = (forceList: IForce[]) => { + let newYAcc = 0; + if (forceList) { + forceList.forEach((force) => { + newYAcc += + (-1 * + (force.magnitude * + Math.sin((force.directionInDegrees * Math.PI) / 180))) / + this.props.mass; + }); + } + return newYAcc; + }; + + // Compute uniform circular motion forces given x, y positions + getNewCircularMotionForces = (xPos: number, yPos: number) => { + let deltaX = (this.props.xMin + this.props.xMax) / 2 - (xPos + this.props.radius); + let deltaY = yPos + this.props.radius - (this.props.yMin + this.props.yMax) / 2; + let dir = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; + const tensionForce: IForce = { + description: "Centripetal Force", + magnitude: (this.props.startVelX ** 2 * this.props.mass) / this.props.circularMotionRadius, + directionInDegrees: dir, + component: false, + }; + return [tensionForce]; + }; + + // Compute spring forces given y position + getNewSpringForces = (yPos: number) => { + let springForce: IForce = { + description: "Spring Force", + magnitude: 0, + directionInDegrees: 90, + component: false, + }; + if (yPos - this.props.springRestLength > 0) { + springForce = { + description: "Spring Force", + magnitude: this.props.springConstant * (yPos - this.props.springRestLength), + directionInDegrees: 90, + component: false, + }; + } else if (yPos - this.props.springRestLength < 0) { + springForce = { + description: "Spring Force", + magnitude: this.props.springConstant * (this.props.springRestLength - yPos), + directionInDegrees: 270, + component: false, + }; + } + return [ + { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: false, + }, + springForce, + ]; + }; + + // Compute pendulum forces given position, velocity + getNewPendulumForces = ( + xPos: number, + yPos: number, + xVel: number, + yVel: number + ) => { + const x = this.props.xMax / 2 - xPos - this.props.radius; + const y = yPos + this.props.radius + 5; + let angle = (Math.atan(y / x) * 180) / Math.PI; + if (angle < 0) { + angle += 180; + } + let oppositeAngle = 90 - angle; + if (oppositeAngle < 0) { + oppositeAngle = 90 - (180 - angle); + } + + const pendulumLength = Math.sqrt(x * x + y * y); + this.props.dataDoc['pendulumAngle'] = oppositeAngle; + + const mag = + this.props.mass * Math.abs(this.props.gravity) * Math.cos((oppositeAngle * Math.PI) / 180) + + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; + + const forceOfTension: IForce = { + description: "Tension", + magnitude: mag, + directionInDegrees: angle, + component: false, + }; + + return [ + { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: false, + }, + forceOfTension, + ]; + }; + + // Check for collisions in x direction + checkForCollisionsWithWall = () => { + let collision = false; + const minX = this.state.xPosition; + const maxX = this.state.xPosition + 2 * this.props.radius; + if (this.state.xVelocity != 0) { + this.state.walls.forEach((wall) => { + if (wall.angleInDegrees == 90) { + const wallX = (wall.xPos / 100) * this.props.layoutDoc._width; + if (wall.xPos < 0.35) { + if (minX <= wallX) { + this.setState({xPosition: wallX+0.01}); + if (this.props.elasticCollisions) { + this.setState({xVelocity: -this.state.xVelocity}); + } else { + this.setState({xVelocity: 0}); + } + collision = true; + } + } else { + if (maxX >= wallX) { + this.setState({xPosition: wallX- 2 * this.props.radius-0.01}); + if (this.props.elasticCollisions) { + this.setState({xVelocity: -this.state.xVelocity}); + } else { + this.setState({xVelocity: 0}); + } + collision = true; + } + } + } + }); + } + return collision; + }; + + // Check for collisions in y direction + checkForCollisionsWithGround = () => { + let collision = false; + const minY = this.state.yPosition; + const maxY = this.state.yPosition + 2 * this.props.radius; + if (this.state.yVelocity > 0) { + this.state.walls.forEach((wall) => { + if (wall.angleInDegrees == 0 && wall.yPos > 0.4) { + const groundY = (wall.yPos / 100) * this.props.layoutDoc._height; + if (maxY > groundY) { + this.setState({yPosition: groundY- 2 * this.props.radius-0.01}); + if (this.props.elasticCollisions) { + this.setState({yVelocity: -this.state.yVelocity}); + } else { + this.setState({yVelocity: 0}); + if (this.props.dataDoc['simulationType'] != "Two Weights") { + const forceOfGravity: IForce = { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: false, + }; + const normalForce: IForce = { + description: "Normal force", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: wall.angleInDegrees + 90, + component: false, + }; + this.props.dataDoc['updatedForces'] = ([forceOfGravity, normalForce]); + if (this.props.dataDoc['simulationType'] == "Inclined Plane") { + const forceOfGravityC: IForce = { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: true, + }; + const normalForceC: IForce = { + description: "Normal force", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: wall.angleInDegrees + 90, + component: true, + }; + this.props.dataDoc['componentForces'] = ([forceOfGravityC, normalForceC]); + } + } + } + collision = true; + } + } + }); + } + if (this.state.yVelocity < 0) { + this.state.walls.forEach((wall) => { + if (wall.angleInDegrees == 0 && wall.yPos < 0.4) { + const groundY = (wall.yPos / 100) * this.props.layoutDoc._height; + if (minY < groundY) { + this.setState({yPosition: groundY + 0.01}); + if (this.props.elasticCollisions) { + this.setState({yVelocity: -this.state.yVelocity}); + } else { + this.setState({yVelocity: 0}); + } + collision = true; + } + } + }); + } + return collision; + }; + + // Called at each RK4 step + evaluate = ( + currentXPos: number, + currentYPos: number, + currentXVel: number, + currentYVel: number, + deltaXPos: number, + deltaYPos: number, + deltaXVel: number, + deltaYVel: number, + dt: number + ) => { + const newXPos = currentXPos + deltaXPos * dt; + const newYPos = currentYPos + deltaYPos * dt; + const newXVel = currentXVel + deltaXVel * dt; + const newYVel = currentYVel + deltaYVel * dt; + const newDeltaXPos = newXVel; + const newDeltaYPos = newYVel; + let forces = this.props.updatedForces; + if (this.props.dataDoc['simulationType'] == "Pendulum") { + forces = this.getNewPendulumForces(newXPos, newYPos, newXVel, newYVel); + } else if (this.props.dataDoc['simulationType'] == "Spring") { + forces = this.getNewSpringForces(newYPos); + } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { + forces = this.getNewCircularMotionForces(newXPos, newYPos); + } + const newDeltaXVel = this.getNewAccelerationX(forces); + const newDeltaYVel = this.getNewAccelerationY(forces); + return { + xPos: newXPos, + yPos: newYPos, + xVel: newXVel, + yVel: newYVel, + deltaXPos: newDeltaXPos, + deltaYPos: newDeltaYPos, + deltaXVel: newDeltaXVel, + deltaYVel: newDeltaYVel, + }; + }; + + // Update position, velocity using RK4 method + update = () => { + let startXVel = this.state.xVelocity; + let startYVel = this.state.yVelocity; + let xPos = this.state.xPosition; + let yPos = this.state.yPosition; + let xVel = this.state.xVelocity; + let yVel = this.state.yVelocity; + let forces: IForce[] = this.props.dataDoc['updatedForces']; + if (this.props.dataDoc['simulationType'] == "Pendulum") { + forces = this.getNewPendulumForces(xPos, yPos, xVel, yVel); + } else if (this.props.dataDoc['simulationType'] == "Spring") { + forces = this.getNewSpringForces(yPos); + } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { + forces = this.getNewCircularMotionForces(xPos, yPos); + } + const xAcc = this.getNewAccelerationX(forces); + const yAcc = this.getNewAccelerationY(forces); + for (let i = 0; i < this.props.simulationSpeed; i++) { + const k1 = this.evaluate(xPos, yPos, xVel, yVel, xVel, yVel, xAcc, yAcc, 0); + const k2 = this.evaluate( + xPos, + yPos, + xVel, + yVel, + k1.deltaXPos, + k1.deltaYPos, + k1.deltaXVel, + k1.deltaYVel, + this.props.timestepSize * 0.5 + ); + const k3 = this.evaluate( + xPos, + yPos, + xVel, + yVel, + k2.deltaXPos, + k2.deltaYPos, + k2.deltaXVel, + k2.deltaYVel, + this.props.timestepSize * 0.5 + ); + const k4 = this.evaluate( + xPos, + yPos, + xVel, + yVel, + k3.deltaXPos, + k3.deltaYPos, + k3.deltaXVel, + k3.deltaYVel, + this.props.timestepSize + ); + + xVel += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel); + yVel += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel); + xPos += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos); + yPos += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaYPos + 2 * (k2.deltaYPos + k3.deltaYPos) + k4.deltaYPos); + } + // make sure harmonic motion maintained and errors don't propagate + if (this.props.dataDoc['simulationType'] == "Spring") { + if (startYVel < 0 && yVel > 0 && yPos < this.props.springRestLength) { + let equilibriumPos = + this.props.springRestLength + (this.props.mass * Math.abs(this.props.gravity)) / this.props.springConstant; + let amplitude = Math.abs(equilibriumPos - this.props.springStartLength); + yPos = equilibriumPos - amplitude; + } else if (startYVel > 0 && yVel < 0 && yPos > this.props.springRestLength) { + let equilibriumPos = + this.props.springRestLength + (this.props.mass * Math.abs(this.props.gravity)) / this.props.springConstant; + let amplitude = Math.abs(equilibriumPos - this.props.springStartLength); + yPos = equilibriumPos + amplitude; + } + } + if (this.props.dataDoc['simulationType'] == "Pendulum") { + let startX = this.state.updatedStartPosX; + if (startXVel <= 0 && xVel > 0) { + xPos = this.state.updatedStartPosX; + if (this.state.updatedStartPosX > this.props.xMax / 2) { + xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; + } + yPos = this.props.startPosY; + } else if (startXVel >= 0 && xVel < 0) { + xPos = this.state.updatedStartPosX; + if (this.state.updatedStartPosX < this.props.xMax / 2) { + xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; + } + yPos = this.props.startPosY; + } + } + if (this.props.dataDoc['simulationType'] == "One Weight") { + if (yPos < this.state.maxPosYConservation) { + yPos = this.state.maxPosYConservation; + } + } + this.setState({xVelocity: xVel}); + this.setState({yVelocity: yVel}); + this.setState({xPosition: xPos}); + this.setState({yPosition: yPos}); + let forcesn = this.props.dataDoc['updatedForces'] + if (this.props.dataDoc['simulationType'] == "Pendulum") { + forcesn = this.getNewPendulumForces(xPos, yPos, xVel, yVel); + } else if (this.props.dataDoc['simulationType'] == "Spring") { + forcesn = this.getNewSpringForces(yPos); + } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { + forcesn = this.getNewCircularMotionForces(xPos, yPos); + } + this.props.dataDoc['updatedForces'] = (forcesn); + + // set component forces if they change + if (this.props.dataDoc['simulationType'] == "Pendulum") { + let x = this.props.xMax / 2 - xPos - this.props.radius; + let y = yPos + this.props.radius + 5; + let angle = (Math.atan(y / x) * 180) / Math.PI; + if (angle < 0) { + angle += 180; + } + let oppositeAngle = 90 - angle; + if (oppositeAngle < 0) { + oppositeAngle = 90 - (180 - angle); + } + + const pendulumLength = Math.sqrt(x * x + y * y); + + const mag = + this.props.mass * Math.abs(this.props.gravity) * Math.cos((oppositeAngle * Math.PI) / 180) + + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; + + const tensionComponent: IForce = { + description: "Tension", + magnitude: mag, + directionInDegrees: angle, + component: true, + }; + const gravityParallel: IForce = { + description: "Gravity Parallel Component", + magnitude: Math.abs(this.props.gravity) * Math.cos(((90 - angle) * Math.PI) / 180), + directionInDegrees: 270 - (90 - angle), + component: true, + }; + const gravityPerpendicular: IForce = { + description: "Gravity Perpendicular Component", + magnitude: Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180), + directionInDegrees: -(90 - angle), + component: true, + }; + if (Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180) < 0) { + gravityPerpendicular.magnitude = Math.abs( + Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180) + ); + gravityPerpendicular.directionInDegrees = 180 - (90 - angle); + } + this.props.dataDoc['componentForces'] = ([ + tensionComponent, + gravityParallel, + gravityPerpendicular, + ]); + } + }; + + // Render weight, spring, rod(s), vectors + render () { + return ( + <div> + <div + className="weightContainer" + onPointerDown={(e) => { + if (this.draggable) { + this.props.dataDoc['paused'] = true; + this.setState({dragging: true}); + this.setState({clickPositionX: e.clientX}); + this.setState({clickPositionY: e.clientY}); + } + }} + onPointerMove={(e) => { + if (this.state.dragging) { + let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; + if (newY > this.props.yMax - 2 * this.props.radius - 10) { + newY = this.props.yMax - 2 * this.props.radius - 10; + } else if (newY < 10) { + newY = 10; + } + + let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; + if (newX > this.props.xMax - 2 * this.props.radius - 10) { + newX = this.props.xMax - 2 * this.props.radius - 10; + } else if (newX < 10) { + newX = 10; + } + if (this.props.dataDoc['simulationType'] == "Suspension") { + if (newX < (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15) { + newX = (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15; + } else if (newX > (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15) { + newX = (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15; + } + } + + this.setState({yPosition: newY}); + this.props.dataDoc['positionYDisplay'] = + Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100 + if (this.props.dataDoc['simulationType'] != "Pulley") { + this.setState({xPosition: newX}); + this.props.dataDoc['positionXDisplay'] = newX + } + if (this.props.dataDoc['simulationType'] != "Suspension") { + if (this.props.dataDoc['simulationType'] != "Pulley") { + this.setState({updatedStartPosX: newX}); + } + this.setState({updatedStartPosY: newY}); + } + this.setState({clickPositionX: e.clientX}); + this.setState({clickPositionY: e.clientY}); + this.setDisplayValues(); + } + }} + onPointerUp={(e) => { + if (this.state.dragging) { + if ( + this.props.dataDoc['simulationType'] != "Pendulum" && + this.props.dataDoc['simulationType'] != "Suspension" + ) { + this.resetEverything(); + } + this.setState({dragging: false}); + let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; + if (newY > this.props.yMax - 2 * this.props.radius - 10) { + newY = this.props.yMax - 2 * this.props.radius - 10; + } else if (newY < 10) { + newY = 10; + } + + let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; + if (newX > this.props.xMax - 2 * this.props.radius - 10) { + newX = this.props.xMax - 2 * this.props.radius - 10; + } else if (newX < 10) { + newX = 10; + } + if (this.props.dataDoc['simulationType'] == "Spring") { + this.props.dataDoc.springStartLength = newY + } + if (this.props.dataDoc['simulationType'] == "Suspension") { + let x1rod = (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200; + let x2rod = (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius; + let deltaX1 = this.state.xPosition + this.props.radius - x1rod; + let deltaX2 = x2rod - (this.state.xPosition + this.props.radius); + let deltaY = this.state.yPosition + this.props.radius; + let dir1T = Math.PI - Math.atan(deltaY / deltaX1); + let dir2T = Math.atan(deltaY / deltaX2); + let tensionMag2 = + (this.props.mass * Math.abs(this.props.gravity)) / + ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + + Math.sin(dir2T)); + let tensionMag1 = + (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + dir1T = (dir1T * 180) / Math.PI; + dir2T = (dir2T * 180) / Math.PI; + const tensionForce1: IForce = { + description: "Tension", + magnitude: tensionMag1, + directionInDegrees: dir1T, + component: false, + }; + const tensionForce2: IForce = { + description: "Tension", + magnitude: tensionMag2, + directionInDegrees: dir2T, + component: false, + }; + const grav: IForce = { + description: "Gravity", + magnitude: this.props.mass * Math.abs(this.props.gravity), + directionInDegrees: 270, + component: false, + }; + this.props.dataDoc['updatedForces'] = ([tensionForce1, tensionForce2, grav]); + } + } + }} + > + <div className="weight" style={this.weightStyle}> + <p className="weightLabel">{this.props.mass} kg</p> + </div> + </div> + {this.props.dataDoc['simulationType'] == "Spring" && ( + <div + className="spring" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((val) => { + const count = 10; + let xPos1; + let yPos1; + let xPos2; + let yPos2; + if (val % 2 == 0) { + xPos1 = this.state.xPosition + this.props.radius - 20; + xPos2 = this.state.xPosition + this.props.radius + 20; + } else { + xPos1 = this.state.xPosition + this.props.radius + 20; + xPos2 = this.state.xPosition + this.props.radius - 20; + } + yPos1 = (val * this.state.yPosition) / count; + yPos2 = ((val + 1) * this.state.yPosition) / count; + return ( + <line + key={val} + x1={xPos1} + y1={yPos1} + x2={xPos2} + y2={yPos2} + stroke={"#808080"} + strokeWidth="10" + /> + ); + })} + </svg> + </div> + )} + + {this.props.dataDoc['simulationType'] == "Pulley" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.state.xPosition + this.props.radius} + y2={this.props.yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Pulley" && ( + <div + className="wheel" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <circle + cx={(this.props.xMax + this.props.xMin) / 2} + cy={this.props.radius} + r={this.props.radius * 1.5} + fill={"#808080"} + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Suspension" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200} + y2={this.props.yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + <p + style={{ + position: "absolute", + left: (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200 + 80 + "px", + top: 10 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round( + ((Math.atan( + (this.state.yPosition + this.props.radius) / + (this.state.xPosition + + this.props.radius - + ((this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200)) + ) * + 180) / + Math.PI) * + 100 + ) / 100} + ° + </p> + <svg width={this.props.layoutDoc._width + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius} + y2={this.props.yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + + <p + style={{ + position: "absolute", + left: (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius - 80 + "px", + top: 10 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round( + ((Math.atan( + (this.state.yPosition + this.props.radius) / + ((this.props.xMax + this.props.xMin) / 2 + + this.props.yMin + + 200 + + this.props.radius - + (this.state.xPosition + this.props.radius)) + ) * + 180) / + Math.PI) * + 100 + ) / 100} + ° + </p> + </div> + )} + {this.props.dataDoc['simulationType'] == "Circular Motion" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMin + this.props.xMax) / 2} + y2={(this.props.yMin + this.props.yMax) / 2} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Pendulum" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.props.xMax / 2} + y2={-5} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + {!this.state.dragging && ( + <div> + <p + style={{ + position: "absolute", + zIndex: 5, + left: this.state.xPosition + "px", + top: this.state.yPosition - 70 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round(this.props.pendulumLength)} m + </p> + <p + style={{ + position: "absolute", + left: this.props.xMax / 2 + "px", + top: 30 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round(this.props.pendulumAngle * 100) / 100}° + </p> + </div> + )} + </div> + )} + {this.props.dataDoc['simulationType'] == "Inclined Plane" && ( + <div> + <div + style={{ position: "absolute", left: "0", top: "0" }} + > + <svg width={this.props.xMax + "px"} height={this.props.yMax + "px"}> + <polygon points={this.state.coordinates} style={{ fill: "burlywood" }} /> + </svg> + </div> + + <p + style={{ + position: "absolute", + left: Math.round(this.props.xMax * 0.5 - 200 + this.props.wedgeWidth - 80) + "px", + top: Math.round(this.props.yMax - 40) + "px", + }} + > + {Math.round( + ((Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI) * 100 + ) / 100} + ° + </p> + </div> + )} + {!this.state.dragging && this.props.showAcceleration && ( + <div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <defs> + <marker + id="accArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill="green" /> + </marker> + </defs> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.state.xPosition + this.props.radius + this.getNewAccelerationX(this.props.updatedForces) * 15} + y2={this.state.yPosition + this.props.radius + this.getNewAccelerationY(this.props.updatedForces) * 15} + stroke={"green"} + strokeWidth="5" + markerEnd="url(#accArrow)" + /> + </svg> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.state.xPosition + this.props.radius + this.state.xAccel * 15 + 25 + "px", + top: this.state.yPosition + this.props.radius + this.state.yAccel * 15 + 70 + "px", + lineHeight: 1, + }} + > + <p> + {Math.round( + 100 * Math.sqrt(this.state.xAccel * this.state.xAccel + this.state.yAccel * this.state.yAccel) + ) / 100}{" "} + m/s + <sup>2</sup> + </p> + </div> + </div> + </div> + )} + {!this.state.dragging && this.props.showVelocity && ( + <div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <defs> + <marker + id="velArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill="blue" /> + </marker> + </defs> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.state.xPosition + this.props.radius + this.state.xVelocity * 7} + y2={this.state.yPosition + this.props.radius + this.state.yVelocity * 7} + stroke={"blue"} + strokeWidth="5" + markerEnd="url(#velArrow)" + /> + </svg> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.state.xPosition + this.props.radius + this.state.xVelocity * 7 + 25 + "px", + top: this.state.yPosition + this.props.radius + this.state.yVelocity * 7 + "px", + lineHeight: 1, + }} + > + <p> + {Math.round( + 100 * + Math.sqrt( + this.props.displayXVelocity * this.props.displayXVelocity + + this.props.displayYVelocity * this.props.displayYVelocity + ) + ) / 100}{" "} + m/s + </p> + </div> + </div> + </div> + )} + {!this.state.dragging && + this.props.showComponentForces && + this.props.componentForces.map((force, index) => { + if (force.magnitude < this.epsilon) { + return; + } + let arrowStartY: number = this.state.yPosition + this.props.radius; + const arrowStartX: number = this.state.xPosition + this.props.radius; + let arrowEndY: number = + arrowStartY - + Math.abs(force.magnitude) * + 20 * + Math.sin((force.directionInDegrees * Math.PI) / 180); + const arrowEndX: number = + arrowStartX + + Math.abs(force.magnitude) * + 20 * + Math.cos((force.directionInDegrees * Math.PI) / 180); + + let color = "#0d0d0d"; + + let labelTop = arrowEndY; + let labelLeft = arrowEndX; + if (force.directionInDegrees > 90 && force.directionInDegrees < 270) { + labelLeft -= 120; + } else { + labelLeft += 30; + } + if (force.directionInDegrees >= 0 && force.directionInDegrees < 180) { + labelTop += 40; + } else { + labelTop -= 40; + } + labelTop = Math.min(labelTop, this.props.yMax + 50); + labelTop = Math.max(labelTop, this.props.yMin); + labelLeft = Math.min(labelLeft, this.props.xMax - 60); + labelLeft = Math.max(labelLeft, this.props.xMin); + + return ( + <div key={index}> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.props.xMin, + top: this.props.yMin, + }} + > + <svg + width={this.props.xMax - this.props.xMin + "px"} + height={this.props.layoutDoc._height + "px"} + > + <defs> + <marker + id="forceArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill={color} /> + </marker> + </defs> + {force.component == true && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + strokeDasharray="10,10" + markerEnd="url(#forceArrow)" + /> + )} + {force.component == false && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + markerEnd="url(#forceArrow)" + /> + )} + </svg> + </div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: labelLeft + "px", + top: labelTop + "px", + lineHeight: 1, + backgroundColor: this.labelBackgroundColor, + }} + > + {force.description && <p>{force.description}</p>} + {!force.description && <p>Force</p>} + {this.props.showForceMagnitudes && ( + <p>{Math.round(100 * force.magnitude) / 100} N</p> + )} + </div> + </div> + ); + })} + {!this.state.dragging && + this.props.showForces && this.props.updatedForces && + this.props.updatedForces.map((force, index) => { + if (force.magnitude < this.epsilon) { + return; + } + let arrowStartY: number = this.state.yPosition + this.props.radius; + const arrowStartX: number = this.state.xPosition + this.props.radius; + let arrowEndY: number = + arrowStartY - + Math.abs(force.magnitude) * + 20 * + Math.sin((force.directionInDegrees * Math.PI) / 180); + const arrowEndX: number = + arrowStartX + + Math.abs(force.magnitude) * + 20 * + Math.cos((force.directionInDegrees * Math.PI) / 180); + + let color = "#0d0d0d"; + + let labelTop = arrowEndY; + let labelLeft = arrowEndX; + if (force.directionInDegrees > 90 && force.directionInDegrees < 270) { + labelLeft -= 120; + } else { + labelLeft += 30; + } + if (force.directionInDegrees >= 0 && force.directionInDegrees < 180) { + labelTop += 40; + } else { + labelTop -= 40; + } + labelTop = Math.min(labelTop, this.props.yMax + 50); + labelTop = Math.max(labelTop, this.props.yMin); + labelLeft = Math.min(labelLeft, this.props.xMax - 60); + labelLeft = Math.max(labelLeft, this.props.xMin); + + return ( + <div key={index}> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.props.xMin, + top: this.props.yMin, + }} + > + <svg + width={this.props.xMax - this.props.xMin + "px"} + height={this.props.layoutDoc._height + "px"} + > + <defs> + <marker + id="forceArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill={color} /> + </marker> + </defs> + {force.component == true && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + strokeDasharray="10,10" + markerEnd="url(#forceArrow)" + /> + )} + {force.component == false && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + markerEnd="url(#forceArrow)" + /> + )} + </svg> + </div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: labelLeft + "px", + top: labelTop + "px", + lineHeight: 1, + backgroundColor: this.labelBackgroundColor, + }} + > + {force.description && <p>{force.description}</p>} + {!force.description && <p>Force</p>} + {this.props.showForceMagnitudes && ( + <p>{Math.round(100 * force.magnitude) / 100} N</p> + )} + </div> + </div> + ); + })} + </div> + )} +}; |