Skip to content

Commit 2d62cbf

Browse files
committed
Finished OOP section, started physics section
1 parent 6090c23 commit 2d62cbf

File tree

3 files changed

+420
-10
lines changed

3 files changed

+420
-10
lines changed

Boids.html

+195-10
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,28 @@
1818
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
1919
<link href="/SimulationLabs/Stylesheets/Default.css" rel="stylesheet">
2020

21+
<script type="text/x-mathjax-config">
22+
MathJax.Hub.Config({
23+
tex2jax: {
24+
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
25+
processEscapes: true
26+
}
27+
});
28+
</script>
29+
30+
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>
31+
2132
<!-- Simulations -->
2233
<script src="/SimulationLabs/Scripts/Boids/Boids.js" defer></script>
2334
<script src="/SimulationLabs/Scripts/Boids/Circles1.js" defer></script>
2435
<script src="/SimulationLabs/Scripts/Boids/Circles2.js" defer></script>
2536
<script src="/SimulationLabs/Scripts/Boids/RainbowCircles.js" defer></script>
37+
2638
<script src="/SimulationLabs/Scripts/Boids/GrowingTrails.js" defer></script>
2739
<script src="/SimulationLabs/Scripts/Boids/RainbowTrails.js" defer></script>
40+
41+
<script src="/SimulationLabs/Scripts/Boids/WrongFPSBalls.js" defer></script>
42+
<script src="/SimulationLabs/Scripts/Boids/CorrectFPSBalls.js" defer></script>
2843
</head>
2944

3045
<body>
@@ -45,7 +60,7 @@ <h2> What you'll create </h2>
4560

4661
<p> The aim of this workshop is to recreate boids, which are a simulation of birds and other flocking entities (like fishes).
4762
Hopefully, you'll be able to apply this in your own projects!
48-
Along the way, we'll learn a little bit about p5.js, rendering, and physics. </p>
63+
Along the way, we'll learn a little bit about p5.js, rendering, and simulating physics. </p>
4964

5065
<div id="BoidContainer"></div>
5166

@@ -70,13 +85,19 @@ <h2> Introduction to p5.js </h2>
7085
</ol>
7186

7287
<h3> What is a sketch? </h3>
73-
<emph>Insert brief description (couple lines) on what a sketch is, and what frames are... </emph>
88+
89+
<p>A sketch is a small program that will work with p5.js to produce a simulation.
90+
Just like a game, your simulation will have frames (an image of your simulation) and p5.js will aim to show 60 frames a second.
91+
You'll be able to code what happens just before a frame is shown, and also code what happens just before the simulation starts!
92+
</p>
93+
94+
<br>
7495

7596
<p> Your new sketch should have 2 functions: </p>
7697

7798
<ul>
7899
<li> <strong> setup() </strong> which is a function executed once at the start </li>
79-
<li> <strong> draw() </strong> which is a function executed every frame (by default, about 60 times a second) </li>
100+
<li> <strong> draw() </strong> which is a function executed every frame </li>
80101
</ul>
81102

82103
We create the canvas (arguments being the width and length) in <strong> setup() </strong> since we only need to do this once, and <strong>background()</strong> (which you can call with RGB colour arguments as below) is called every frame in <strong> draw()</strong>!
@@ -179,7 +200,38 @@ <h2> Using Arrays and Objects </h2>
179200
<li>An object representing a circle, which stores the position and size of the circle </li>
180201
</ul>
181202

182-
<p> Let's create a class for that object first! </p>
203+
<p> Let's get the growing circle part out of way; this part isn't important, but the rest of the code makes less sense without it: </p>
204+
205+
<pre>
206+
<code class="language-js">
207+
function setup() {
208+
createCanvas(400, 400);
209+
}
210+
211+
const maxCircleSize = 15;
212+
const minCircleSize = 5;
213+
let circleSize = 5;
214+
215+
function draw() {
216+
background(222, 222, 222);
217+
218+
// update circleSize
219+
circleSize++;
220+
221+
if(circleSize > maxCircleSize){
222+
circleSize = minCircleSize;
223+
}
224+
225+
// draw circle
226+
circle(mouseX, mouseY, circleSize);
227+
}
228+
</code>
229+
</pre>
230+
<em>Could also use modulo to loop the circleSize, but the if statement is more readable here</em>
231+
232+
<p>Don't worry about how this is performed; it's more important that you understand how objects, classes, and arrays work in JavaScript. </p>
233+
234+
<p> Now let's create a class for the circle! </p>
183235

184236
<h3> Coding the Class </h3>
185237

@@ -244,7 +296,6 @@ <h3> Storing the trail </h3>
244296

245297
<p>We can declare an array for the trail and a trail length pretty easily: </p>
246298

247-
248299
<pre>
249300
<code class="language-js">
250301
const trails = [];
@@ -255,13 +306,17 @@ <h3> Storing the trail </h3>
255306
<p> Note <strong>const</strong> here doesn't mean an uneditable array; you can insert and remove from the array, but you can't reassign the <em>trails</em> variable to something else.
256307
It will forever be that array, but you can edit the array as you wish. </p>
257308

