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;