Documentation on how the simulation works so that you can add to it.
View Simulation »
Table of Contents
Because electric fields cannot be touched or seen, simulations are often utilized to build students' understanding of them by providing them with a visual experience of electric fields and the motion of test charges through them. The objective of the simulation is to improve students’ qualitative understanding of how electric fields are impacted by the charges around them by creating a dynamic representation of the electric field lines, field vectors, equipotential lines and the voltage created by the charges on screen. After creating a charge configuration, students can observe the motion of test charges through the electric field. The simulation was built in JavaScript so it will run on most browsers on a computer or mobile device. The simulation is intended to be used by college students taking introductory physics courses. The core principles of the simulation will then be used to create a game. Although the Learning objectives of the game and the simulation will be almost identical, we hope that the game will motivate students to build their intuition on how electric fields work outside of class in their own time. The effectiveness of the simulation and game will be analyzed after students test both in an introductory physics class.
Students will be able to:
- Predict the general shape and direction of electric field lines for a system of charges
- Relate the shape of equipotential lines to the shape of electric field lines
- Compare the curve of the electric field lines to the trajectory of a test charge
The entire simulation runs inside 2 canvases in a webpage, so it is accessible from both mobile phones and computers. HTML and a little CSS are used to set up the page then p5.js creates the canvas with the game loop and everything else. Everything else is done in Javascript.
The simulation is primarily designed to target the learning goals listed above. There are different mods that display different inforamtion about the configuration of charges shown on then screen.
Object-oriented programming is used throughout the simulation. The Button, Charge, FieldLine, CheckBox, TestCharge, fieldVector, PointCharge and other classes can be found in own their self-titled files. Functions that primarily only use that one class can also be found in that classes self-titled file.
The p5.js library is inside the file titled p5.min.js. It should not be tampered with. The library creates the game loop and has useful Vector math functions.
There is a file called variables.js that has all global variables in it. They can technically be declared anywhere but this is a little more organized.
JavaScript has inbuild functions that can be done to arrays that are used throughout this game. Learn more about them here: Read more on that here. The 2 canvases are layered on top of eachother and the background gradient is always blured with css. This is the canvas that the voltage is displayed on. Everything else is in the foreground canvas. Anytime you want to call an inbuilt p5 function, you have to specify which canvas you will call it from because p5 is no longer in a global mode.
Because the simulation uses 2 canvases, they each run on their own instance of p5.js. This is called Instance Mode https://p5js.org/examples/instance-mode-instantiation.html
When the page is first loaded, each canvas will look for its preload(), setup() and draw() functions. They are run in that order and are all indepentant from eachother.
The preload() function is used to handle asynchronous loading of external files in a blocking way. If a preload function is defined, setup() will wait until any load calls within have finished. This is where all of the images and fonts are moved to the user's RAM for later usage.
The setup() function is called once when the program starts. It's used to create the canvas tag which is not included in the HTML file wih the rest of the HTML tags. There can only be one setup() function for each program and it shouldn't be called again after its initial execution.
Called directly after setup(), the draw() function continuously executes the lines of code contained inside its block until the program is stopped. The number of times draw() executes in each second may be controlled with the frameRate() function.
All of these functions can be found a the top of the main.js file.
Buttons are used in the side panel and the right click menu.
This is how a button is created:
new Button({
position: canvas.createVector(100, 255), // x y position of the button
width: 200, // integer
height: 200, // integer
text: "Single", // String
image: icons[1] // optional - will replace the text with an image
onClick: function(){ createPreset('single') } // function will run when button is clicked
})
CheckBoxes are used in the side panel.
This is how a CheckBox is created:
new CheckBox({
position: canvas.createVector(100, 255), // x y position of the CheckBox
width: 200, // integer
height: 200, // integer
text: "Show Grid", // String
value: true, // boolean
onClick: function(){createGrid = this.value;} // function will run when checkbox is clicked
})
This is how a charge is created.
class Charge
{
constructor(x, y, charge)
{
this.x = x;
this.y = y;
this.position = createVector(x,y);
this.charge = charge || 0;
this.selected = true;
this.dragging = false;
}
display()
{
let charge = this;
push();
strokeWeight(2);
if (charge.selected)
{
stroke(255);
charge.charge = slider.value();
}
else
{
noStroke();
}
if (charge.charge > 0){ fill(chargeColor.positive); }
else if (charge.charge == 0){ fill(chargeColor.neutral); }
else { fill(chargeColor.negative); }
ellipse(charge.x, charge.y, chargeDiameter, chargeDiameter);
textSize(16);
if (charge.charge > 0){ fill(textColor.positive); }
else if (charge.charge == 0){ fill(textColor.neutral); }
else { fill(textColor.negative); }
noStroke();
if (charge.charge > 0)
{
text(`+${charge.charge}`, charge.x, charge.y + 7);
}
else
{
text(charge.charge, charge.x, charge.y + 7);
}
pop();
}
}
Physics concepts related to
and superimposed electric fields given by are used to run the game.There is a netForceAtPoint() function in game.js that is given an x-y position vector as an input and outputs a vector with x and y components. It does this using Coulomb's Law and trig.
function netForceAtPoint(position) // the position comes in in a createVector(x,y) format
{
let finalVector = createVector(0,0); // starts with a 0 vector
charges.forEach(charge => // calculates force from each charge and adds them to the finalVector variable
{
let chargePosition = createVector(charge.x, charge.y); //position of charge object
//F = KQ / (r^2) // k is a constant that is used to fine tune the size of the force vector
let kq = charge.charge * k; // q is the magnitude and sign of the charges charge
let r = p5.Vector.dist(position, chargePosition); // gets the distance from the charge to the point in pixels
if (r < 10)
{ // this prevents the radius from being too small
r = 10; // and keeps the net force capped at a resonable size.
} // also prevents dividing by zero
let rSquared = Math.pow(r,2);
//F = KQ / (r^2) // coulombs law
let force = kq / rSquared; // magnitude of force
let theta = chargePosition.sub(position).heading(); // angle from point to charge
let forceX = force * cos(theta);
let forceY = force * sin(theta);
let forceVector = createVector(forceX, forceY).mult(-1); // force from the one charge
finalVector.add(forceVector); // adds the one force to the net force
});
return finalVector; // returns net force in a vector format
}
This function is used to move test charges by calculating the net force on the charge every frame and using euler's method to translate the net force to an acceleration to a velocity to an x-y position on the screen. This is how Euler's method is implimented with code
let force = netForceAtPoint(testCharge.position);
if (force.mag() != Infinity && testCharge.moving) // if the distance between two points is zero, the magnitude of the
{ // force would be infinity. testCharge.moving is true when the gamemode
// F = qE // is "Play"
// ma = qE
// a = (qE)/m
// m = 1
// a = qE
testCharge.acceleration = force.mult(testCharge.charge); // E = force and q = testCharge.charge
testCharge.velocity.add(testCharge.acceleration); // the next two lines are eulers method
testCharge.position.add(testCharge.velocity);
}
The netForceAtPoint() function seen above is also used to draw field lines by creating a starting point inside a charge and converting the net force at that point into a unit vector of length 5 pixels. Recursion is then used to keep adding a new force unit vector to the tip of the previous one until one end of the vector has collided with a charge. Each starting point begins at a new position around the charge.
The getFieldLinePoints() function in fieldLines.js has 3 inputs. The first input is an x position and the second input is a y position. The third input is the position of the charge in the charges array that the field line will come out of. When the field line either hits a charge with a different index in the charges array or is very far from it's original starting point, then field line is finished. The function will return an array of points that can be connected together in order to make a field line.
The createFieldLines() funcion will create all of the field lines necessary for the configuration of charges on screen.
charges.forEach((charge, i) => // loops through each charge in the charges array and gives it an index i
{
fieldLines[i] = [];
let radius = chargeRadius + 1; // the starting point for field lines is one pixel outside the circle that makes up a charge
// this is so the field line is not colliding with itself and stopping the recursion
let times = Math.abs(charge.charge) * fieldLinesPerCoulomb; // the number of field lines coming out of a charge grows with the charges magnitude
let origin = charge.position; //the place the field lines originae from
let point = createVector(0, radius);
for (let a = 0; a < times; a++)
{
getFieldLinePoints(point.x + origin.x, point.y + origin.y, i); // this function gets an array of points that make up the field like starting at the field lines origin
point.rotate(360/times); // this rotates the starting point of the field line around the charge in accordance to the charges magnitude
}
});
function getFieldLinePoints(x, y, baseCharge)
{
let position = createVector(x,y);
let forceVector = netForceAtPoint(position);
forceVector.setMag(chargeRadius);
let forceVectorFinalPosition = p5.Vector.add(forceVector, position);
let vectorToChargeDistance = p5.Vector.dist(forceVectorFinalPosition, charges[0].position);
let startingPointIsInsideCharge = false;
let i = 0;
let chargesLength = charges.length;
for (i; i < chargesLength; i++)
{
let distanceFromEndOfVectorToCharge = p5.Vector.dist(position, charges[i].position);
if (distanceFromEndOfVectorToCharge < (chargeRadius) && charges[i].charge != 0)
{
startingPointIsInsideCharge = true;
}
}
if (!startingPointIsInsideCharge && vectorToChargeDistance < windowSize)
{
try
{
points.push(position);
getFieldLinePoints(forceVectorFinalPosition.x, forceVectorFinalPosition.y, baseCharge);
}
catch (e)
{
//console.log(e);
}
}
else
{
points.unshift(charges[baseCharge].position);
let chargeDistances = [];
for (let i = 0; i < charges.length; i++)
{
chargeDistances.push(charges[i].position.dist(points[points.length - 1]));
}
let closestChargeDistance = Math.min(...chargeDistances);
for (let i = 0; i < chargeDistances.length; i++)
{
if (chargeDistances[i] == closestChargeDistance && closestChargeDistance < 100)
{
let halfWayPoint = points[points.length - 1].add(charges[i].position).div(2);
points.push(halfWayPoint);
halfWayPoint = points[points.length - 1].add(charges[i].position).div(2);
points.push(halfWayPoint);
points.push(p5.Vector.add(charges[i].position, createVector(1, 0)));
}
}
fieldLines.push(new FieldLine(points));
points = [];
}
}
- Fork the Project
- Create your Feature Branch
- Commit your Changes
- Push to the Branch
- Open a Pull Request
Dr. Colleen Countryman - Assistant Professor - [email protected]
Project Link: https://github.com/tedkmburu/E-FieldSim-v2
-
Dr. Colleen Countryman
-
Ted Mburu
-
Ithaca College Physics & Astronomy Department
-
Ithaca College IT
-
P5.js (p5js.org)
-
Daniel Shiffman (The Coding Train)