258-
<p> Now let's create a method updateTrail to add the current circle to the trail; we'll have a variable <em>circleSize</em> for the current size of the circle that we can reference. </p>
309+
<p> Now let's create a method updateTrail to add the current circle to the trail; remember that we have <em>circleSize</em> from the growing circle part of the sketch. </p>
259310

260311
<br>
261312
<p>Our method will need to:</p>
262313
<ul>
263-
<li></li>
314+
<li>Create a new trail object, and add it to the array</li>
315+
<li>If the array is full, then remove the oldest circle in the array</li>
264316
</ul>
317+
318+
<p> I'll arbitrarily decide that the circles will be added to the end of the array, so therefore the oldest circles will be in the front of the array. Here's one implementation of the specification above:</p>
319+
265320
<pre>
266321
<code class="language-js">
267322
function updateTrail(){
@@ -276,17 +331,147 @@ <h3> Storing the trail </h3>
276331
</code>
277332
</pre>
278333

279-
<p>You can see JavaScript's array methods <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift">here</a></p>
334+
<p>This might be a little confusing because of the use of <em>shift()</em> and <em>push()</em>; they're just JavaScript methods to make our lives easier.
335+
336+
You can see JavaScript's array methods <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift">here</a>!
337+
</p>
338+
339+
<p> We do need to call updateTrail() in draw; let's call it just after we draw the current circle. </p>
340+
341+
<pre>
342+
<code class="language-js">
343+
function draw() {
344+
background(222, 222, 222);
345+
346+
// update circleSize
347+
circleSize++;
348+
349+
if(circleSize > maxCircleSize){
350+
circleSize = minCircleSize;
351+
}
352+
353+
// draw circle
354+
circle(mouseX, mouseY, circleSize);
355+
356+
// update stuff
357+
updateTrail();
358+
}
359+
</code>
360+
</pre>
361+
<em>Our updated draw method</em>
362+
363+
<br>
364+
365+
<p> Okay great, now we just need to display the trail! </p>
366+
367+
<h3> Displaying the trail</h3>
280368

281-
Useful: use framerate to debug
369+
<p>All we have to do is loop through our array of trail circles and display each one. I want to draw the oldest first, so I'll iterate from the start to the end of the array. </p>
370+
371+
<pre>
372+
<code class="language-js">
373+
function drawTrail(){
374+
for(let i = 0; i &lt; trails.length; i++){
375+
let trailObj = trails[i];
376+
377+
// draw the circle!
378+
trailObj.draw();
379+
}
380+
}
381+
</code>
382+
</pre>
383+
384+
<p> And now we'll call <em>drawTrail()</em> before the current circle (so we don't draw the trail over the current circle!) </p>
385+
386+
<pre>
387+
<code class="language-js">
388+
function draw() {
389+
background(222, 222, 222); // background drawn every frame!
390+
391+
// update circleSize
392+
circleSize++;
393+
394+
if(circleSize > maxCircleSize){
395+
circleSize = minCircleSize;
396+
}
397+
398+
// draw stuff
399+
drawTrail();
400+
circle(mouseX, mouseY, circleSize);
401+
402+
// update stuff
403+
updateTrail();
404+
}
405+
</code>
406+
</pre>
407+
<em>Our final version of draw()!</em>
408+
409+
<h3> Full Code for Growing Trails </h3>
410+
411+
<p> You can have a look at the full program <a href="https://editor.p5js.org/RexMortem/sketches/hJOgrWgry">here</a></p>
282412

283413
<h3> Task: Rainbow Trails </h3>
284414

415+
<p> With the help of the Growing Trails code, modify your solution to the task Multicolour to create rainbow trails. </p>
416+
417+
<p> You should keep the colour changing code, and store the previous 10 or so circles! Your simulation should look something like this:</p>
418+
285419
<div id="RainbowTrailsContainer"></div>
286420

421+
<p><strong>Tip:</strong> Once you've finished your sketch, sometimes it's a little hard to see whether the trail circles have the colour and position that they should. You can verify this more easily by slowing down your simulation! Use <em>frameRate(10)</em> in setup to set your sketch to run at 10 frames per second. </p>
422+
287423
<h2> May the Force be with you </h2>
288424

