import React, { useEffect, useState } from "react";
import "./PhysicsSimulationBox.scss";
import { IForce, Weight } from "./PhysicsSimulationWeight";
import {Wall, IWallProps } from "./PhysicsSimulationWall"
import {Wedge} from "./PhysicsSimulationWedge"
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 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 [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, 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);
}
};
// 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();
}
}
}, [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 && (
)}
{!simulationPaused && (
)}
{simulationPaused && (
)}
{/* {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;