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 && (

SIMULATION IN PROGRESS

)}
{showForces && currentForceSketch && simulationPaused && (
)} {showForces && forceSketches.length > 0 && simulationPaused && forceSketches.map((element: VectorTemplate, index) => { return (
{ if (deleteMode) { deleteForce(element); } else { editForce(element); } }} />
); })} {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;