From d5ebbf476aeb7ce3f88e2e4c3961ffed4ed8e61a Mon Sep 17 00:00:00 2001
From: brynnchernosky <56202540+brynnchernosky@users.noreply.github.com>
Date: Mon, 30 Jan 2023 14:14:46 -0500
Subject: start adding physics sim
---
src/client/views/nodes/PhysicsSimulationApp.tsx | 2154 ++++++++++++++++++++
src/client/views/nodes/PhysicsSimulationBox.scss | 72 +-
src/client/views/nodes/PhysicsSimulationBox.tsx | 53 +-
src/client/views/nodes/PhysicsSimulationWall.tsx | 35 +
src/client/views/nodes/PhysicsSimulationWedge.tsx | 64 +
src/client/views/nodes/PhysicsSimulationWeight.tsx | 913 +++++++++
6 files changed, 3238 insertions(+), 53 deletions(-)
create mode 100644 src/client/views/nodes/PhysicsSimulationApp.tsx
create mode 100644 src/client/views/nodes/PhysicsSimulationWall.tsx
create mode 100644 src/client/views/nodes/PhysicsSimulationWedge.tsx
create mode 100644 src/client/views/nodes/PhysicsSimulationWeight.tsx
(limited to 'src')
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) => (
+
+ ))(({ 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(
);
+ const [coefficientOfKineticFriction, setCoefficientOfKineticFriction] =
+ React.useState>(0);
+ const [coefficientOfStaticFriction, setCoefficientOfStaticFriction] =
+ React.useState>(0);
+ const [currentForceSketch, setCurrentForceSketch] =
+ useState(null);
+ const [deleteMode, setDeleteMode] = useState(false);
+ const [displayChange, setDisplayChange] = useState<{
+ xDisplay: number;
+ yDisplay: number;
+ }>({ xDisplay: 0, yDisplay: 0 });
+ const [elasticCollisions, setElasticCollisions] = useState(false);
+ const [forceSketches, setForceSketches] = useState([]);
+ const [questionPartOne, setQuestionPartOne] = useState("");
+ const [hintDialogueOpen, setHintDialogueOpen] = useState(false);
+ const [mode, setMode] = useState("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(0);
+ const [reviewGravityAngle, setReviewGravityAngle] = useState(0);
+ const [reviewGravityMagnitude, setReviewGravityMagnitude] =
+ useState(0);
+ const [reviewNormalAngle, setReviewNormalAngle] = useState(0);
+ const [reviewNormalMagnitude, setReviewNormalMagnitude] = useState(0);
+ const [reviewStaticAngle, setReviewStaticAngle] = useState(0);
+ const [reviewStaticMagnitude, setReviewStaticMagnitude] = useState(0);
+ const [selectedQuestion, setSelectedQuestion] = useState(
+ questions.inclinePlane[0]
+ );
+ const [selectedTutorial, setSelectedTutorial] = useState(
+ tutorials.inclinePlane
+ );
+ const [questionPartTwo, setQuestionPartTwo] = useState("");
+ const [selectedSolutions, setSelectedSolutions] = useState([]);
+ const [showAcceleration, setShowAcceleration] = useState(false);
+ const [showForces, setShowForces] = useState(true);
+ const [showVelocity, setShowVelocity] = useState(false);
+ const [simulationPaused, setSimulationPaused] = useState(true);
+ const [simulationReset, setSimulationReset] = useState(false);
+ const [simulationType, setSimulationType] =
+ useState("Inclined Plane");
+ const [sketching, setSketching] = useState(false);
+ const [startForces, setStartForces] = useState([forceOfGravity]);
+ const [startPendulumAngle, setStartPendulumAngle] = useState(0);
+ const [stepNumber, setStepNumber] = useState(0);
+ const [timer, setTimer] = useState(0);
+ const [updatedForces, setUpdatedForces] = useState([
+ forceOfGravity,
+ ]);
+ const [wallPositions, setWallPositions] = useState([]);
+ const [wedge, setWedge] = useState(false);
+ const [wedgeAngle, setWedgeAngle] = React.useState<
+ number | string | Array
+ >(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();
+ };
+
+ // 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(
+
+
+ FG
+
+ }
+ lowerBound={0}
+ changeValue={setReviewGravityMagnitude}
+ step={0.1}
+ unit={"N"}
+ upperBound={50}
+ value={reviewGravityMagnitude}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+
+ );
+ } else if (question.answerParts[i] == "angle of gravity") {
+ setReviewGravityAngle(0);
+ answerInput.push(
+
+
+ θG
+
+ }
+ lowerBound={0}
+ changeValue={setReviewGravityAngle}
+ step={1}
+ unit={"°"}
+ upperBound={360}
+ value={reviewGravityAngle}
+ radianEquivalent={true}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+
+ );
+ } else if (question.answerParts[i] == "normal force") {
+ setReviewNormalMagnitude(0);
+ answerInput.push(
+
+
+ FN
+
+ }
+ lowerBound={0}
+ changeValue={setReviewNormalMagnitude}
+ step={0.1}
+ unit={"N"}
+ upperBound={50}
+ value={reviewNormalMagnitude}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+
+ );
+ } else if (question.answerParts[i] == "angle of normal force") {
+ setReviewNormalAngle(0);
+ answerInput.push(
+
+
+ θN
+
+ }
+ lowerBound={0}
+ changeValue={setReviewNormalAngle}
+ step={1}
+ unit={"°"}
+ upperBound={360}
+ value={reviewNormalAngle}
+ radianEquivalent={true}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+
+ );
+ } else if (question.answerParts[i] == "force of static friction") {
+ setReviewStaticMagnitude(0);
+ answerInput.push(
+
+
+ F
+
+ Fs
+
+
+ }
+ lowerBound={0}
+ changeValue={setReviewStaticMagnitude}
+ step={0.1}
+ unit={"N"}
+ upperBound={50}
+ value={reviewStaticMagnitude}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+
+ );
+ } else if (question.answerParts[i] == "angle of static friction") {
+ setReviewStaticAngle(0);
+ answerInput.push(
+
+
+ θ
+
+ Fs
+
+
+ }
+ lowerBound={0}
+ changeValue={setReviewStaticAngle}
+ step={1}
+ unit={"°"}
+ upperBound={360}
+ value={reviewStaticAngle}
+ radianEquivalent={true}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+
+ );
+ } else if (question.answerParts[i] == "coefficient of static friction") {
+ updateReviewForcesBasedOnCoefficient(0);
+ answerInput.push(
+
+
+
+ μs
+
+ Coefficient of static friction; between 0 and 1
+
+ }
+ followCursor
+ >
+
+ μs
+
+
+ }
+ lowerBound={0}
+ changeValue={setCoefficientOfStaticFriction}
+ step={0.1}
+ unit={""}
+ upperBound={1}
+ value={coefficientOfStaticFriction}
+ effect={updateReviewForcesBasedOnCoefficient}
+ showIcon={showIcon}
+ correctValue={answers[i]}
+ />
+
+ );
+ } else if (question.answerParts[i] == "wedge angle") {
+ updateReviewForcesBasedOnAngle(0);
+ answerInput.push(
+
+
+ θ
+ Angle of incline plane from the ground, 0-49
+
+ }
+ followCursor
+ >
+ θ
+
+ }
+ 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]}
+ />
+
+ );
+ }
+ }
+
+ setAnswerInputFields(
+
+ {answerInput}
+
+ );
+ };
+
+ // 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(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 (
+
+
+
{
+ 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);
+ }
+ }}
+ >
+
+
+ {!simulationPaused && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {showForces && currentForceSketch && simulationPaused && (
+
+ )}
+ {showForces &&
+ forceSketches.length > 0 &&
+ simulationPaused &&
+ forceSketches.map((element: VectorTemplate, index) => {
+ return (
+
+
+
+ );
+ })}
+ {weight && (
+
+ )}
+ {twoWeights && (
+
+ )}
+ {wedge && (
+
+ )}
+
+
+ {wallPositions.map((element, index) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ {simulationPaused && mode != "Tutorial" && (
+
+ {
+ setSimulationPaused(false);
+ }}
+ >
+
+
+
+ )}
+ {!simulationPaused && mode != "Tutorial" && (
+
+ {
+ setSimulationPaused(true);
+ }}
+ >
+
+
+
+ )}
+ {simulationPaused && mode != "Tutorial" && (
+
+ {
+ setSimulationReset(!simulationReset);
+ }}
+ >
+
+
+
+ )}
+
+
+
+
+
+ {mode == "Review" && (
+
+ {!hintDialogueOpen && (
+
{
+ setHintDialogueOpen(true);
+ }}
+ sx={{
+ position: "fixed",
+ left: xMax - 50 + "px",
+ top: yMin + 14 + "px",
+ }}
+ >
+
+
+ )}
+
+
+
+
{questionPartOne}
+
{questionPartTwo}
+
+
{answerInputFields}
+
+
+ )}
+ {mode == "Tutorial" && (
+
+
+
Problem
+
{selectedTutorial.question}
+
+
+
{
+ 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}
+ >
+
+
+
+
+ Step {stepNumber + 1}:{" "}
+ {selectedTutorial.steps[stepNumber].description}
+
+
{selectedTutorial.steps[stepNumber].content}
+
+
{
+ 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}
+ >
+
+
+
+
+
Resources
+ {simulationType == "One Weight" && (
+
+ )}
+ {simulationType == "Inclined Plane" && (
+
+ )}
+ {simulationType == "Pendulum" && (
+
+ )}
+
+
+ )}
+ {mode == "Review" && (
+
+
setMode("Tutorial")}
+ >
+ {" "}
+ Go to walkthrough{" "}
+
+
+
+
+
+
+ )}
+
+ {mode == "Freeform" && (
+
+
+
+ {!wedge && !pendulum && (
+
+ setElasticCollisions(!elasticCollisions)
+ }
+ />
+ }
+ label="Make collisions elastic"
+ labelPlacement="start"
+ />
+ )}
+ {!wedge && !pendulum && }
+ setShowForces(!showForces)}
+ defaultChecked
+ />
+ }
+ label="Show force vectors"
+ labelPlacement="start"
+ />
+ setShowAcceleration(!showAcceleration)}
+ />
+ }
+ label="Show acceleration vector"
+ labelPlacement="start"
+ />
+ setShowVelocity(!showVelocity)}
+ />
+ }
+ label="Show velocity vector"
+ labelPlacement="start"
+ />
+
+
+ {wedge && simulationPaused && (
+
+
+ θ
+ Angle of incline plane from the ground, 0-49
+
+ }
+ followCursor
+ >
+ θ
+
+ }
+ lowerBound={0}
+ changeValue={setWedgeAngle}
+ step={1}
+ unit={"°"}
+ upperBound={49}
+ value={wedgeAngle}
+ effect={changeWedgeBasedOnNewAngle}
+ radianEquivalent={true}
+ mode={"Freeform"}
+ />
+
+
+ μs
+
+ Coefficient of static friction, between 0 and 1
+
+ }
+ followCursor
+ >
+
+ μs
+
+
+ }
+ lowerBound={0}
+ changeValue={setCoefficientOfStaticFriction}
+ step={0.1}
+ unit={""}
+ upperBound={1}
+ value={coefficientOfStaticFriction}
+ effect={updateForcesWithFriction}
+ mode={"Freeform"}
+ />
+
+
+ μk
+
+ Coefficient of kinetic friction, between 0 and
+ coefficient of static friction
+
+ }
+ followCursor
+ >
+
+ μk
+
+
+ }
+ lowerBound={0}
+ changeValue={setCoefficientOfKineticFriction}
+ step={0.1}
+ unit={""}
+ upperBound={Number(coefficientOfStaticFriction)}
+ value={coefficientOfKineticFriction}
+ mode={"Freeform"}
+ />
+
+ )}
+ {wedge && !simulationPaused && (
+
+ θ: {Math.round(Number(wedgeAngle) * 100) / 100}° ≈{" "}
+ {Math.round(((Number(wedgeAngle) * Math.PI) / 180) * 100) /
+ 100}{" "}
+ rad
+
+ μ s: {coefficientOfStaticFriction}
+
+ μ k: {coefficientOfKineticFriction}
+
+ )}
+ {pendulum && !simulationPaused && (
+
+ θ: {Math.round(pendulumAngle * 100) / 100}° ≈{" "}
+ {Math.round(((pendulumAngle * Math.PI) / 180) * 100) / 100}{" "}
+ rad
+
+ )}
+ {pendulum && simulationPaused && (
+
+
+ θ
+ Pendulum angle offest from equilibrium
+
+ }
+ followCursor
+ >
+ θ
+
+ }
+ 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"}
+ />
+
+ Length
+ Pendulum rod length
+
+ }
+ followCursor
+ >
+ Length
+
+ }
+ 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"}
+ />
+
+ )}
+
+ )}
+
+ {mode == "Freeform" && twoWeights &&
Red Weight
}
+ {mode == "Freeform" && weight && (
+
+
+
+ |
+ X |
+ Y |
+
+
+ {
+ window.open(
+ "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ );
+ }}
+ >
+
+ Position
+ Equation: x1
+ =x
+ 0
+ +v
+ 0
+ t+0.5at
+ 2
+
+ Units: m
+
+ }
+ followCursor
+ >
+ Position
+
+ |
+
+ {(!simulationPaused || wedge) && (
+
+ {positionXDisplay} m
+
+ )}{" "}
+ {simulationPaused && !wedge && (
+ {
+ setDisplayChange({
+ xDisplay: value,
+ yDisplay: positionYDisplay,
+ });
+ }}
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ |
+
+ {(!simulationPaused || wedge) && (
+
+ {positionYDisplay} m
+
+ )}{" "}
+ {simulationPaused && !wedge && (
+ {
+ setDisplayChange({
+ xDisplay: positionXDisplay,
+ yDisplay: value,
+ });
+ }}
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ |
+
+
+ {
+ window.open(
+ "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ );
+ }}
+ >
+
+ Velocity
+ Equation: v1
+ =v
+ 0
+ +at
+
+ Units: m/s
+
+ }
+ followCursor
+ >
+ Velocity
+
+ |
+
+ {(!simulationPaused || pendulum || wedge) && (
+
+ {velocityXDisplay} m/s
+
+ )}{" "}
+ {simulationPaused && !pendulum && !wedge && (
+
+ setDisplayChange({
+ xDisplay: positionXDisplay,
+ yDisplay: positionYDisplay,
+ })
+ }
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ |
+
+ {(!simulationPaused || pendulum || wedge) && (
+
+ {velocityYDisplay} m/s
+
+ )}{" "}
+ {simulationPaused && !pendulum && !wedge && (
+
+ setDisplayChange({
+ xDisplay: positionXDisplay,
+ yDisplay: positionYDisplay,
+ })
+ }
+ small={true}
+ mode={"Freeform"}
+ />
+ )}{" "}
+ |
+
+
+ {
+ window.open(
+ "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ );
+ }}
+ >
+
+
+ Acceleration
+
+ Equation: a=F/m
+
+ Units: m/s
+ 2
+
+ }
+ followCursor
+ >
+ Acceleration
+
+ |
+
+ {accelerationXDisplay} m/s2
+ |
+
+ {accelerationYDisplay} m/s2
+ |
+
+
+
+ )}
+
+ {/* {mode == "Freeform" &&
+ simulationElements.length > 0 &&
+ simulationElements[0].pendulum && (
+
+
+
+
+ |
+ Value |
+
+
+ Potential Energy |
+
+ {Math.round(
+ pendulumLength *
+ (1 - Math.cos(pendulumAngle)) *
+ 9.81 *
+ 10
+ ) / 10}{" "}
+ J
+ |
+
+
+ Kinetic Energy |
+
+ {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
+ |
+
+
+
+ Total Energy
+ |
+
+ {Math.round(
+ pendulumLength *
+ (1 - Math.cos(startPendulumAngle)) *
+ 9.81 *
+ 10
+ ) / 10}{" "}
+ J
+ |
+
+
+
+
+ )}*/}
+
+
+
+
+ );
+}
+
+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() {
- 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 (
-
-
+
+
);
}
}
\ 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 ;
+};
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 (
+
+
+
+
+ {Math.round(((angleInRadians * 180) / Math.PI) * 100) / 100}°
+
+
+ );
+};
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 (
+
+
{
+ 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]);
+ }
+ }
+ }}
+ >
+
+
+ {pendulum && (
+
+
+ {!dragging && (
+
+
+ {Math.round(pendulumLength)} m
+
+
+ {Math.round(pendulumAngle * 100) / 100}°
+
+
+ )}
+
+ )}
+ {!dragging && showAcceleration && (
+
+
+
+
+
+ {Math.round(
+ 100 *
+ Math.sqrt(
+ Math.pow(getNewAccelerationX(updatedForces) * 3, 2) +
+ Math.pow(getNewAccelerationY(updatedForces) * 3, 2)
+ )
+ ) / 100}{" "}
+ m/s2
+
+
+
+
+ )}
+ {!dragging && showVelocity && (
+
+
+
+
+
+ {Math.round(
+ 100 * Math.sqrt(xVelocity * xVelocity + yVelocity * yVelocity)
+ ) / 100}{" "}
+ m/s
+
+
+
+
+ )}
+ {!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 (
+
+
+
+ {force.description &&
{force.description}
}
+ {!force.description &&
Force
}
+ {showForceMagnitudes && (
+
{Math.round(100 * force.magnitude) / 100} N
+ )}
+
+
+ );
+ })}
+
+ );
+};
--
cgit v1.2.3-70-g09d2