aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/nodes/PhysicsSimulationApp.tsx2154
-rw-r--r--src/client/views/nodes/PhysicsSimulationBox.scss72
-rw-r--r--src/client/views/nodes/PhysicsSimulationBox.tsx53
-rw-r--r--src/client/views/nodes/PhysicsSimulationWall.tsx35
-rw-r--r--src/client/views/nodes/PhysicsSimulationWedge.tsx64
-rw-r--r--src/client/views/nodes/PhysicsSimulationWeight.tsx913
6 files changed, 3238 insertions, 53 deletions
diff --git a/src/client/views/nodes/PhysicsSimulationApp.tsx b/src/client/views/nodes/PhysicsSimulationApp.tsx
new file mode 100644
index 000000000..7486aa88d
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationApp.tsx
@@ -0,0 +1,2154 @@
+import AddIcon from "@mui/icons-material/Add";
+import AddCircleIcon from "@mui/icons-material/AddCircle";
+import ClearIcon from "@mui/icons-material/Clear";
+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 { SelectChangeEvent } from "@mui/material/Select";
+import {
+ Alert,
+ Box,
+ Button,
+ Checkbox,
+ CircularProgress,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
+ Divider,
+ FormControl,
+ FormControlLabel,
+ FormGroup,
+ IconButton,
+ InputAdornment,
+ InputLabel,
+ LinearProgress,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ MenuItem,
+ Popover,
+ Select,
+ Stack,
+ TextField,
+ Tooltip,
+} from "@mui/material";
+import { styled } from "@mui/material/styles";
+import { tooltipClasses, TooltipProps } from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
+import React, { useEffect, useState } from "react";
+import "./PhysicsSimulationBox.scss";
+import { IForce, Weight } from "./PhysicsSimulationWeight";
+import { Description } from "@mui/icons-material";
+
+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;
+ }[];
+ showMagnitude: boolean;
+ }[];
+}
+
+function App() {
+ // Constants
+ const gravityMagnitude = 9.81;
+ const forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: gravityMagnitude,
+ directionInDegrees: 270,
+ };
+ const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
+ <Tooltip {...props} classes={{ popper: className }} />
+ ))(({ theme }) => ({
+ [`& .${tooltipClasses.tooltip}`]: {
+ backgroundColor: "#f5f5f9",
+ color: "rgba(0, 0, 0, 0.87)",
+ maxWidth: 220,
+ fontSize: theme.typography.pxToRem(12),
+ border: "1px solid #dadde9",
+ },
+ }));
+ const xMin = 0;
+ const yMin = 0;
+ const xMax = window.innerWidth * 0.7;
+ const yMax = window.innerHeight * 0.8;
+ const color = `rgba(0,0,0,0.5)`;
+
+ // Variables
+ let questionVariables: number[] = [];
+ let reviewCoefficient: number = 0;
+
+ // State variables
+ const [startPosX, setStartPosX] = useState(0);
+ const [startPosY, setStartPosY] = useState(0);
+ const [accelerationXDisplay, setAccelerationXDisplay] = useState(0);
+ const [accelerationYDisplay, setAccelerationYDisplay] = useState(0);
+ const [positionXDisplay, setPositionXDisplay] = useState(0);
+ const [positionYDisplay, setPositionYDisplay] = useState(0);
+ const [velocityXDisplay, setVelocityXDisplay] = useState(0);
+ const [velocityYDisplay, setVelocityYDisplay] = useState(0);
+
+ const [startPosX2, setStartPosX2] = useState(0);
+ const [startPosY2, setStartPosY2] = useState(0);
+ const [accelerationXDisplay2, setAccelerationXDisplay2] = useState(0);
+ const [accelerationYDisplay2, setAccelerationYDisplay2] = useState(0);
+ const [positionXDisplay2, setPositionXDisplay2] = useState(0);
+ const [positionYDisplay2, setPositionYDisplay2] = useState(0);
+ const [velocityXDisplay2, setVelocityXDisplay2] = useState(0);
+ const [velocityYDisplay2, setVelocityYDisplay2] = useState(0);
+
+ const [adjustPendulumAngle, setAdjustPendulumAngle] = useState<{
+ angle: number;
+ length: number;
+ }>({ angle: 0, length: 0 });
+ const [answerInputFields, setAnswerInputFields] = useState(<div></div>);
+ const [coefficientOfKineticFriction, setCoefficientOfKineticFriction] =
+ React.useState<number | string | Array<number | string>>(0);
+ const [coefficientOfStaticFriction, setCoefficientOfStaticFriction] =
+ React.useState<number | string | Array<number | string>>(0);
+ const [currentForceSketch, setCurrentForceSketch] =
+ useState<VectorTemplate | null>(null);
+ const [deleteMode, setDeleteMode] = useState(false);
+ const [displayChange, setDisplayChange] = useState<{
+ xDisplay: number;
+ yDisplay: number;
+ }>({ xDisplay: 0, yDisplay: 0 });
+ const [elasticCollisions, setElasticCollisions] = useState<boolean>(false);
+ const [forceSketches, setForceSketches] = useState<VectorTemplate[]>([]);
+ const [questionPartOne, setQuestionPartOne] = useState<string>("");
+ const [hintDialogueOpen, setHintDialogueOpen] = useState<boolean>(false);
+ const [mode, setMode] = useState<string>("Freeform");
+ const [noMovement, setNoMovement] = useState(false);
+ const [weight, setWeight] = useState(false);
+ const [pendulum, setPendulum] = useState(false);
+ const [pendulumAngle, setPendulumAngle] = useState(0);
+ const [pendulumLength, setPendulumLength] = useState(300);
+ const [questionNumber, setQuestionNumber] = useState<number>(0);
+ const [reviewGravityAngle, setReviewGravityAngle] = useState<number>(0);
+ const [reviewGravityMagnitude, setReviewGravityMagnitude] =
+ useState<number>(0);
+ const [reviewNormalAngle, setReviewNormalAngle] = useState<number>(0);
+ const [reviewNormalMagnitude, setReviewNormalMagnitude] = useState<number>(0);
+ const [reviewStaticAngle, setReviewStaticAngle] = useState<number>(0);
+ const [reviewStaticMagnitude, setReviewStaticMagnitude] = useState<number>(0);
+ const [selectedQuestion, setSelectedQuestion] = useState<QuestionTemplate>(
+ questions.inclinePlane[0]
+ );
+ const [selectedTutorial, setSelectedTutorial] = useState<TutorialTemplate>(
+ tutorials.inclinePlane
+ );
+ const [questionPartTwo, setQuestionPartTwo] = useState<string>("");
+ const [selectedSolutions, setSelectedSolutions] = useState<number[]>([]);
+ const [showAcceleration, setShowAcceleration] = useState<boolean>(false);
+ const [showForces, setShowForces] = useState<boolean>(true);
+ const [showVelocity, setShowVelocity] = useState<boolean>(false);
+ const [simulationPaused, setSimulationPaused] = useState<boolean>(true);
+ const [simulationReset, setSimulationReset] = useState<boolean>(false);
+ const [simulationType, setSimulationType] =
+ useState<string>("Inclined Plane");
+ const [sketching, setSketching] = useState(false);
+ const [startForces, setStartForces] = useState<IForce[]>([forceOfGravity]);
+ const [startPendulumAngle, setStartPendulumAngle] = useState(0);
+ const [stepNumber, setStepNumber] = useState<number>(0);
+ const [timer, setTimer] = useState<number>(0);
+ const [updatedForces, setUpdatedForces] = useState<IForce[]>([
+ forceOfGravity,
+ ]);
+ const [wallPositions, setWallPositions] = useState<IWallProps[]>([]);
+ const [wedge, setWedge] = useState(false);
+ const [wedgeAngle, setWedgeAngle] = React.useState<
+ number | string | Array<number | string>
+ >(26);
+ const [wedgeHeight, setWedgeHeight] = useState(
+ Math.tan((26 * Math.PI) / 180) * 400
+ );
+ const [wedgeWidth, setWedgeWidth] = useState(400);
+ const [twoWeights, setTwoWeights] = useState(false);
+
+ // Add one weight to the simulation
+ const addWeight = () => {
+ setWeight(true);
+ setTwoWeights(false);
+ setWedge(false);
+ setPendulum(false);
+ };
+
+ // Add two weights to the simulation
+ const addTwoWeights = () => {
+ setWeight(true);
+ setTwoWeights(true);
+ setWedge(false);
+ setPendulum(false);
+ };
+
+ // Add a wedge with a One Weight to the simulation
+ const addWedge = () => {
+ setWeight(true);
+ setTwoWeights(false);
+ setWedge(true);
+ setPendulum(false);
+ };
+
+ // Add a simple pendulum to the simulation
+ const addPendulum = () => {
+ setWeight(true);
+ setTwoWeights(false);
+ setPendulum(true);
+ setWedge(false);
+ };
+
+ // Update forces when coefficient of static friction changes in freeform mode
+ const updateForcesWithFriction = (
+ coefficient: number,
+ width: number = wedgeWidth,
+ height: number = wedgeHeight
+ ) => {
+ const normalForce: IForce = {
+ description: "Normal Force",
+ magnitude: forceOfGravity.magnitude * Math.cos(Math.atan(height / width)),
+ directionInDegrees:
+ 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ let frictionForce: IForce = {
+ description: "Static Friction Force",
+ magnitude:
+ coefficient *
+ forceOfGravity.magnitude *
+ Math.cos(Math.atan(height / width)),
+ directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ // reduce magnitude of friction force if necessary such that block cannot slide up plane
+ let yForce = -forceOfGravity.magnitude;
+ yForce +=
+ normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+ yForce +=
+ frictionForce.magnitude *
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude =
+ (-normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
+ forceOfGravity.magnitude) /
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+ if (coefficient != 0) {
+ setStartForces([forceOfGravity, normalForce, frictionForce]);
+ setUpdatedForces([forceOfGravity, normalForce, frictionForce]);
+ } else {
+ setStartForces([forceOfGravity, normalForce]);
+ setUpdatedForces([forceOfGravity, normalForce]);
+ }
+ };
+
+ // Change wedge height and width and weight position to match new wedge angle
+ const changeWedgeBasedOnNewAngle = (angle: number) => {
+ let width = 0;
+ let height = 0;
+ if (angle < 50) {
+ width = 400;
+ height = Math.tan((angle * Math.PI) / 180) * 400;
+ setWedgeWidth(width);
+ setWedgeHeight(height);
+ } else if (angle < 70) {
+ width = 200;
+ height = Math.tan((angle * Math.PI) / 180) * 200;
+ setWedgeWidth(width);
+ setWedgeHeight(height);
+ } else {
+ width = 100;
+ height = Math.tan((angle * Math.PI) / 180) * 100;
+ setWedgeWidth(width);
+ setWedgeHeight(height);
+ }
+
+ // update weight position based on updated wedge width/height
+ let yPos = (width - 50) * Math.tan((angle * Math.PI) / 180);
+ if (angle < 40) {
+ yPos += Math.sqrt(angle);
+ } else if (angle < 58) {
+ yPos += angle / 2;
+ } else if (angle < 68) {
+ yPos += angle;
+ } else if (angle < 70) {
+ yPos += angle * 1.3;
+ } else if (angle < 75) {
+ yPos += angle * 1.5;
+ } else if (angle < 78) {
+ yPos += angle * 2;
+ } else if (angle < 79) {
+ yPos += angle * 2.25;
+ } else if (angle < 80) {
+ yPos += angle * 2.6;
+ } else {
+ yPos += angle * 3;
+ }
+
+ setStartPosX(Math.round((xMax * 0.5 - 200) * 10) / 10);
+ setStartPosY(getDisplayYPos(yPos));
+ if (mode == "Freeform") {
+ updateForcesWithFriction(
+ Number(coefficientOfStaticFriction),
+ width,
+ height
+ );
+ }
+ };
+
+ // Helper function to go between display and real values
+ const getDisplayYPos = (yPos: number) => {
+ return yMax - yPos - 2 * 50 + 5;
+ };
+
+ // In review mode, update forces when coefficient of static friction changed
+ const updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
+ let theta: number = Number(wedgeAngle);
+ let index =
+ selectedQuestion.variablesForQuestionSetup.indexOf("theta - max 45");
+ if (index >= 0) {
+ theta = questionVariables[index];
+ }
+ if (isNaN(theta)) {
+ return;
+ }
+ setReviewGravityMagnitude(forceOfGravity.magnitude);
+ setReviewGravityAngle(270);
+ setReviewNormalMagnitude(
+ forceOfGravity.magnitude * Math.cos((theta * Math.PI) / 180)
+ );
+ setReviewNormalAngle(90 - theta);
+ let yForce = -forceOfGravity.magnitude;
+ yForce +=
+ 9.81 *
+ Math.cos((theta * Math.PI) / 180) *
+ Math.sin(((90 - theta) * Math.PI) / 180);
+ yForce +=
+ coefficient *
+ 9.81 *
+ Math.cos((theta * Math.PI) / 180) *
+ Math.sin(((180 - theta) * Math.PI) / 180);
+ let friction = coefficient * 9.81 * Math.cos((theta * Math.PI) / 180);
+ if (yForce > 0) {
+ friction =
+ (-(forceOfGravity.magnitude * Math.cos((theta * Math.PI) / 180)) *
+ Math.sin(((90 - theta) * Math.PI) / 180) +
+ forceOfGravity.magnitude) /
+ Math.sin(((180 - theta) * Math.PI) / 180);
+ }
+ setReviewStaticMagnitude(friction);
+ setReviewStaticAngle(180 - theta);
+ };
+
+ // In review mode, update forces when wedge angle changed
+ const updateReviewForcesBasedOnAngle = (angle: number) => {
+ setReviewGravityMagnitude(9.81);
+ setReviewGravityAngle(270);
+ setReviewNormalMagnitude(9.81 * Math.cos((Number(angle) * Math.PI) / 180));
+ setReviewNormalAngle(90 - angle);
+ let yForce = -forceOfGravity.magnitude;
+ yForce +=
+ 9.81 *
+ Math.cos((Number(angle) * Math.PI) / 180) *
+ Math.sin(((90 - Number(angle)) * Math.PI) / 180);
+ yForce +=
+ reviewCoefficient *
+ 9.81 *
+ Math.cos((Number(angle) * Math.PI) / 180) *
+ Math.sin(((180 - Number(angle)) * Math.PI) / 180);
+ let friction =
+ reviewCoefficient * 9.81 * Math.cos((Number(angle) * Math.PI) / 180);
+ if (yForce > 0) {
+ friction =
+ (-(9.81 * Math.cos((Number(angle) * Math.PI) / 180)) *
+ Math.sin(((90 - Number(angle)) * Math.PI) / 180) +
+ forceOfGravity.magnitude) /
+ Math.sin(((180 - Number(angle)) * Math.PI) / 180);
+ }
+ setReviewStaticMagnitude(friction);
+ setReviewStaticAngle(180 - angle);
+ };
+
+ // Solve for the correct answers to the generated problem
+ const getAnswersToQuestion = (
+ question: QuestionTemplate,
+ questionVars: number[]
+ ) => {
+ const solutions: number[] = [];
+
+ let theta: number = Number(wedgeAngle);
+ let index = question.variablesForQuestionSetup.indexOf("theta - max 45");
+ if (index >= 0) {
+ theta = questionVars[index];
+ }
+ let muS: number = Number(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(
+ forceOfGravity.magnitude * Math.cos((theta / 180) * Math.PI)
+ );
+ } else if (
+ description ==
+ "solve static force magnitude from wedge angle given equilibrium"
+ ) {
+ let normalForceMagnitude =
+ forceOfGravity.magnitude * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let frictionForceMagnitude =
+ (-normalForceMagnitude *
+ Math.sin((normalForceAngle * Math.PI) / 180) +
+ 9.81) /
+ 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 =
+ forceOfGravity.magnitude * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let frictionForceMagnitude =
+ (-normalForceMagnitude *
+ Math.sin((normalForceAngle * Math.PI) / 180) +
+ 9.81) /
+ 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);
+ }
+ }
+ setSelectedSolutions(solutions);
+ return solutions;
+ };
+
+ // In review mode, check if input answers match correct answers and optionally generate alert
+ const checkAnswers = (showAlert: boolean = true) => {
+ let error: boolean = false;
+ let epsilon: number = 0.01;
+ if (selectedQuestion) {
+ for (let i = 0; i < selectedQuestion.answerParts.length; i++) {
+ if (selectedQuestion.answerParts[i] == "force of gravity") {
+ if (
+ Math.abs(reviewGravityMagnitude - selectedSolutions[i]) > epsilon
+ ) {
+ error = true;
+ }
+ } else if (selectedQuestion.answerParts[i] == "angle of gravity") {
+ if (Math.abs(reviewGravityAngle - selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (selectedQuestion.answerParts[i] == "normal force") {
+ if (
+ Math.abs(reviewNormalMagnitude - selectedSolutions[i]) > epsilon
+ ) {
+ error = true;
+ }
+ } else if (selectedQuestion.answerParts[i] == "angle of normal force") {
+ if (Math.abs(reviewNormalAngle - selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (
+ selectedQuestion.answerParts[i] == "force of static friction"
+ ) {
+ if (
+ Math.abs(reviewStaticMagnitude - selectedSolutions[i]) > epsilon
+ ) {
+ error = true;
+ }
+ } else if (
+ selectedQuestion.answerParts[i] == "angle of static friction"
+ ) {
+ if (Math.abs(reviewStaticAngle - selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (
+ selectedQuestion.answerParts[i] == "coefficient of static friction"
+ ) {
+ if (
+ Math.abs(
+ Number(coefficientOfStaticFriction) - selectedSolutions[i]
+ ) > epsilon
+ ) {
+ error = true;
+ }
+ } else if (selectedQuestion.answerParts[i] == "wedge angle") {
+ if (Math.abs(Number(wedgeAngle) - selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ }
+ }
+ }
+ if (showAlert) {
+ if (!error) {
+ setSimulationPaused(false);
+ setTimeout(() => {
+ setSimulationPaused(true);
+ }, 3000);
+ } else {
+ setSimulationPaused(false);
+ setTimeout(() => {
+ setSimulationPaused(true);
+ }, 3000);
+ }
+ }
+ if (selectedQuestion.goal == "noMovement") {
+ if (!error) {
+ setNoMovement(true);
+ } else {
+ setNoMovement(false);
+ }
+ }
+ };
+
+ const resetReviewValuesToDefault = () => {
+ // Reset all values to default
+ setReviewGravityMagnitude(0);
+ setReviewGravityAngle(0);
+ setReviewNormalMagnitude(0);
+ setReviewNormalAngle(0);
+ setReviewStaticMagnitude(0);
+ setReviewStaticAngle(0);
+ setCoefficientOfKineticFriction(0);
+ setSimulationPaused(true);
+ setAnswerInputFields(<div></div>);
+ };
+
+ // In review mode, edit force arrow sketch on mouse movement
+ const editForce = (element: VectorTemplate) => {
+ if (!sketching) {
+ const sketches = forceSketches.filter((sketch) => sketch != element);
+ setForceSketches(sketches);
+ setCurrentForceSketch(element);
+ setSketching(true);
+ }
+ };
+
+ // In review mode, used to delete force arrow sketch on SHIFT+click
+ const deleteForce = (element: VectorTemplate) => {
+ if (!sketching) {
+ const sketches = forceSketches.filter((sketch) => sketch != element);
+ setForceSketches(sketches);
+ }
+ };
+
+ // In review mode, reset problem variables and generate a new question
+ const generateNewQuestion = () => {
+ resetReviewValuesToDefault();
+
+ const vars: number[] = [];
+ let question: QuestionTemplate = questions.inclinePlane[0];
+
+ if (simulationType == "Inclined Plane") {
+ if (questionNumber == questions.inclinePlane.length - 1) {
+ setQuestionNumber(0);
+ } else {
+ setQuestionNumber(questionNumber + 1);
+ }
+ question = questions.inclinePlane[questionNumber];
+
+ let coefficient = 0;
+ let wedge = 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);
+ wedge = randValue;
+ } else if (
+ question.variablesForQuestionSetup[i] ==
+ "coefficient of static friction"
+ ) {
+ let randValue = Math.round(Math.random() * 1000) / 1000;
+ vars.push(randValue);
+ coefficient = randValue;
+ }
+ }
+ setWedgeAngle(wedge);
+ changeWedgeBasedOnNewAngle(wedge);
+ setCoefficientOfStaticFriction(coefficient);
+ 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)";
+ }
+ }
+ }
+ questionVariables = vars;
+ setSelectedQuestion(question);
+ setQuestionPartOne(q);
+ setQuestionPartTwo(question.question);
+ const answers = getAnswersToQuestion(question, vars);
+ generateInputFieldsForQuestion(false, question, answers);
+ };
+
+ // Generate answerInputFields for new review question
+ const generateInputFieldsForQuestion = (
+ showIcon: boolean = false,
+ question: QuestionTemplate = selectedQuestion,
+ answers: number[] = selectedSolutions
+ ) => {
+ let answerInput = [];
+ const d = new Date();
+ for (let i = 0; i < question.answerParts.length; i++) {
+ if (question.answerParts[i] == "force of gravity") {
+ setReviewGravityMagnitude(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <p>
+ F<sub>G</sub>
+ </p>
+ }
+ lowerBound={0}
+ changeValue={setReviewGravityMagnitude}
+ step={0.1}
+ unit={"N"}
+ upperBound={50}
+ value={reviewGravityMagnitude}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ } else if (question.answerParts[i] == "angle of gravity") {
+ setReviewGravityAngle(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <p>
+ &theta;<sub>G</sub>
+ </p>
+ }
+ lowerBound={0}
+ changeValue={setReviewGravityAngle}
+ step={1}
+ unit={"°"}
+ upperBound={360}
+ value={reviewGravityAngle}
+ radianEquivalent={true}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ } else if (question.answerParts[i] == "normal force") {
+ setReviewNormalMagnitude(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <p>
+ F<sub>N</sub>
+ </p>
+ }
+ lowerBound={0}
+ changeValue={setReviewNormalMagnitude}
+ step={0.1}
+ unit={"N"}
+ upperBound={50}
+ value={reviewNormalMagnitude}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ } else if (question.answerParts[i] == "angle of normal force") {
+ setReviewNormalAngle(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <p>
+ &theta;<sub>N</sub>
+ </p>
+ }
+ lowerBound={0}
+ changeValue={setReviewNormalAngle}
+ step={1}
+ unit={"°"}
+ upperBound={360}
+ value={reviewNormalAngle}
+ radianEquivalent={true}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ } else if (question.answerParts[i] == "force of static friction") {
+ setReviewStaticMagnitude(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <p>
+ F
+ <sub>
+ F<sub>s</sub>
+ </sub>
+ </p>
+ }
+ lowerBound={0}
+ changeValue={setReviewStaticMagnitude}
+ step={0.1}
+ unit={"N"}
+ upperBound={50}
+ value={reviewStaticMagnitude}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ } else if (question.answerParts[i] == "angle of static friction") {
+ setReviewStaticAngle(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <p>
+ &theta;
+ <sub>
+ F<sub>s</sub>
+ </sub>
+ </p>
+ }
+ lowerBound={0}
+ changeValue={setReviewStaticAngle}
+ step={1}
+ unit={"°"}
+ upperBound={360}
+ value={reviewStaticAngle}
+ radianEquivalent={true}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ } else if (question.answerParts[i] == "coefficient of static friction") {
+ updateReviewForcesBasedOnCoefficient(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">
+ &mu;<sub>s</sub>
+ </Typography>
+ Coefficient of static friction; between 0 and 1
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>
+ &mu;<sub>s</sub>
+ </Box>
+ </Tooltip>
+ }
+ lowerBound={0}
+ changeValue={setCoefficientOfStaticFriction}
+ step={0.1}
+ unit={""}
+ upperBound={1}
+ value={coefficientOfStaticFriction}
+ effect={updateReviewForcesBasedOnCoefficient}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ } else if (question.answerParts[i] == "wedge angle") {
+ updateReviewForcesBasedOnAngle(0);
+ answerInput.push(
+ <div key={i + d.getTime()}>
+ <InputField
+ label={
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">&theta;</Typography>
+ Angle of incline plane from the ground, 0-49
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>&theta;</Box>
+ </Tooltip>
+ }
+ lowerBound={0}
+ changeValue={setWedgeAngle}
+ step={1}
+ unit={"°"}
+ upperBound={49}
+ value={wedgeAngle}
+ effect={(val: number) => {
+ changeWedgeBasedOnNewAngle(val);
+ updateReviewForcesBasedOnAngle(val);
+ }}
+ radianEquivalent={true}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+ </div>
+ );
+ }
+ }
+
+ setAnswerInputFields(
+ <div
+ style={{ display: "flex", flexDirection: "column", alignItems: "left" }}
+ >
+ {answerInput}
+ </div>
+ );
+ };
+
+ // Remove floor and walls from simulation
+ const removeWalls = () => {
+ setWallPositions([]);
+ };
+
+ // Add floor and walls to simulation
+ const addWalls = () => {
+ if (wallPositions.length == 0) {
+ const walls: IWallProps[] = [];
+ walls.push({ length: 70, xPos: 0, yPos: 80, angleInDegrees: 0 });
+ walls.push({ length: 80, xPos: 0, yPos: 0, angleInDegrees: 90 });
+ walls.push({ length: 80, xPos: 69.5, yPos: 0, angleInDegrees: 90 });
+ setWallPositions(walls);
+ }
+ };
+
+ // Use effect hook to handle mode/topic change
+ useEffect(() => {
+ if (mode == "Freeform") {
+ setShowForceMagnitudes(true);
+ if (simulationType == "One Weight") {
+ addWeight();
+ setStartPosY(yMin + 50);
+ setStartPosX((xMax + xMin - 50) / 2);
+ setUpdatedForces([forceOfGravity]);
+ setStartForces([forceOfGravity]);
+ addWalls();
+ setSimulationReset(!simulationReset);
+ } else if (simulationType == "Two Weights") {
+ addTwoWeights();
+ setStartPosY(yMax - 100);
+ setStartPosX((xMax + xMin - 200) / 2);
+ setStartPosY2(yMax - 100);
+ setStartPosX2((xMax + xMin + 200) / 2);
+ setUpdatedForces([forceOfGravity]);
+ setStartForces([forceOfGravity]);
+ addWalls();
+ setSimulationReset(!simulationReset);
+ // TODO
+ } else if (simulationType == "Inclined Plane") {
+ addWedge();
+ changeWedgeBasedOnNewAngle(26);
+ addWalls();
+ setStartForces([forceOfGravity]);
+ updateForcesWithFriction(Number(coefficientOfStaticFriction));
+ } else if (simulationType == "Pendulum") {
+ const length = 300;
+ const angle = 50;
+ const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
+ const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
+ const xPos = xMax / 2 - x - 50;
+ const yPos = y - 50 - 5;
+ addPendulum();
+ setStartPosX(xPos);
+ setStartPosY(yPos);
+ const mag = 9.81 * Math.cos((50 * Math.PI) / 180);
+ const forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: 90 - angle,
+ };
+ setUpdatedForces([forceOfGravity, forceOfTension]);
+ setStartForces([forceOfGravity, forceOfTension]);
+ setPendulumAngle(50);
+ setPendulumLength(300);
+ setAdjustPendulumAngle({ angle: 50, length: 300 });
+ removeWalls();
+ }
+ } else if (mode == "Review") {
+ setShowForceMagnitudes(true);
+ if (simulationType == "Two Weights") {
+ // TODO
+ } else if (simulationType == "Inclined Plane") {
+ addWedge();
+ setUpdatedForces([]);
+ setStartForces([]);
+ addWalls();
+ }
+ setShowAcceleration(false);
+ setShowVelocity(false);
+ setShowForces(true);
+ generateNewQuestion();
+ } else if (mode == "Tutorial") {
+ setStepNumber(0);
+ if (simulationType == "One Weight") {
+ addWeight();
+ setStartPosY(yMin + 50);
+ setStartPosX((xMax + xMin - 50) / 2);
+ setSelectedTutorial(tutorials.freeWeight);
+ setSelectedTutorial(tutorials.freeWeight);
+ setStartForces(getForceFromJSON(tutorials.freeWeight.steps[0].forces));
+ setShowForceMagnitudes(tutorials.freeWeight.steps[0].showMagnitude);
+ addWalls();
+ } else if (simulationType == "Two Weights") {
+ // TODO
+ } else if (simulationType == "Pendulum") {
+ 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 = xMax / 2 - x - 50;
+ const yPos = y - 50 - 5;
+ addPendulum();
+ setStartPosX(xPos);
+ setStartPosY(yPos);
+ setSelectedTutorial(tutorials.pendulum);
+ setStartForces(getForceFromJSON(tutorials.pendulum.steps[0].forces));
+ setShowForceMagnitudes(tutorials.pendulum.steps[0].showMagnitude);
+ setPendulumAngle(50);
+ setPendulumLength(300);
+ setAdjustPendulumAngle({ angle: 30, length: 300 });
+ removeWalls();
+ } else if (simulationType == "Inclined Plane") {
+ addWedge();
+ setWedgeAngle(26);
+ changeWedgeBasedOnNewAngle(26);
+ setSelectedTutorial(tutorials.inclinePlane);
+ setStartForces(
+ getForceFromJSON(tutorials.inclinePlane.steps[0].forces)
+ );
+ setShowForceMagnitudes(tutorials.inclinePlane.steps[0].showMagnitude);
+ addWalls();
+ }
+ setSimulationReset(!simulationReset);
+ }
+ }, [simulationType, mode]);
+
+ const [showForceMagnitudes, setShowForceMagnitudes] = useState<boolean>(true);
+
+ const getForceFromJSON = (
+ json: {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+ }[]
+ ): 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,
+ };
+ forces.push(force);
+ }
+ return forces;
+ };
+
+ // Use effect hook to handle force change in review mode
+ useEffect(() => {
+ if (mode == "Review") {
+ const forceOfGravityReview: IForce = {
+ description: "Gravity",
+ magnitude: reviewGravityMagnitude,
+ directionInDegrees: reviewGravityAngle,
+ };
+ const normalForceReview: IForce = {
+ description: "Normal Force",
+ magnitude: reviewNormalMagnitude,
+ directionInDegrees: reviewNormalAngle,
+ };
+ const staticFrictionForceReview: IForce = {
+ description: "Static Friction Force",
+ magnitude: reviewStaticMagnitude,
+ directionInDegrees: reviewStaticAngle,
+ };
+ setStartForces([
+ forceOfGravityReview,
+ normalForceReview,
+ staticFrictionForceReview,
+ ]);
+ setUpdatedForces([
+ forceOfGravityReview,
+ normalForceReview,
+ staticFrictionForceReview,
+ ]);
+ }
+ }, [
+ reviewGravityMagnitude,
+ reviewGravityAngle,
+ reviewNormalMagnitude,
+ reviewNormalAngle,
+ reviewStaticMagnitude,
+ reviewStaticAngle,
+ ]);
+
+ // Use effect to add listener for SHIFT key, which determines if sketch force arrow will be edited or deleted on click
+ useEffect(() => {
+ document.addEventListener("keydown", (e) => {
+ if (e.shiftKey) {
+ setDeleteMode(true);
+ }
+ });
+ document.addEventListener("keyup", (e) => {
+ if (e.shiftKey) {
+ setDeleteMode(false);
+ }
+ });
+ }, []);
+
+ // Timer for animating the simulation
+ setInterval(() => {
+ setTimer(timer + 1);
+ }, 60);
+
+ return (
+ <div>
+ <div className="mechanicsSimulationContainer">
+ <div
+ className="mechanicsSimulationContentContainer"
+ onPointerMove={(e) => {
+ if (sketching) {
+ const x1 = positionXDisplay + 50;
+ const y1 = yMax - positionYDisplay - 2 * 50 + 5 + 50;
+ const x2 = e.clientX;
+ const y2 = e.clientY;
+ const height = Math.abs(y1 - y2) + 120;
+ const width = Math.abs(x1 - x2) + 120;
+ const top = Math.min(y1, y2) - 60;
+ const left = Math.min(x1, x2) - 60;
+ const x1Updated = x1 - left;
+ const x2Updated = x2 - left;
+ const y1Updated = y1 - top;
+ const y2Updated = y2 - top;
+ setCurrentForceSketch({
+ top: top,
+ left: left,
+ width: width,
+ height: height,
+ x1: x1Updated,
+ y1: y1Updated,
+ x2: x2Updated,
+ y2: y2Updated,
+ weightX: positionXDisplay,
+ weightY: positionYDisplay,
+ });
+ }
+ }}
+ onPointerDown={(e) => {
+ if (sketching && currentForceSketch) {
+ setSketching(false);
+ const sketches = forceSketches;
+ sketches.push(currentForceSketch);
+ setForceSketches(sketches);
+ setCurrentForceSketch(null);
+ }
+ }}
+ >
+ <div className="mechanicsSimulationButtonsAndElements">
+ <div className="mechanicsSimulationButtons">
+ {!simulationPaused && (
+ <div
+ style={{
+ position: "fixed",
+ left: "10vw",
+ top: "95vh",
+ width: "50vw",
+ }}
+ >
+ <LinearProgress />
+ </div>
+ )}
+ <div
+ style={{
+ position: "fixed",
+ top: 1 + "em",
+ left: xMin + 12 + "px",
+ }}
+ >
+ <div className="dropdownMenu">
+ <select
+ value={simulationType}
+ onChange={(event) => {
+ setSimulationType(event.target.value);
+ }}
+ style={{ height: "2em", width: "100%", fontSize: "16px" }}
+ >
+ <option value="One Weight">One Weight</option>
+ <option value="Two Weights">Two Weights</option>
+ <option value="Inclined Plane">Inclined Plane</option>
+ <option value="Pendulum">Pendulum</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div className="mechanicsSimulationElements">
+ {showForces && currentForceSketch && simulationPaused && (
+ <div
+ style={{
+ position: "fixed",
+ top: currentForceSketch.top,
+ left: currentForceSketch.left,
+ }}
+ >
+ <svg
+ width={currentForceSketch.width + "px"}
+ height={currentForceSketch.height + "px"}
+ >
+ <defs>
+ <marker
+ id="sketchArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="2"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,4 L6,2 z" fill={color} />
+ </marker>
+ </defs>
+ <line
+ x1={currentForceSketch.x1}
+ y1={currentForceSketch.y1}
+ x2={currentForceSketch.x2}
+ y2={currentForceSketch.y2}
+ stroke={color}
+ strokeWidth="10"
+ markerEnd="url(#sketchArrow)"
+ />
+ </svg>
+ </div>
+ )}
+ {showForces &&
+ forceSketches.length > 0 &&
+ simulationPaused &&
+ forceSketches.map((element: VectorTemplate, index) => {
+ return (
+ <div
+ key={index}
+ style={{
+ position: "fixed",
+ top: element.top + (positionYDisplay - element.weightY),
+ left:
+ element.left + (positionXDisplay - element.weightX),
+ }}
+ >
+ <svg
+ width={element.width + "px"}
+ height={element.height + "px"}
+ >
+ <defs>
+ <marker
+ id="sketchArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="2"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,4 L6,2 z" fill={color} />
+ </marker>
+ </defs>
+ <line
+ x1={element.x1}
+ y1={element.y1}
+ x2={element.x2}
+ y2={element.y2}
+ stroke={color}
+ strokeWidth="10"
+ markerEnd="url(#sketchArrow)"
+ onClick={() => {
+ if (deleteMode) {
+ deleteForce(element);
+ } else {
+ editForce(element);
+ }
+ }}
+ />
+ </svg>
+ </div>
+ );
+ })}
+ {weight && (
+ <Weight
+ adjustPendulumAngle={adjustPendulumAngle}
+ color={"red"}
+ displayXPosition={positionXDisplay}
+ displayXVelocity={velocityXDisplay}
+ displayYPosition={positionYDisplay}
+ displayYVelocity={velocityYDisplay}
+ elasticCollisions={elasticCollisions}
+ incrementTime={timer}
+ mass={1}
+ mode={mode}
+ noMovement={noMovement}
+ paused={simulationPaused}
+ pendulum={pendulum}
+ pendulumAngle={pendulumAngle}
+ pendulumLength={pendulumLength}
+ radius={50}
+ reset={simulationReset}
+ showForceMagnitudes={showForceMagnitudes}
+ setSketching={setSketching}
+ setDisplayXAcceleration={setAccelerationXDisplay}
+ setDisplayXPosition={setPositionXDisplay}
+ setDisplayXVelocity={setVelocityXDisplay}
+ setDisplayYAcceleration={setAccelerationYDisplay}
+ setDisplayYPosition={setPositionYDisplay}
+ setDisplayYVelocity={setVelocityYDisplay}
+ setPaused={setSimulationPaused}
+ setPendulumAngle={setPendulumAngle}
+ setPendulumLength={setPendulumLength}
+ setStartPendulumAngle={setStartPendulumAngle}
+ setUpdatedForces={setUpdatedForces}
+ showAcceleration={showAcceleration}
+ showForces={showForces}
+ showVelocity={showVelocity}
+ startForces={startForces}
+ startPosX={startPosX}
+ startPosY={startPosY}
+ timestepSize={0.002}
+ updateDisplay={displayChange}
+ updatedForces={updatedForces}
+ walls={wallPositions}
+ wedge={wedge}
+ wedgeHeight={wedgeHeight}
+ wedgeWidth={wedgeWidth}
+ coefficientOfKineticFriction={Number(
+ coefficientOfKineticFriction
+ )}
+ />
+ )}
+ {twoWeights && (
+ <Weight
+ adjustPendulumAngle={adjustPendulumAngle}
+ color={"blue"}
+ displayXPosition={positionXDisplay2}
+ displayXVelocity={velocityXDisplay2}
+ displayYPosition={positionYDisplay2}
+ displayYVelocity={velocityYDisplay2}
+ elasticCollisions={elasticCollisions}
+ incrementTime={timer}
+ mass={1}
+ mode={mode}
+ noMovement={noMovement}
+ paused={simulationPaused}
+ pendulum={pendulum}
+ pendulumAngle={pendulumAngle}
+ pendulumLength={pendulumLength}
+ radius={50}
+ reset={simulationReset}
+ showForceMagnitudes={showForceMagnitudes}
+ setSketching={setSketching}
+ setDisplayXAcceleration={setAccelerationXDisplay2}
+ setDisplayXPosition={setPositionXDisplay2}
+ setDisplayXVelocity={setVelocityXDisplay2}
+ setDisplayYAcceleration={setAccelerationYDisplay2}
+ setDisplayYPosition={setPositionYDisplay2}
+ setDisplayYVelocity={setVelocityYDisplay2}
+ setPaused={setSimulationPaused}
+ setPendulumAngle={setPendulumAngle}
+ setPendulumLength={setPendulumLength}
+ setStartPendulumAngle={setStartPendulumAngle}
+ setUpdatedForces={setUpdatedForces}
+ showAcceleration={showAcceleration}
+ showForces={showForces}
+ showVelocity={showVelocity}
+ startForces={startForces}
+ startPosX={startPosX2}
+ startPosY={startPosY2}
+ timestepSize={0.002}
+ updateDisplay={displayChange}
+ updatedForces={updatedForces}
+ walls={wallPositions}
+ wedge={wedge}
+ wedgeHeight={wedgeHeight}
+ wedgeWidth={wedgeWidth}
+ coefficientOfKineticFriction={Number(
+ coefficientOfKineticFriction
+ )}
+ />
+ )}
+ {wedge && (
+ <Wedge
+ startWidth={wedgeWidth}
+ startHeight={wedgeHeight}
+ startLeft={xMax * 0.5 - 200}
+ />
+ )}
+ </div>
+ <div>
+ {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}>
+ {simulationPaused && mode != "Tutorial" && (
+ <Tooltip title="Start simulation" followCursor>
+ <IconButton
+ onClick={() => {
+ setSimulationPaused(false);
+ }}
+ >
+ <PlayArrowIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ {!simulationPaused && mode != "Tutorial" && (
+ <Tooltip title="Pause simulation" followCursor>
+ <IconButton
+ onClick={() => {
+ setSimulationPaused(true);
+ }}
+ >
+ <PauseIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ {simulationPaused && mode != "Tutorial" && (
+ <Tooltip title="Reset simulation" followCursor>
+ <IconButton
+ onClick={() => {
+ setSimulationReset(!simulationReset);
+ }}
+ >
+ <ReplayIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ </Stack>
+ <div className="dropdownMenu">
+ <select
+ value={mode}
+ onChange={(event) => {
+ setMode(event.target.value);
+ }}
+ style={{ height: "2em", width: "100%", fontSize: "16px" }}
+ >
+ <option value="Freeform">Freeform Mode</option>
+ <option value="Review">Review Mode</option>
+ <option value="Tutorial">Tutorial Mode</option>
+ </select>
+ </div>
+ </div>
+ {mode == "Review" && (
+ <div>
+ {!hintDialogueOpen && (
+ <IconButton
+ onClick={() => {
+ setHintDialogueOpen(true);
+ }}
+ sx={{
+ position: "fixed",
+ left: xMax - 50 + "px",
+ top: yMin + 14 + "px",
+ }}
+ >
+ <QuestionMarkIcon />
+ </IconButton>
+ )}
+ <Dialog
+ maxWidth={"sm"}
+ fullWidth={true}
+ open={hintDialogueOpen}
+ onClose={() => setHintDialogueOpen(false)}
+ >
+ <DialogTitle>Hints</DialogTitle>
+ <DialogContent>
+ {selectedQuestion.hints.map((hint, index) => {
+ return (
+ <div key={index}>
+ <DialogContentText>
+ <details>
+ <summary>
+ <b>
+ Hint {index + 1}: {hint.description}
+ </b>
+ </summary>
+ {hint.content}
+ </details>
+ </DialogContentText>
+ </div>
+ );
+ })}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ onClick={() => {
+ setHintDialogueOpen(false);
+ }}
+ >
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ <div className="wordProblemBox">
+ <div className="question">
+ <p>{questionPartOne}</p>
+ <p>{questionPartTwo}</p>
+ </div>
+ <div className="answer">{answerInputFields}</div>
+ </div>
+ </div>
+ )}
+ {mode == "Tutorial" && (
+ <div className="wordProblemBox">
+ <div className="question">
+ <h2>Problem</h2>
+ <p>{selectedTutorial.question}</p>
+ </div>
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "spaceBetween",
+ width: "100%",
+ }}
+ >
+ <IconButton
+ onClick={() => {
+ let step = stepNumber - 1;
+ step = Math.max(step, 0);
+ step = Math.min(step, selectedTutorial.steps.length - 1);
+ setStepNumber(step);
+ setStartForces(
+ getForceFromJSON(selectedTutorial.steps[step].forces)
+ );
+ setUpdatedForces(
+ getForceFromJSON(selectedTutorial.steps[step].forces)
+ );
+ setShowForceMagnitudes(
+ selectedTutorial.steps[step].showMagnitude
+ );
+ }}
+ disabled={stepNumber == 0}
+ >
+ <ArrowLeftIcon />
+ </IconButton>
+ <div>
+ <h3>
+ Step {stepNumber + 1}:{" "}
+ {selectedTutorial.steps[stepNumber].description}
+ </h3>
+ <p>{selectedTutorial.steps[stepNumber].content}</p>
+ </div>
+ <IconButton
+ onClick={() => {
+ let step = stepNumber + 1;
+ step = Math.max(step, 0);
+ step = Math.min(step, selectedTutorial.steps.length - 1);
+ setStepNumber(step);
+ setStartForces(
+ getForceFromJSON(selectedTutorial.steps[step].forces)
+ );
+ setUpdatedForces(
+ getForceFromJSON(selectedTutorial.steps[step].forces)
+ );
+ setShowForceMagnitudes(
+ selectedTutorial.steps[step].showMagnitude
+ );
+ }}
+ disabled={stepNumber == selectedTutorial.steps.length - 1}
+ >
+ <ArrowRightIcon />
+ </IconButton>
+ </div>
+ <div>
+ <p>Resources</p>
+ {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>
+ )}
+ {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>
+ )}
+ {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>
+ )}
+ {mode == "Review" && (
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ marginTop: "10px",
+ }}
+ >
+ <p
+ style={{
+ color: "blue",
+ textDecoration: "underline",
+ cursor: "pointer",
+ }}
+ onClick={() => setMode("Tutorial")}
+ >
+ {" "}
+ Go to walkthrough{" "}
+ </p>
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <Button
+ onClick={() => {
+ setSimulationReset(!simulationReset);
+ checkAnswers();
+ generateInputFieldsForQuestion(true);
+ }}
+ variant="outlined"
+ >
+ <Typography>Submit</Typography>
+ </Button>
+ <Button
+ onClick={() => generateNewQuestion()}
+ variant="outlined"
+ >
+ <Typography>New question</Typography>
+ </Button>
+ </div>
+ </div>
+ )}
+
+ {mode == "Freeform" && (
+ <div>
+ <FormControl component="fieldset">
+ <FormGroup>
+ {!wedge && !pendulum && (
+ <FormControlLabel
+ control={
+ <Checkbox
+ value={elasticCollisions}
+ onChange={() =>
+ setElasticCollisions(!elasticCollisions)
+ }
+ />
+ }
+ label="Make collisions elastic"
+ labelPlacement="start"
+ />
+ )}
+ {!wedge && !pendulum && <Divider />}
+ <FormControlLabel
+ control={
+ <Checkbox
+ value={showForces}
+ onChange={() => setShowForces(!showForces)}
+ defaultChecked
+ />
+ }
+ label="Show force vectors"
+ labelPlacement="start"
+ />
+ <FormControlLabel
+ control={
+ <Checkbox
+ value={showAcceleration}
+ onChange={() => setShowAcceleration(!showAcceleration)}
+ />
+ }
+ label="Show acceleration vector"
+ labelPlacement="start"
+ />
+ <FormControlLabel
+ control={
+ <Checkbox
+ value={showVelocity}
+ onChange={() => setShowVelocity(!showVelocity)}
+ />
+ }
+ label="Show velocity vector"
+ labelPlacement="start"
+ />
+ </FormGroup>
+ </FormControl>
+ {wedge && simulationPaused && (
+ <div>
+ <InputField
+ label={
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">&theta;</Typography>
+ Angle of incline plane from the ground, 0-49
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>&theta;</Box>
+ </Tooltip>
+ }
+ lowerBound={0}
+ changeValue={setWedgeAngle}
+ step={1}
+ unit={"°"}
+ upperBound={49}
+ value={wedgeAngle}
+ effect={changeWedgeBasedOnNewAngle}
+ radianEquivalent={true}
+ mode={"Freeform"}
+ />
+ <InputField
+ label={
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">
+ &mu;<sub>s</sub>
+ </Typography>
+ Coefficient of static friction, between 0 and 1
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>
+ &mu;<sub>s</sub>
+ </Box>
+ </Tooltip>
+ }
+ lowerBound={0}
+ changeValue={setCoefficientOfStaticFriction}
+ step={0.1}
+ unit={""}
+ upperBound={1}
+ value={coefficientOfStaticFriction}
+ effect={updateForcesWithFriction}
+ mode={"Freeform"}
+ />
+ <InputField
+ label={
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">
+ &mu;<sub>k</sub>
+ </Typography>
+ Coefficient of kinetic friction, between 0 and
+ coefficient of static friction
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>
+ &mu;<sub>k</sub>
+ </Box>
+ </Tooltip>
+ }
+ lowerBound={0}
+ changeValue={setCoefficientOfKineticFriction}
+ step={0.1}
+ unit={""}
+ upperBound={Number(coefficientOfStaticFriction)}
+ value={coefficientOfKineticFriction}
+ mode={"Freeform"}
+ />
+ </div>
+ )}
+ {wedge && !simulationPaused && (
+ <Typography>
+ &theta;: {Math.round(Number(wedgeAngle) * 100) / 100}° ≈{" "}
+ {Math.round(((Number(wedgeAngle) * Math.PI) / 180) * 100) /
+ 100}{" "}
+ rad
+ <br />
+ &mu; <sub>s</sub>: {coefficientOfStaticFriction}
+ <br />
+ &mu; <sub>k</sub>: {coefficientOfKineticFriction}
+ </Typography>
+ )}
+ {pendulum && !simulationPaused && (
+ <Typography>
+ &theta;: {Math.round(pendulumAngle * 100) / 100}° ≈{" "}
+ {Math.round(((pendulumAngle * Math.PI) / 180) * 100) / 100}{" "}
+ rad
+ </Typography>
+ )}
+ {pendulum && simulationPaused && (
+ <div>
+ <InputField
+ label={
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">&theta;</Typography>
+ Pendulum angle offest from equilibrium
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>&theta;</Box>
+ </Tooltip>
+ }
+ lowerBound={0}
+ changeValue={setPendulumAngle}
+ step={1}
+ unit={"°"}
+ upperBound={59}
+ value={pendulumAngle}
+ effect={(value) => {
+ if (pendulum) {
+ const mag =
+ 1 * 9.81 * Math.cos((value * Math.PI) / 180);
+
+ const forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: 90 - value,
+ };
+ setUpdatedForces([forceOfGravity, forceOfTension]);
+ setAdjustPendulumAngle({
+ angle: value,
+ length: pendulumLength,
+ });
+ }
+ }}
+ radianEquivalent={true}
+ mode={"Freeform"}
+ />
+ <InputField
+ label={
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">Length</Typography>
+ Pendulum rod length
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>Length</Box>
+ </Tooltip>
+ }
+ lowerBound={0}
+ changeValue={setPendulumLength}
+ step={1}
+ unit={"m"}
+ upperBound={400}
+ value={pendulumLength}
+ effect={(value) => {
+ if (pendulum) {
+ setAdjustPendulumAngle({
+ angle: pendulumAngle,
+ length: value,
+ });
+ }
+ }}
+ radianEquivalent={false}
+ mode={"Freeform"}
+ />
+ </div>
+ )}
+ </div>
+ )}
+ <div className="mechanicsSimulationEquation">
+ {mode == "Freeform" && twoWeights && <p>Red Weight</p>}
+ {mode == "Freeform" && weight && (
+ <table>
+ <tbody>
+ <tr>
+ <td>&nbsp;</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"
+ );
+ }}
+ >
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">Position</Typography>
+ Equation: x<sub>1</sub>
+ =x
+ <sub>0</sub>
+ +v
+ <sub>0</sub>
+ t+0.5at
+ <sup>2</sup>
+ <br />
+ Units: m
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>Position</Box>
+ </Tooltip>
+ </td>
+ <td>
+ {(!simulationPaused || wedge) && (
+ <p style={{ cursor: "default" }}>
+ {positionXDisplay} m
+ </p>
+ )}{" "}
+ {simulationPaused && !wedge && (
+ <InputField
+ lowerBound={0}
+ changeValue={setPositionXDisplay}
+ step={1}
+ unit={"m"}
+ upperBound={xMax}
+ value={positionXDisplay}
+ effect={(value) => {
+ setDisplayChange({
+ xDisplay: value,
+ yDisplay: positionYDisplay,
+ });
+ }}
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ </td>
+ <td>
+ {(!simulationPaused || wedge) && (
+ <p style={{ cursor: "default" }}>
+ {positionYDisplay} m
+ </p>
+ )}{" "}
+ {simulationPaused && !wedge && (
+ <InputField
+ lowerBound={0}
+ changeValue={setPositionYDisplay}
+ step={1}
+ unit={"m"}
+ upperBound={yMax}
+ value={positionYDisplay}
+ effect={(value) => {
+ setDisplayChange({
+ xDisplay: positionXDisplay,
+ yDisplay: value,
+ });
+ }}
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ </td>
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: "help" }}
+ onClick={() => {
+ window.open(
+ "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ );
+ }}
+ >
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">Velocity</Typography>
+ Equation: v<sub>1</sub>
+ =v
+ <sub>0</sub>
+ +at
+ <br />
+ Units: m/s
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>Velocity</Box>
+ </Tooltip>
+ </td>
+ <td>
+ {(!simulationPaused || pendulum || wedge) && (
+ <p style={{ cursor: "default" }}>
+ {velocityXDisplay} m/s
+ </p>
+ )}{" "}
+ {simulationPaused && !pendulum && !wedge && (
+ <InputField
+ lowerBound={-50}
+ changeValue={setVelocityXDisplay}
+ step={1}
+ unit={"m/s"}
+ upperBound={50}
+ value={velocityXDisplay}
+ effect={(value) =>
+ setDisplayChange({
+ xDisplay: positionXDisplay,
+ yDisplay: positionYDisplay,
+ })
+ }
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ </td>
+ <td>
+ {(!simulationPaused || pendulum || wedge) && (
+ <p style={{ cursor: "default" }}>
+ {velocityYDisplay} m/s
+ </p>
+ )}{" "}
+ {simulationPaused && !pendulum && !wedge && (
+ <InputField
+ lowerBound={-50}
+ changeValue={setVelocityYDisplay}
+ step={1}
+ unit={"m/s"}
+ upperBound={50}
+ value={velocityYDisplay}
+ effect={(value) =>
+ setDisplayChange({
+ xDisplay: positionXDisplay,
+ yDisplay: positionYDisplay,
+ })
+ }
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ </td>
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: "help" }}
+ onClick={() => {
+ window.open(
+ "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ );
+ }}
+ >
+ <Tooltip
+ title={
+ <React.Fragment>
+ <Typography color="inherit">
+ Acceleration
+ </Typography>
+ Equation: a=F/m
+ <br />
+ Units: m/s
+ <sup>2</sup>
+ </React.Fragment>
+ }
+ followCursor
+ >
+ <Box>Acceleration</Box>
+ </Tooltip>
+ </td>
+ <td style={{ cursor: "default" }}>
+ {accelerationXDisplay} m/s<sup>2</sup>
+ </td>
+ <td style={{ cursor: "default" }}>
+ {accelerationYDisplay} m/s<sup>2</sup>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ )}
+ </div>
+ {/* {mode == "Freeform" &&
+ simulationElements.length > 0 &&
+ simulationElements[0].pendulum && (
+ <div className="mechanicsSimulationEquation">
+ <table>
+ <tbody>
+ <tr>
+ <td>&nbsp;</td>
+ <td>Value</td>
+ </tr>
+ <tr>
+ <td>Potential Energy</td>
+ <td>
+ {Math.round(
+ pendulumLength *
+ (1 - Math.cos(pendulumAngle)) *
+ 9.81 *
+ 10
+ ) / 10}{" "}
+ J
+ </td>
+ </tr>
+ <tr>
+ <td>Kinetic Energy</td>
+ <td>
+ {Math.round(
+ (Math.round(
+ pendulumLength *
+ (1 - Math.cos(startPendulumAngle)) *
+ 9.81 *
+ 10
+ ) /
+ 10 -
+ Math.round(
+ pendulumLength *
+ (1 - Math.cos(pendulumAngle)) *
+ 9.81 *
+ 10
+ ) /
+ 10) *
+ 10
+ ) / 10}{" "}
+ J
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <b>Total Energy</b>
+ </td>
+ <td>
+ {Math.round(
+ pendulumLength *
+ (1 - Math.cos(startPendulumAngle)) *
+ 9.81 *
+ 10
+ ) / 10}{" "}
+ J
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ )}*/}
+ </div>
+ </div>
+ <CoordinateSystem top={window.innerHeight - 120} right={xMin + 90} />
+ </div>
+ );
+}
+
+export default App;
diff --git a/src/client/views/nodes/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsSimulationBox.scss
index 2eb6e6ff0..f756d59fc 100644
--- a/src/client/views/nodes/PhysicsSimulationBox.scss
+++ b/src/client/views/nodes/PhysicsSimulationBox.scss
@@ -1,3 +1,69 @@
-.physicsSimulationContainer {
-
-} \ No newline at end of file
+* {
+ box-sizing: border-box;
+ font-size: 14px;
+}
+
+.mechanicsSimulationContainer {
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+
+ .mechanicsSimulationContentContainer {
+ width: 70%;
+
+ .mechanicsSimulationButtons {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+
+ .mechanicsSimulationEquationContainer {
+ width: 30%;
+ padding: 1em;
+ display: flex;
+ flex-direction: column;
+
+ .mechanicsSimulationControls {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .slider {
+ margin-top: 0.5em;
+ }
+ }
+}
+
+.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: 5000;
+}
+
+.wordProblemBox {
+ border-style: solid;
+ border-color: black;
+ border-width: 1px;
+ margin-top: 10px;
+ padding: 10px;
+}
+
+.answer-inactive {
+ pointer-events: none;
+}
diff --git a/src/client/views/nodes/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsSimulationBox.tsx
index b16bccdde..4a68ba5aa 100644
--- a/src/client/views/nodes/PhysicsSimulationBox.tsx
+++ b/src/client/views/nodes/PhysicsSimulationBox.tsx
@@ -3,6 +3,7 @@ import { FieldView, FieldViewProps } from './FieldView';
import React = require('react');
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { observer } from 'mobx-react';
+import App from './PhysicsSimulationApp';
export interface IForce {
description: string;
@@ -18,65 +19,17 @@ export interface IWallProps {
@observer
export default class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
- forceOfGravity: IForce = {
- description: "Gravity",
- magnitude: 9.81,
- directionInDegrees: 270,
-};
-
- // Logic for Dash integration
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PhysicsSimulationBox, fieldKey); }
constructor(props: any) {
super(props);
- this.state = {
- timer: 0,
- weight: true,
- pendulum: false,
- wedge: false,
- startPosX: 0,
- startPosY: 0,
- showVelocity: false,
- showAcceleration: false,
- showForces: false,
- elasticCollisions: false,
- updatedForces: [this.forceOfGravity],
- wallPositions: []
- }
}
- // Add one weight to the simulation
- addWeight = () => {
- this.setState({weight: true});
- this.setState({wedge: false});
- this.setState({pendulum: false});
- };
-
-// Remove floor and walls from simulation
-removeWalls = () => {
- this.setState({wallPositions: []});
-};
-
-// Add floor and walls to simulation
-addWalls = () => {
- const walls: IWallProps[] = [];
- walls.push({ length: 70, xPos: 0, yPos: 80, angleInDegrees: 0 });
- walls.push({ length: 80, xPos: 0, yPos: 0, angleInDegrees: 90 });
- walls.push({ length: 80, xPos: 69.5, yPos: 0, angleInDegrees: 90 });
- this.setState({wallPositions: walls});
-};
-
- // Timer for animating the simulation
- // setInterval(() => {
- // const time = this.timer ?? 0
- // this.setState({timer: time+1});
- // }, 60);
-
render () {
return (
<div className = "physicsSimulationContainer">
-
- </div>
+ <App/>
+ </div>
);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/PhysicsSimulationWall.tsx b/src/client/views/nodes/PhysicsSimulationWall.tsx
new file mode 100644
index 000000000..c63538cc0
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationWall.tsx
@@ -0,0 +1,35 @@
+import { useState, useEffect } from "react";
+import "./Weight.scss";
+
+export interface Force {
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWallProps {
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
+}
+
+export const Wall = (props: IWallProps) => {
+ const { length, xPos, yPos, angleInDegrees } = props;
+
+ let wallStyle = {
+ width: length + "%",
+ height: 0.5 + "vw",
+ position: "absolute" as "absolute",
+ left: xPos + "%",
+ top: yPos + "%",
+ backgroundColor: "#6c7b8b",
+ zIndex: -1000,
+ margin: 0,
+ padding: 0,
+ };
+ if (angleInDegrees != 0) {
+ wallStyle.width = 0.5 + "vw";
+ wallStyle.height = length + "%";
+ }
+
+ return <div style={wallStyle}></div>;
+};
diff --git a/src/client/views/nodes/PhysicsSimulationWedge.tsx b/src/client/views/nodes/PhysicsSimulationWedge.tsx
new file mode 100644
index 000000000..af01c1c51
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationWedge.tsx
@@ -0,0 +1,64 @@
+import { useState, useEffect, useCallback } from "react";
+import "./Wedge.scss";
+
+export interface IWedgeProps {
+ startHeight: number;
+ startWidth: number;
+ startLeft: number;
+}
+
+export const Wedge = (props: IWedgeProps) => {
+ const { startHeight, startWidth, startLeft } = props;
+
+ const [angleInRadians, setAngleInRadians] = useState(
+ Math.atan(startHeight / startWidth)
+ );
+ const [left, setLeft] = useState(startLeft);
+ const [coordinates, setCoordinates] = useState("");
+
+ const color = "#deb887";
+
+ useEffect(() => {
+ const coordinatePair1 =
+ Math.round(left) + "," + Math.round(window.innerHeight * 0.8) + " ";
+ const coordinatePair2 =
+ Math.round(left + startWidth) +
+ "," +
+ Math.round(window.innerHeight * 0.8) +
+ " ";
+ const coordinatePair3 =
+ Math.round(left) +
+ "," +
+ Math.round(window.innerHeight * 0.8 - startHeight);
+ const coord = coordinatePair1 + coordinatePair2 + coordinatePair3;
+ setCoordinates(coord);
+ }, [left, startWidth, startHeight]);
+
+ useEffect(() => {
+ setAngleInRadians(Math.atan(startHeight / startWidth));
+ }, [startWidth, startHeight]);
+
+ return (
+ <div>
+ <div style={{ position: "absolute", left: "0", top: "0", zIndex: -5 }}>
+ <svg
+ width={window.innerWidth * 0.7 + "px"}
+ height={window.innerHeight * 0.8 + "px"}
+ >
+ <polygon points={coordinates} style={{ fill: "burlywood" }} />
+ </svg>
+ </div>
+
+ <p
+ style={{
+ position: "absolute",
+ zIndex: 500,
+ left: Math.round(left + startWidth - 80) + "px",
+ top: Math.round(window.innerHeight * 0.8 - 40) + "px",
+ }}
+ >
+ {Math.round(((angleInRadians * 180) / Math.PI) * 100) / 100}°
+ </p>
+ </div>
+ );
+};
diff --git a/src/client/views/nodes/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsSimulationWeight.tsx
new file mode 100644
index 000000000..227f20901
--- /dev/null
+++ b/src/client/views/nodes/PhysicsSimulationWeight.tsx
@@ -0,0 +1,913 @@
+import { InputAdornment, TextField } from "@mui/material";
+import { useEffect, useState } from "react";
+import { IWallProps } from "./PhysicsSimulationWall";
+import { Wedge } from "./PhysicsSimulationWedge";
+
+export interface IForce {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWeightProps {
+ adjustPendulumAngle: { angle: number; length: number };
+ color: string;
+ displayXPosition: number;
+ displayYPosition: number;
+ displayXVelocity: number;
+ displayYVelocity: number;
+ elasticCollisions: boolean;
+ startForces: IForce[];
+ incrementTime: number;
+ mass: number;
+ paused: boolean;
+ pendulum: boolean;
+ pendulumLength: number;
+ wedge: boolean;
+ radius: number;
+ reset: boolean;
+ setDisplayXAcceleration: (val: number) => any;
+ setDisplayXPosition: (val: number) => any;
+ setDisplayXVelocity: (val: number) => any;
+ setDisplayYAcceleration: (val: number) => any;
+ setDisplayYPosition: (val: number) => any;
+ setDisplayYVelocity: (val: number) => any;
+ setPaused: (bool: boolean) => any;
+ setPendulumAngle: (val: number) => any;
+ setPendulumLength: (val: number) => any;
+ setStartPendulumAngle: (val: number) => any;
+ showAcceleration: boolean;
+ mode: string;
+ noMovement: boolean;
+ pendulumAngle: number;
+ setSketching: (val: boolean) => any;
+ showForces: boolean;
+ showForceMagnitudes: boolean;
+ showVelocity: boolean;
+ startPosX: number;
+ startPosY: number;
+ startVelX?: number;
+ startVelY?: number;
+ timestepSize: number;
+ updateDisplay: { xDisplay: number; yDisplay: number };
+ updatedForces: IForce[];
+ setUpdatedForces: (val: IForce[]) => any;
+ walls: IWallProps[];
+ coefficientOfKineticFriction: number;
+ wedgeWidth: number;
+ wedgeHeight: number;
+}
+
+export const Weight = (props: IWeightProps) => {
+ const {
+ adjustPendulumAngle,
+ color,
+ displayXPosition,
+ displayYPosition,
+ displayXVelocity,
+ displayYVelocity,
+ elasticCollisions,
+ startForces,
+ incrementTime,
+ mass,
+ paused,
+ pendulum,
+ pendulumLength,
+ wedge,
+ radius,
+ mode,
+ noMovement,
+ pendulumAngle,
+ reset,
+ setSketching,
+ setDisplayXAcceleration,
+ setDisplayXPosition,
+ setDisplayXVelocity,
+ setDisplayYAcceleration,
+ setDisplayYPosition,
+ setDisplayYVelocity,
+ setPaused,
+ setPendulumAngle,
+ setPendulumLength,
+ setStartPendulumAngle,
+ showAcceleration,
+ showForces,
+ showForceMagnitudes,
+ showVelocity,
+ startPosX,
+ startPosY,
+ startVelX,
+ startVelY,
+ timestepSize,
+ updateDisplay,
+ updatedForces,
+ setUpdatedForces,
+ walls,
+ coefficientOfKineticFriction,
+ wedgeWidth,
+ wedgeHeight,
+ } = props;
+
+ // Constants
+ const draggable = !wedge && mode == "Freeform";
+ const epsilon = 0.0001;
+
+ const forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: mass * 9.81,
+ directionInDegrees: 270,
+ };
+ const xMax = window.innerWidth * 0.7;
+ const xMin = 0;
+ const yMax = window.innerHeight * 0.8;
+ const yMin = 0;
+
+ // State hooks
+ const [dragging, setDragging] = useState(false);
+ const [kineticFriction, setKineticFriction] = useState(false);
+ const [updatedStartPosX, setUpdatedStartPosX] = useState(startPosX);
+ const [updatedStartPosY, setUpdatedStartPosY] = useState(startPosY);
+ const [xPosition, setXPosition] = useState(startPosX);
+ const [xVelocity, setXVelocity] = useState(startVelX ?? 0);
+ const [yPosition, setYPosition] = useState(startPosY);
+ const [yVelocity, setYVelocity] = useState(startVelY ?? 0);
+
+ // Helper function to go between display and real values
+ const getDisplayYPos = (yPos: number) => {
+ return yMax - yPos - 2 * radius + 5;
+ };
+ const getYPosFromDisplay = (yDisplay: number) => {
+ return yMax - yDisplay - 2 * radius + 5;
+ };
+
+ // Set display values based on real values
+ const setYPosDisplay = (yPos: number) => {
+ const displayPos = getDisplayYPos(yPos);
+ setDisplayYPosition(Math.round(displayPos * 100) / 100);
+ };
+ const setXPosDisplay = (xPos: number) => {
+ setDisplayXPosition(Math.round(xPos * 100) / 100);
+ };
+ const setYVelDisplay = (yVel: number) => {
+ setDisplayYVelocity((-1 * Math.round(yVel * 100)) / 100);
+ };
+ const setXVelDisplay = (xVel: number) => {
+ setDisplayXVelocity(Math.round(xVel * 100) / 100);
+ };
+
+ const setDisplayValues = (
+ xPos: number = xPosition,
+ yPos: number = yPosition,
+ xVel: number = xVelocity,
+ yVel: number = yVelocity
+ ) => {
+ setYPosDisplay(yPos);
+ setXPosDisplay(xPos);
+ setYVelDisplay(yVel);
+ setXVelDisplay(xVel);
+ setDisplayYAcceleration(
+ (-1 * Math.round(getNewAccelerationY(updatedForces) * 100)) / 100
+ );
+ setDisplayXAcceleration(
+ Math.round(getNewAccelerationX(updatedForces) * 100) / 100
+ );
+ };
+
+ // When display values updated by user, update real values
+ useEffect(() => {
+ if (updateDisplay.xDisplay != xPosition) {
+ let x = updateDisplay.xDisplay;
+ x = Math.max(0, x);
+ x = Math.min(x, xMax - 2 * radius);
+ setUpdatedStartPosX(x);
+ setXPosition(x);
+ setDisplayXPosition(x);
+ }
+
+ if (updateDisplay.yDisplay != getDisplayYPos(yPosition)) {
+ let y = updateDisplay.yDisplay;
+ y = Math.max(0, y);
+ y = Math.min(y, yMax - 2 * radius);
+ setDisplayYPosition(y);
+ let coordinatePosition = getYPosFromDisplay(y);
+ setUpdatedStartPosY(coordinatePosition);
+ setYPosition(coordinatePosition);
+ }
+
+ if (displayXVelocity != xVelocity) {
+ let x = displayXVelocity;
+ setXVelocity(x);
+ setDisplayXVelocity(x);
+ }
+
+ if (displayYVelocity != -yVelocity) {
+ let y = displayYVelocity;
+ setYVelocity(-y);
+ setDisplayYVelocity(y);
+ }
+ }, [updateDisplay]);
+
+ // Check for collisions and update
+ useEffect(() => {
+ if (!paused && !noMovement) {
+ let collisions = false;
+ if (!pendulum) {
+ const collisionsWithGround = checkForCollisionsWithGround();
+ const collisionsWithWalls = checkForCollisionsWithWall();
+ collisions = collisionsWithGround || collisionsWithWalls;
+ }
+ if (!collisions) {
+ update();
+ }
+ setDisplayValues();
+ }
+ }, [incrementTime]);
+
+ useEffect(() => {
+ resetEverything();
+ }, [reset]);
+
+ useEffect(() => {
+ setXVelocity(startVelX ?? 0);
+ setYVelocity(startVelY ?? 0);
+ setDisplayValues();
+ }, [startForces]);
+
+ const resetEverything = () => {
+ setKineticFriction(false);
+ setXPosition(updatedStartPosX);
+ setYPosition(updatedStartPosY);
+ setXVelocity(startVelX ?? 0);
+ setYVelocity(startVelY ?? 0);
+ setUpdatedForces(startForces);
+ setDisplayValues();
+ };
+
+ // Change pendulum angle based on input field
+ useEffect(() => {
+ let length = adjustPendulumAngle.length;
+ const x =
+ length * Math.cos(((90 - adjustPendulumAngle.angle) * Math.PI) / 180);
+ const y =
+ length * Math.sin(((90 - adjustPendulumAngle.angle) * Math.PI) / 180);
+ const xPos = xMax / 2 - x - radius;
+ const yPos = y - radius - 5;
+ setXPosition(xPos);
+ setYPosition(yPos);
+ setUpdatedStartPosX(xPos);
+ setUpdatedStartPosY(yPos);
+ setPendulumAngle(adjustPendulumAngle.angle);
+ setPendulumLength(adjustPendulumAngle.length);
+ }, [adjustPendulumAngle]);
+
+ const getNewAccelerationX = (forceList: IForce[]) => {
+ let newXAcc = 0;
+ forceList.forEach((force) => {
+ newXAcc +=
+ (force.magnitude *
+ Math.cos((force.directionInDegrees * Math.PI) / 180)) /
+ mass;
+ });
+ return newXAcc;
+ };
+
+ const getNewAccelerationY = (forceList: IForce[]) => {
+ let newYAcc = 0;
+ forceList.forEach((force) => {
+ newYAcc +=
+ (-1 *
+ (force.magnitude *
+ Math.sin((force.directionInDegrees * Math.PI) / 180))) /
+ mass;
+ });
+ return newYAcc;
+ };
+
+ const getNewForces = (
+ xPos: number,
+ yPos: number,
+ xVel: number,
+ yVel: number
+ ) => {
+ if (!pendulum) {
+ return updatedForces;
+ }
+ const x = xMax / 2 - xPos - radius;
+ const y = yPos + 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);
+ setPendulumAngle(oppositeAngle);
+ setPendulumLength(Math.sqrt(x * x + y * y));
+
+ const mag =
+ mass * 9.81 * Math.cos((oppositeAngle * Math.PI) / 180) +
+ (mass * (xVel * xVel + yVel * yVel)) / pendulumLength;
+
+ const forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: angle,
+ };
+
+ return [forceOfGravity, forceOfTension];
+ };
+
+ const getNewPosition = (pos: number, vel: number) => {
+ return pos + vel * timestepSize;
+ };
+
+ const getNewVelocity = (vel: number, acc: number) => {
+ return vel + acc * timestepSize;
+ };
+
+ const checkForCollisionsWithWall = () => {
+ let collision = false;
+ const minX = xPosition;
+ const maxX = xPosition + 2 * radius;
+ const containerWidth = window.innerWidth;
+ if (xVelocity != 0) {
+ walls.forEach((wall) => {
+ if (wall.angleInDegrees == 90) {
+ const wallX = (wall.xPos / 100) * window.innerWidth;
+ if (wall.xPos < 0.35) {
+ if (minX <= wallX) {
+ if (elasticCollisions) {
+ setXVelocity(-xVelocity);
+ } else {
+ setXVelocity(0);
+ setXPosition(wallX + 5);
+ }
+ collision = true;
+ }
+ } else {
+ if (maxX >= wallX) {
+ if (elasticCollisions) {
+ setXVelocity(-xVelocity);
+ } else {
+ setXVelocity(0);
+ setXPosition(wallX - 2 * radius + 5);
+ }
+ collision = true;
+ }
+ }
+ }
+ });
+ }
+ return collision;
+ };
+
+ const checkForCollisionsWithGround = () => {
+ let collision = false;
+ const maxY = yPosition + 2 * radius;
+ if (yVelocity > 0) {
+ walls.forEach((wall) => {
+ if (wall.angleInDegrees == 0) {
+ const groundY = (wall.yPos / 100) * window.innerHeight;
+ if (maxY >= groundY) {
+ if (elasticCollisions) {
+ setYVelocity(-yVelocity);
+ } else {
+ setYVelocity(0);
+ setYPosition(groundY - 2 * radius + 5);
+ const forceOfGravity: IForce = {
+ description: "Gravity",
+ magnitude: 9.81 * mass,
+ directionInDegrees: 270,
+ };
+ const normalForce: IForce = {
+ description: "Normal force",
+ magnitude: 9.81 * mass,
+ directionInDegrees: wall.angleInDegrees + 90,
+ };
+ setUpdatedForces([forceOfGravity, normalForce]);
+ }
+ collision = true;
+ }
+ }
+ });
+ }
+ return collision;
+ };
+
+ useEffect(() => {
+ if (wedge && xVelocity != 0 && mode != "Review" && !kineticFriction) {
+ setKineticFriction(true);
+ //switch from static to kinetic friction
+ const normalForce: IForce = {
+ description: "Normal Force",
+ magnitude:
+ forceOfGravity.magnitude *
+ Math.cos(Math.atan(wedgeHeight / wedgeWidth)),
+ directionInDegrees:
+ 180 - 90 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI,
+ };
+ let frictionForce: IForce = {
+ description: "Kinetic Friction Force",
+ magnitude:
+ coefficientOfKineticFriction *
+ forceOfGravity.magnitude *
+ Math.cos(Math.atan(wedgeHeight / wedgeWidth)),
+ directionInDegrees:
+ 180 - (Math.atan(wedgeHeight / wedgeWidth) * 180) / Math.PI,
+ };
+ // reduce magnitude of friction force if necessary such that block cannot slide up plane
+ let yForce = -forceOfGravity.magnitude;
+ yForce +=
+ normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+ yForce +=
+ frictionForce.magnitude *
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude =
+ (-normalForce.magnitude *
+ Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
+ forceOfGravity.magnitude) /
+ Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+ if (coefficientOfKineticFriction != 0) {
+ setUpdatedForces([forceOfGravity, normalForce, frictionForce]);
+ } else {
+ setUpdatedForces([forceOfGravity, normalForce]);
+ }
+ }
+ }, [xVelocity]);
+
+ const update = () => {
+ // RK4 update
+ let xPos = xPosition;
+ let yPos = yPosition;
+ let xVel = xVelocity;
+ let yVel = yVelocity;
+ for (let i = 0; i < 60; i++) {
+ let forces1 = getNewForces(xPos, yPos, xVel, yVel);
+ const xAcc1 = getNewAccelerationX(forces1);
+ const yAcc1 = getNewAccelerationY(forces1);
+ const xVel1 = getNewVelocity(xVel, xAcc1);
+ const yVel1 = getNewVelocity(yVel, yAcc1);
+
+ let xVel2 = getNewVelocity(xVel, xAcc1 / 2);
+ let yVel2 = getNewVelocity(yVel, yAcc1 / 2);
+ let xPos2 = getNewPosition(xPos, xVel1 / 2);
+ let yPos2 = getNewPosition(yPos, yVel1 / 2);
+ const forces2 = getNewForces(xPos2, yPos2, xVel2, yVel2);
+ const xAcc2 = getNewAccelerationX(forces2);
+ const yAcc2 = getNewAccelerationY(forces2);
+ xVel2 = getNewVelocity(xVel2, xAcc2);
+ yVel2 = getNewVelocity(yVel2, yAcc2);
+ xPos2 = getNewPosition(xPos2, xVel2);
+ yPos2 = getNewPosition(yPos2, yVel2);
+
+ let xVel3 = getNewVelocity(xVel, xAcc2 / 2);
+ let yVel3 = getNewVelocity(yVel, yAcc2 / 2);
+ let xPos3 = getNewPosition(xPos, xVel2 / 2);
+ let yPos3 = getNewPosition(yPos, yVel2 / 2);
+ const forces3 = getNewForces(xPos3, yPos3, xVel3, yVel3);
+ const xAcc3 = getNewAccelerationX(forces3);
+ const yAcc3 = getNewAccelerationY(forces3);
+ xVel3 = getNewVelocity(xVel3, xAcc3);
+ yVel3 = getNewVelocity(yVel3, yAcc3);
+ xPos3 = getNewPosition(xPos3, xVel3);
+ yPos3 = getNewPosition(yPos3, yVel3);
+
+ let xVel4 = getNewVelocity(xVel, xAcc3);
+ let yVel4 = getNewVelocity(yVel, yAcc3);
+ let xPos4 = getNewPosition(xPos, xVel3);
+ let yPos4 = getNewPosition(yPos, yVel3);
+ const forces4 = getNewForces(xPos4, yPos4, xVel4, yVel4);
+ const xAcc4 = getNewAccelerationX(forces4);
+ const yAcc4 = getNewAccelerationY(forces4);
+ xVel4 = getNewVelocity(xVel4, xAcc4);
+ yVel4 = getNewVelocity(yVel4, yAcc4);
+ xPos4 = getNewPosition(xPos4, xVel4);
+ yPos4 = getNewPosition(yPos4, yVel4);
+
+ xVel +=
+ timestepSize * (xAcc1 / 6.0 + xAcc2 / 3.0 + xAcc3 / 3.0 + xAcc4 / 6.0);
+ yVel +=
+ timestepSize * (yAcc1 / 6.0 + yAcc2 / 3.0 + yAcc3 / 3.0 + yAcc4 / 6.0);
+ xPos +=
+ timestepSize * (xVel1 / 6.0 + xVel2 / 3.0 + xVel3 / 3.0 + xVel4 / 6.0);
+ yPos +=
+ timestepSize * (yVel1 / 6.0 + yVel2 / 3.0 + yVel3 / 3.0 + yVel4 / 6.0);
+ }
+
+ setXVelocity(xVel);
+ setYVelocity(yVel);
+ setXPosition(xPos);
+ setYPosition(yPos);
+ setUpdatedForces(getNewForces(xPos, yPos, xVel, yVel));
+ };
+
+ let weightStyle = {
+ backgroundColor: color,
+ borderStyle: "solid",
+ borderColor: "black",
+ position: "absolute" as "absolute",
+ left: xPosition + "px",
+ top: yPosition + "px",
+ width: 2 * radius + "px",
+ height: 2 * radius + "px",
+ borderRadius: 50 + "%",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ touchAction: "none",
+ };
+ if (dragging) {
+ weightStyle.borderColor = "lightblue";
+ }
+
+ const [clickPositionX, setClickPositionX] = useState(0);
+ const [clickPositionY, setClickPositionY] = useState(0);
+ const labelBackgroundColor = `rgba(255,255,255,0.5)`;
+
+ // Update x start position
+ useEffect(() => {
+ setUpdatedStartPosX(startPosX);
+ setXPosition(startPosX);
+ setXPosDisplay(startPosX);
+ }, [startPosX]);
+
+ // Update y start position
+ useEffect(() => {
+ setUpdatedStartPosY(startPosY);
+ setYPosition(startPosY);
+ setYPosDisplay(startPosY);
+ }, [startPosY]);
+
+ return (
+ <div style={{ zIndex: -1000 }}>
+ <div
+ className="weightContainer"
+ onPointerDown={(e) => {
+ if (draggable) {
+ e.preventDefault();
+ setPaused(true);
+ setDragging(true);
+ setClickPositionX(e.clientX);
+ setClickPositionY(e.clientY);
+ } else if (mode == "Review") {
+ setSketching(true);
+ }
+ }}
+ onPointerMove={(e) => {
+ e.preventDefault();
+ if (dragging) {
+ let newY = yPosition + e.clientY - clickPositionY;
+ if (newY > yMax - 2 * radius) {
+ newY = yMax - 2 * radius;
+ }
+
+ let newX = xPosition + e.clientX - clickPositionX;
+ if (newX > xMax - 2 * radius) {
+ newX = xMax - 2 * radius;
+ } else if (newX < 0) {
+ newX = 0;
+ }
+
+ setXPosition(newX);
+ setYPosition(newY);
+ setUpdatedStartPosX(newX);
+ setUpdatedStartPosY(newY);
+ setDisplayYPosition(
+ Math.round((yMax - 2 * radius - newY + 5) * 100) / 100
+ );
+ setClickPositionX(e.clientX);
+ setClickPositionY(e.clientY);
+ setDisplayValues();
+ }
+ }}
+ onPointerUp={(e) => {
+ if (dragging) {
+ e.preventDefault();
+ if (!pendulum) {
+ resetEverything();
+ }
+ setDragging(false);
+ let newY = yPosition + e.clientY - clickPositionY;
+ if (newY > yMax - 2 * radius) {
+ newY = yMax - 2 * radius;
+ }
+
+ let newX = xPosition + e.clientX - clickPositionX;
+ if (newX > xMax - 2 * radius) {
+ newX = xMax - 2 * radius;
+ } else if (newX < 0) {
+ newX = 0;
+ }
+ if (pendulum) {
+ const x = xMax / 2 - newX - radius;
+ const y = newY + 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);
+ setPendulumAngle(oppositeAngle);
+ setPendulumLength(Math.sqrt(x * x + y * y));
+ const mag = 9.81 * Math.cos((oppositeAngle * Math.PI) / 180);
+ const forceOfTension: IForce = {
+ description: "Tension",
+ magnitude: mag,
+ directionInDegrees: angle,
+ };
+
+ setKineticFriction(false);
+ setXVelocity(startVelX ?? 0);
+ setYVelocity(startVelY ?? 0);
+ setDisplayValues();
+ setUpdatedForces([forceOfGravity, forceOfTension]);
+ }
+ }
+ }}
+ >
+ <div className="weight" style={weightStyle}>
+ <p className="weightLabel">{mass} kg</p>
+ </div>
+ </div>
+ {pendulum && (
+ <div
+ className="rod"
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ zIndex: -2,
+ }}
+ >
+ <svg width={xMax + "px"} height={window.innerHeight + "px"}>
+ <line
+ x1={xPosition + radius}
+ y1={yPosition + radius}
+ x2={xMax / 2}
+ y2={-5}
+ stroke={"#deb887"}
+ strokeWidth="10"
+ />
+ </svg>
+ {!dragging && (
+ <div>
+ <p
+ style={{
+ position: "absolute",
+ zIndex: 5,
+ left: xPosition + "px",
+ top: yPosition - 70 + "px",
+ backgroundColor: labelBackgroundColor,
+ }}
+ >
+ {Math.round(pendulumLength)} m
+ </p>
+ <p
+ style={{
+ position: "absolute",
+ zIndex: -1,
+ left: xMax / 2 + "px",
+ top: 30 + "px",
+ backgroundColor: labelBackgroundColor,
+ }}
+ >
+ {Math.round(pendulumAngle * 100) / 100}°
+ </p>
+ </div>
+ )}
+ </div>
+ )}
+ {!dragging && showAcceleration && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ zIndex: -1,
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={xMax + "px"} height={window.innerHeight + "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={xPosition + radius}
+ y1={yPosition + radius}
+ x2={xPosition + radius + getNewAccelerationX(updatedForces) * 5}
+ y2={yPosition + radius + getNewAccelerationY(updatedForces) * 5}
+ stroke={"green"}
+ strokeWidth="5"
+ markerEnd="url(#accArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left:
+ xPosition +
+ radius +
+ getNewAccelerationX(updatedForces) * 5 +
+ 25 +
+ "px",
+ top:
+ yPosition +
+ radius +
+ getNewAccelerationY(updatedForces) * 5 +
+ 25 +
+ "px",
+ zIndex: -1,
+ lineHeight: 0.5,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 *
+ Math.sqrt(
+ Math.pow(getNewAccelerationX(updatedForces) * 3, 2) +
+ Math.pow(getNewAccelerationY(updatedForces) * 3, 2)
+ )
+ ) / 100}{" "}
+ m/s<sup>2</sup>
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!dragging && showVelocity && (
+ <div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ zIndex: -1,
+ left: 0,
+ top: 0,
+ }}
+ >
+ <svg width={xMax + "px"} height={window.innerHeight + "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={xPosition + radius}
+ y1={yPosition + radius}
+ x2={xPosition + radius + xVelocity * 3}
+ y2={yPosition + radius + yVelocity * 3}
+ stroke={"blue"}
+ strokeWidth="5"
+ markerEnd="url(#velArrow)"
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: xPosition + radius + xVelocity * 3 + 25 + "px",
+ top: yPosition + radius + yVelocity * 3 + "px",
+ zIndex: -1,
+ lineHeight: 0.5,
+ }}
+ >
+ <p>
+ {Math.round(
+ 100 * Math.sqrt(xVelocity * xVelocity + yVelocity * yVelocity)
+ ) / 100}{" "}
+ m/s
+ </p>
+ </div>
+ </div>
+ </div>
+ )}
+ {!dragging &&
+ showForces &&
+ updatedForces.map((force, index) => {
+ if (force.magnitude < epsilon) {
+ return;
+ }
+ let arrowStartY: number = yPosition + radius;
+ const arrowStartX: number = xPosition + 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, yMax + 50);
+ labelTop = Math.max(labelTop, yMin);
+ labelLeft = Math.min(labelLeft, xMax - 60);
+ labelLeft = Math.max(labelLeft, xMin);
+
+ return (
+ <div key={index}>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ zIndex: -1,
+ left: xMin,
+ top: yMin,
+ }}
+ >
+ <svg
+ width={xMax - xMin + "px"}
+ height={window.innerHeight + "px"}
+ >
+ <defs>
+ <marker
+ id="forceArrow"
+ markerWidth="10"
+ markerHeight="10"
+ refX="0"
+ refY="3"
+ orient="auto"
+ markerUnits="strokeWidth"
+ >
+ <path d="M0,0 L0,6 L9,3 z" fill={color} />
+ </marker>
+ </defs>
+ <line
+ x1={arrowStartX}
+ y1={arrowStartY}
+ x2={arrowEndX}
+ y2={arrowEndY}
+ stroke={color}
+ strokeWidth="5"
+ markerEnd="url(#forceArrow)"
+ />
+ </svg>
+ </div>
+ <div
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ left: labelLeft + "px",
+ top: labelTop + "px",
+ // zIndex: -1,
+ lineHeight: 0.5,
+ backgroundColor: labelBackgroundColor,
+ }}
+ >
+ {force.description && <p>{force.description}</p>}
+ {!force.description && <p>Force</p>}
+ {showForceMagnitudes && (
+ <p>{Math.round(100 * force.magnitude) / 100} N</p>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+};