289-
<p> To make our boids move, </p>
425+
<p> To make our boids move, we're going to use forces. We know from physics that $F=ma$, but we're going to simplify things by ignoring mass and jump straight to changing acceleration! </p>
426+
427+
<p> Let's try modelling a simple force; a constant acceleration to the right. </p>
428+
429+
<h3> Accelerating Ball </h3>
430+
431+
<p> First, let's establish a class for this ball to make things clear to ourselves. </p>
432+
433+
<pre>
434+
<code class="language-js">
435+
class Ball {
436+
constructor(){
437+
this.position = createVector(0,0);
438+
this.velocity = createVector(0,0);
439+
this.acceleration = createVector(0,0);
440+
}
441+
442+
draw(){
443+
circle(this.position.x, this.position.y, 10);
444+
}
445+
}
446+
</code>
447+
</pre>
448+
449+
<p> This is all stuff we've basically done, but we also need a method to simulate all the physics.
450+
From physics, we know that acceleration is the rate of change of velocity, and that velocity is the rate of change of position.
451+
</p>
452+
453+
<br>
454+
455+
<p> So to simulate our ball's motion, we would: </p>
456+
457+
<ol>
458+
<li> Update the velocity with acceleration </li>
459+
<li> Update the position with velocity </li>
460+
</ol>
461+
462+
<p> As we'll soon see, we have to do this in a <em>very careful</em> way. </p>
463+
464+
<h3>Enter deltaTime</h3>
465+
466+
<p>Until now, we've not really considered how long each frame takes.
467+
It'd be reasonable to assume each frame takes about 1/60 seconds to process <em>however</em> each frame takes a slightly different amount of time and, when simulations get more complex, each frame might take a non-slight different amount of time!
468+
469+
</p>
470+
471+
<div id="WrongFPSBallsContainer"></div>
472+
473+
<div id="CorrectFPSBallsContainer"></div>
474+
290475

291476
<a href="https://p5js.org/reference/p5/deltaTime/"> delta time docs </a>
292477

Scripts/Boids/CorrectFPSBalls.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
let correctFPSBalls = new p5((sk) => {
2+
class PhysicsBall {
3+
constructor(framesToSkip){
4+
this.position = sk.createVector(0,0);
5+
this.velocity = sk.createVector(0,0);
6+
this.acceleration = sk.createVector(0,0);
7+
this.colour = sk.color(0,0,0);
8+
9+
this.nFrames = framesToSkip + 1; // bad names but only I have to look at this
10+
this.i = 0;
11+
this.timeAccum = 0;
12+
}
13+
14+
reset(){
15+
this.timeAccum = 0;
16+
this.i = 0;
17+
}
18+
19+
checkInCanvas(){
20+
if(this.position.x > 400){
21+
this.position.x = (this.position.x % 400);
22+
}
23+
}
24+
25+
drawMarker(){
26+
sk.stroke(this.colour);
27+
sk.line(this.position.x + 15, 0, this.position.x + 15, 400);
28+
}
29+
30+
correctPhysics(){
31+
this.position.add(p5.Vector.mult(this.velocity, this.timeAccum * 0.5));
32+
this.velocity.add(p5.Vector.mult(this.acceleration, this.timeAccum));
33+
this.position.add(p5.Vector.mult(this.velocity, this.timeAccum * 0.5));
34+
35+
this.checkInCanvas();
36+
}
37+
38+
simulate(dt){
39+
this.i++;
40+
this.timeAccum += dt;
41+
42+
if(this.i == this.nFrames){ // frames 0-(i-1) are skipped; frame i drawn
43+
this.correctPhysics();
44+
this.drawMarker();
45+
this.reset();
46+
}
47+
}
48+
49+
draw(){
50+
sk.circle(this.position.x, this.position.y, 30);
51+
}
52+
}
53+
54+
let b1;
55+
let b2;
56+
let running = false;
57+
58+
function resetSimulation(){
59+
b1 = new PhysicsBall(0); // skipping no frames
60+
b2 = new PhysicsBall(5); // skipping 5 frames, for every drawn frame
61+
62+
b1.colour = sk.color(0, 255, 0);
63+
b2.colour = sk.color(255, 0, 0);
64+
65+
b1.position = sk.createVector(50, 50);
66+
b2.position = sk.createVector(50, 150);
67+
68+
b1.acceleration = sk.createVector(2,0);
69+
b2.acceleration = sk.createVector(2,0);
70+
}
71+
72+
sk.setup = () => {
73+
let cnv = sk.createCanvas(400, 200);
74+
cnv.parent("CorrectFPSBallsContainer");
75+
76+
resetSimulation();
77+
78+
// pre-click state
79+
sk.background(220);
80+
b1.draw();
81+
b2.draw();
82+
83+
sk.textSize(60);
84+
sk.text('Click to start', 20, 120);
85+
sk.textSize(30); // for the fps text
86+
}
87+
88+
sk.draw = () => {
89+
if(running){
90+
sk.background(220);
91+
sk.text('60 FPS', 250, 50);
92+
sk.text('10 FPS', 250, 150);
93+
94+
b1.simulate(sk.deltaTime*0.001);
95+
b2.simulate(sk.deltaTime*0.001);
96+
97+
sk.stroke(0,0,0);
98+
b1.draw();
99+
b2.draw();
100+
}
101+
}
102+
103+
sk.mouseClicked = () => {
104+
if(((0 <= sk.mouseX) && (sk.mouseX <= 400)) && ((0 <= sk.mouseY) && (sk.mouseY <= 200))){
105+
running = !running;
106+
resetSimulation();
107+
}
108+
}
109+
});

0 commit comments

Comments
 (0)