From 22d9e3920ba95a3dc29dca5fcdcbabfc3bebf38f Mon Sep 17 00:00:00 2001
From: shiffman If object-oriented programming is at all new to you, one aspect here may seem a bit strange. I spent the beginning of this chapter discussing the In fact, this is just about the most normal thing ever. An object is something that holds data (and functionality). That data can be numbers, or it can be other objects (arrays too)! You’ll see this over and over again in this book. In Chapter 4 , for example, I’ll write a class to describe a system of particles. That In fact, this is just about the most normal thing ever. An object is something that holds data (and functionality). That data can be numbers, or it can be other objects (arrays too)! You’ll see this over and over again in this book. In Chapter 4 , for example, I’ll write a class to describe a system of particles. That At this point, you hopefully feel comfortable with two things: (1) what a vector is, and (2) how to use vectors inside of an object to keep track of its position and movement. This is an excellent first step and deserves a mild round of applause. Before standing ovations are in order, however, you need to make one more, somewhat bigger step forward. After all, watching the Motion 101 example is fairly boring. The circle never speeds up, never slows down, and never turns. For more sophisticated motion—the kind of motion that appears in the world around us—one more vector needs to be added to the class: Acceleration is the rate of change of velocity. Think about that definition for a moment. Is it a new concept? Not really. Earlier I defined velocity as the rate of change of position, so in essence I’m developing a “trickle-down” effect. Acceleration affects velocity, which in turn affects position. (To provide some brief foreshadowing, this point will become even more crucial in the next chapter, when I look at how forces like friction affect acceleration, which affects velocity, which affects position.) In code, this trickle-down effect reads: In the final example of Chapter 1, I demonstrated how to calculate a dynamic acceleration based on a vector pointing from a circle on the canvas to the mouse position. The resulting motion resembled a magnetic attraction between shape and mouse, as if some force was pulling the circle in toward the mouse. In this chapter, I’ll detail the concept of a force and its relationship to acceleration. The goal, by the end of this chapter, is to build a simple physics engine and understand how objects move around a canvas responding to a variety of environmental forces. A physics engine is a computer program (or code library) that simulates the behavior of objects in a physical environment. With a p5.js sketch, the objects are two-dimensional shapes, and the environment is a rectangular canvas. Physics engines can be developed to be highly precise (requiring high-performance computing) or real-time (using simple and fast algorithms). This chapter will focus on building a rudimentary physics engine, with a focus on speed and ease of understanding. Let’s begin by taking a conceptual look at what it means to be a force in the real world. Just like the word “vector,” the term “force” can have a variety of meanings. It can indicate a powerful physical intensity, as in “They pushed the boulder with great force,” or a powerful influence, as in “They’re a force to be reckoned with!” The definition of force that I’m interested in for this chapter is more formal and comes from Sir Isaac Newton’s three laws of motion: A force is a vector that causes an object with mass to accelerate. Polar to Cartesian conversion is common enough that p5.js includes as handy function to take care of it for you. It’s included as a static method of the Are you amazed yet? I’ve demonstrated some pretty great uses of tangent (for finding the angle of a vector) and sine and cosine (for converting from polar to Cartesian coordinates). I could stop right here and be satisfied. But I’m not going to. This is only the beginning. As I’ll show you next, what sine and cosine can do for you goes beyond mathematical formulas and right triangles. “That is wise. Were I to invoke logic, however, logic clearly dictates that the needs of the many outweigh the needs of the few.” — Spock In 1982, William T. Reeves, a researcher at Lucasfilm Ltd., was working on the film Star Trek II: The Wrath of Khan. Much of the movie revolves around the Genesis Device, a torpedo that when shot at a barren, lifeless planet has the ability to reorganize matter and create a habitable world for colonization. During the sequence, a wall of fire ripples over the planet while it is being “terraformed.” The term particle system, an incredibly common and useful technique in computer graphics, was coined in the creation of this particular effect. In 1982, William T. Reeves, a researcher at Lucasfilm Ltd., was working on the film Star Trek II: The Wrath of Khan. Much of the movie revolves around the Genesis Device, a torpedo that, when shot at a barren, lifeless planet, has the ability to reorganize matter and create a habitable world for colonization. During the sequence, a wall of fire ripples over the planet while it’s being “terraformed.” The term particle system, an incredibly common and useful technique in computer graphics, was coined in the creation of this particular effect. As Reeves put it: “A particle system is a collection of many many minute particles that together represent a fuzzy object. Over a period of time, particles are generated into a system, move and change from within the system, and die from the system.” —William Reeves, "Particle Systems—A Technique for Modeling a Class of Fuzzy Objects," ACM Transactions on Graphics 2:2 (April 1983), 92. —William Reeves, “Particle Systems—A Technique for Modeling a Class of Fuzzy Objects,” ACM Transactions on Graphics 2, no. 2 (April 1983): 92. Since the early 1980s, particle systems have been used in countless video games, animations, digital art pieces, and installations to model various irregular types of natural phenomena, such as fire, smoke, waterfalls, fog, grass, bubbles, and so on. This chapter is dedicated to looking at implementation strategies for coding a particle system. How do you organize your code? Where do you store information related to individual particles versus information related to the system as a whole? The examples I’ll cover will focus on managing the data associated with a particle system. They’ll use simple dots for the particles and apply only the most basic behaviors (such as gravity). However, by building on this framework and adding more creative ways to render the particles and compute behaviors, you can achieve a variety of effects. I’ve defined a particle system to be a collection of independent objects, often represented by a simple shape or dot. Why does this matter? Certainly, the prospect of modeling some of the phenomena listed (waterfalls!) is attractive and potentially useful. But really, there’s an even better reason to explore particle systems. If you want to get anywhere in this nature of code life, you’re likely to find yourself developing systems of many things–balls bouncing, birds flocking, ecosystems evolving, all sorts of things in plural. Just about every chapter after this one is going to deal with a list of objects. Yes, I’ve dipped my toe in the array waters in some of the first vector and forces examples. But now it‘s time to go where no array has gone before. First, I’m going to want to deal with flexible quantities of elements. Some examples will have zero things, sometimes one thing, sometimes ten things, and sometimes ten thousand things. Second, I’m going to want to take a more sophisticated object-oriented approach. Instead of writing a class to describe a single particle, I’m also going to want to write a class that describes the collection of particles—the particle system itself. The goal here is to be able to write a sketch that looks like the following: This chapter is dedicated to looking at strategies for coding a particle system and managing the associated data. How do you organize your code? Where do you store information related to individual particles versus information related to the system as a whole? The examples I’ll cover will use simple dots for the particles and apply only the most basic behaviors. However, the fact that the particles I’ll generate look or behave a certain way shouldn’t limit your imagination. Just because particle systems tend to look sparkly, fly forward, or fall with gravity doesn’t mean that those are the characteristics yours should have, too. By building on this chapter’s framework and adding more creative ways to render the particles and compute their behavior, you can achieve a variety of effects. In other words, the focus of this chapter is on how to keep track of a system of many elements. What those elements actually do and how they actually look is entirely up to you. A particle system is a collection of independent objects, often represented by dots or other simple shapes. But why does this matter? Certainly, the prospect of modeling some of the phenomena listed (waterfalls!) is attractive and potentially useful. More broadly, though, as you start developing more sophisticated simulations, you’re likely to find yourself working with systems of many things—balls bouncing, birds flocking, ecosystems evolving, all sorts of things in plural. The particle system strategies discussed here will help you in all of those situations. In fact, just about every chapter from this one on is going include sketches involving lists of objects, and that’s basically what a particle system is. Yes, I’ve already dipped my toe in the array waters in some of the previous chapters’ examples. But now it’s time to go where no array has gone before (in this book, anyway). First, I’m going to want to accommodate flexible quantities of elements. Some examples may have zero things, sometimes one thing, sometimes ten things, and sometimes ten thousand things. Second, I’m going to want to take a more sophisticated, object-oriented approach. In addition to writing a class to describe a single particle, I’m also going to want to write a class that describes the whole collection of particles—the particle system itself. The goal here is to be able to write a sketch that looks like this: No single particle is referenced in the above code, yet the result will be full of particles flying all over the canvas. Getting used to writing sketches with multiple classes, and classes that keep lists of instances of other classes, will prove very useful as you get to later chapters in this book. Finally, working with particle systems is also a good excuse to tackle two other object-oriented programming techniques: inheritance and polymorphism. With the examples you’ve seen up until now, I’ve always used an array of a single type of object, like "movers" or “oscillators.” With inheritance (and polymorphism), I’ll demonstrate a convenient way to store a single list containing objects of different types. This way, a particle system need not only be a system of a one kind of particle. Though it may seem obvious to you, I’d also like to point out that my examples are modeled after conventional implementations of particle systems, and that’s where I will begin in this chapter. However, the fact that the particles in this chapter look or behave a certain way should not limit your imagination. Just because particle systems tend to look sparkly, fly forward, and fall with gravity doesn’t mean that those are the characteristics yours should have. The focus here is on how to keep track of a system of many elements. What those elements do and how those elements look is up to you. Before I can get rolling on coding the system itself, I need to write the class to describe a single particle. The good news: I’ve done this already! The No single particle is referenced in this code, and yet the result will be full of particles flying all over the canvas. This works because the details are hidden inside the Finally, working with particle systems is also an opportunity to tackle two other object-oriented programming techniques: inheritance and polymorphism. With the examples you’ve seen up until now, I’ve always used an array of a single type of object, like an array of movers or an array of oscillators. With inheritance and polymorphism, I’ll demonstrate a convenient way to use a single list to store objects of different types. This way, a particle system need not only be a system of a one kind of particle. Before I can get rolling on coding the particle system itself, I need to write a class to describe a single particle. The good news: I’ve done this already! The This is about as simple as a particle can get. From here, I could take the particle in several directions. I could add the Some particle systems involve something called an emitter. The emitter is the source of the particles and controls the initial settings for the particles: position, velocity, and more. An emitter might emit a single burst of particles, or a continuous stream of particles, or both. The new feature here is that a particle born at the emitter does not live forever. If it were to live forever, the p5.js sketch would eventually grind to a halt as the amount of particles increases to an unwieldy number over time. As new particles are born, old particles need to be removed. This creates the illusion of an infinite stream of particles, and the performance of the sketch does not suffer. There are many different ways to decide when a particle is ready to be removed. For example, it could come into contact with another object, or it could leave the canvas. For this first This is about as simple as a particle can get. From here, I could take the particle in several directions. I could add the Some particle systems involve something called an emitter that serves as the source of the particles. The emitter controls the initial settings for the particles: position, velocity, and more. It might emit a single burst of particles or a continuous stream of particles, or some variation thereof. The new feature here is that particles born at the emitter can’t live forever. If they did, the p5.js sketch would eventually grind to a halt as the amount of particles increases to an unwieldy number over time. As new particles are born, old particles need to be removed, creating the illusion of an infinite stream of particles without hurting the performance of the sketch. There are many different ways to decide when a particle is ready to be removed. For example, it could “die” when it comes into contact with another object, or when it leaves the frame of the canvas. For now, I’ll choose to give particles a The reason I chose to start the lifespan at 255 and count down to 0 is for convenience. With those values, I can assign With the addition of the With With the addition of the Or more simply, I can return the result of the boolean expression itself! Even more simply, I can just return the result of the Boolean expression itself! Before I get to the next step of making many particles, it’s worth taking a moment to make sure the particle works correctly and create a sketch with one single Before I get to the next step of making many particles, it’s worth taking a moment to confirm the particle works correctly. For that, I’ll create a sketch featuring a single In this example, there is only one particle at a time for the sake of simplicity and testing. Each time the particle reaches the end of its lifespan, the Create a Create a Add angular velocity (rotation) to the particle. Create your own non-circle particle design. Add angular velocity (rotation) to the particle, and create your own non-circle particle design so the rotation is visible. Now that I have a class to describe a single particle, it’s time for the next big step. How do you keep track of many particles, not knowing exactly how many particles you might have at any given time? Thankfully, the wonderful JavaScript Now that I have a class to describe a single particle, it’s time for the next big step: how can I keep track of many particles, without knowing in advance exactly how many I might have at any given time? The answer is the JavaScript array, a data structure that stores an arbitrarily long list of values. In JavaScript, an array is actually an object created from the As I bring arrays into the picture, I’ll use a solution to Exercise 4.1 and assume a This last The Let’s take a ride on the loop-de-loop rollercoaster of choices for iterating over an array: Here's how the Let’s translate that. Say “each” instead of “let” and “in” instead of “of”: “For each particle in particles, update and display that particle!” I know. You cannot contain your excitement. I can’t. I know it’s not necessary, but I just have to type that again. Simple, elegant, concise, lovely. Take a moment. Breathe. I have some bad news. Yes, I may love that The code I’ve written above doesn’t take advantage of the JavaScript's ability to remove elements from an array. I need to build an example that fits with the particle system scenario, where a continuous stream of particles are emitted, adding one new particle with each cycle through To translate this code, say “each” instead of “let” and “in” instead of “of.” Putting it together, you get, “For each particle in particles, update and display that particle.” Simple, elegant, concise, lovely. But before you get too excited about Creating a new particle every frame is easy: I can just call the Run the above code for a few minutes and you’ll start to see the frame rate slow down further and further until the program grinds to a halt (my tests yielded horrific performance after fifteen minutes). The issue of course is that I am adding more and more particles without removing any. Fortunately, particles can be removed from the array referencing the index position of the particle to be removed. This is why I cannot use the enhanced Run this code for a few minutes and you’ll start to see the frame rate slow down further and further until the program grinds to a halt. (My tests yielded horrific performance after 15 minutes.) The issue, of course, is that I’m adding more and more particles without removing any. To fix this, I can use the Although the above code will run just fine (and the program will never grind to a halt), I have opened up a medium-sized can of worms. Whenever you manipulate the contents of an array while iterating through that very array, you can get into trouble. Take, for example, the following code. Although this code will run just fine and never grind to a halt, I’ve opened up a medium-sized can of worms by trying to manipulate the contents of an array while iterating through that very same array. This is just asking for trouble. Take, for example, the following code: This is a somewhat extreme example (with flawed logic), but it proves the point. In the above case, for each particle in the list, I add a new particle to the list (and the While removing elements from the array during a loop doesn’t cause the sketch to crash (as it would with adding), the problem is perhaps more insidious in that it leaves no evidence. To discover the flaw I must first establish an important fact. When an element is removed from the array with This is a somewhat extreme example (with flawed logic), but it proves the point. For each particle in the list, this code adds a new particle to the list, and so the While removing elements from the array during a loop doesn’t cause the sketch to crash (as it would with adding), the problem is perhaps more insidious in that it leaves no evidence. To discover the flaw, I must first establish an important fact: when an element is removed from an array with Let’s consider a counter Notice the problem? Particle D was never checked! When C was deleted from slot #2, D moved into slot #2, but There are two solutions to this problem. The first solution is to iterate through the array backwards. If you are sliding elements from right to left as elements are removed, it’s impossible to skip an element. Here’s how the code looks: Consider what happens as counter Notice the problem? Particle D is never checked! When C is deleted from slot 2, D moves into slot 2 in its place, but There are two solutions to this problem. The first solution is to iterate through the array backwards. Since elements slide from right to left as other elements are removed, it’s impossible to skip an element this way. Here’s how the code looks: A second solution is to use something known as a “higher-order” function. A higher-order function is one that receives another function as an argument (or returns a function). In the case of JavaScript arrays there are many higher-order functions. A common one is A second solution is to use a higher-order function. This is a function that receives another function as an argument (or provides a function as its return value). In the case of JavaScript arrays, there are many higher-order functions. For example, a common one is This is more commonly written using JavaScript's arrow notation. (To learn more, you can watch this higher-order functions and arrow notation video tutorial.) This is more commonly written using JavaScript's arrow notation. (To learn more, check out my Coding Train video tutorial on higher-order functions and arrow notation.) For the purposes of this book, I am going to stick with the For the purposes of this book, I’m going to stick with the OK. Now I’ve done two things. I’ve written a class to describe an individual I could stop here. However, one additional step I can and should take is to write a class to describe the list of If you recall the goal I set at the beginning of this chapter was to You might be wondering why, instead of checking each particle individually, I don’t just remove the oldest particle after some period of time (determined by checking the frameCount or array length). In this example, that approach would actually work! I could even use a different array method called I’ve conquered the array and used it to manage a list of Recall that one of the goals I set at the beginning of this chapter was to write Let’s take the code from Example 4.2 and review a bit of object-oriented programming, looking at how each piece To get to this point, look at each piece of I could also add new features to the particle system itself. For example, it might be useful for the The example emitter is a static source of particles, which is a nice way to begin working with particle systems. However, it doesn't have to be that way! The emitter itself could have its own behaviors and experience physics, oscillate, react to user interaction, or any other kind of motion demonstrated in previous chapters. The particles could then emanate from various positions over time, creating trails and even more complex and intriguing patterns. What if the emitter moves? Can you emit particles from the mouse position or use the concepts of velocity and acceleration to move the system autonomously. What if the emitter moves? Can you emit particles from the mouse position, or use the concepts of velocity and acceleration to move the system autonomously? Building off Chapter 3’s “Asteroids” example, use a particle system to emit particles from the ship’s “thrusters” whenever a thrust force is applied. The particles’ initial velocity should be related to the ship’s current direction. Building off Chapter 3’s Asteroids example, use a particle system to emit particles from the ship’s “thrusters” whenever a thrust force is applied. The particles’ initial velocity should be related to the ship’s current direction. Let’s take a moment to recap what I’ve covered so far. I described an individual This line of thinking could take you even further, and you might lock yourself in a basement for days sketching out a diagram of a system of systems of systems of systems of systems of systems. Of systems. After all, I could create a description of the world in this way. An organ is a system of cells, a human body is a system of organs, a neighborhood is a system of human bodies, a city is a system of neighborhoods, and so on and so forth. While this is an interesting road to travel down, it’s a bit beyond where I’d like to be right now. It is, however, quite useful to know how to write a sketch that keeps track of many particle systems, each of which keep track of many particles. Take the following scenario. You start with a blank screen. So far I’ve described an individual particle and organized its code into a I could take this line of thinking even further, locking myself in a basement for days and sketching out a diagram of a system of systems of systems of systems of systems of systems . . . until I get this whole system thing out of my, well, system. After all, I could describe the world in a similar way: an organ is a system of cells, a human body is a system of organs, a neighborhood is a system of human bodies, a city is a system of neighborhoods, and so on and so forth. I’m not ready to go quite that far just yet, but it would still be useful to look at how to write a sketch that keeps track of many particle systems, each of which keeps track of many particles. Consider this scenario: you start with a blank screen. You click the mouse and generate a particle system at the mouse’s position. Each time you click the mouse, a new particle system is created at the mouse’s position. You keep clicking the mouse. Each time, another particle system springs up where you clicked. In Example 4.3, I stored a single reference to an How to do this? In Example 4.3, I stored a single reference to an For this new example, what I want to do instead is create an Now I’ll call the variable Whenever the mouse is pressed, a new Whenever the mouse is pressed, a new And in Then, in Notice how I am back to using a Rewrite Example 4.4 so that each particle system doesn’t live forever. When a particle system is empty (i.e. has no particles left), remove it from the array Rewrite Example 4.4 so each particle system doesn’t live forever. Set a limit on how many particles an individual system can generate. Then, when a particle system is empty (has no particles left), remove it from the Create a simulation of an object shattering into many pieces. How can you turn one large shape into many small particles? Can you create several large shapes on the screen that each shatter when clicked on? Create a simulation of an object shattering into many pieces. How can you turn one large shape into many small particles? Can you create several large shapes on the screen that each shatter when clicked? You may have encountered the terms inheritance and polymorphism in your programming life before this book. After all, they are two of the three fundamental principles behind the theory of object-oriented programming (the other being encapsulation). If you’ve read other programming books, chances are it’s been covered. My beginner text, Learning Processing, has close to an entire chapter (#22) dedicated to these two topics. Still, perhaps you’ve only learned about it in the abstract sense and never had a reason to really use inheritance and polymorphism. If this is true, you’ve come to the right place. Without these concepts, your ability to program a variety of particles and particle systems is extremely limited. (In the chapter 6, I’ll also demonstrate how understanding these topics will help you use physics libraries.) Imagine the following. It’s a Saturday morning, you’ve just gone out for a lovely jog, had a delicious bowl of cereal, and are sitting quietly at your computer with a cup of warm chamomile tea. It’s your old friend So and So’s birthday and you’ve decided you’d like to make a greeting card with p5. How about some confetti for a birthday? Purple confetti, pink confetti, star-shaped confetti, square confetti, fast confetti, fluttery confetti, etc. All of these pieces of confetti with different appearances and different behaviors explode onto the screen at once. What you’ve got is clearly a particle system—a collection of individual pieces of confetti (i.e. particles). You might be able to cleverly design a Up to now, all the particles in my systems have been identical, with the same basic appearance and behaviors. Who says this has to be the case? By harnessing two fundamental principles of object-oriented programming, inheritance and polymorphism, I can create particle systems with significantly more variety and interest. Perhaps you’ve encountered these two terms in your programming life before this book. For example, my beginner text, Learning Processing, has close to an entire chapter (#22) dedicated to them. Still, perhaps you’ve only learned about inheritance and polymorphism in the abstract and never had a reason to really use them. If that’s true, you’ve come to the right place. Without these techniques, your ability to program diverse particles and particle systems is extremely limited. (In Chapter 6, I’ll also demonstrate how understanding these topics will help you use physics libraries.) Imagine it’s a Saturday morning. You’ve just gone out for a lovely jog, had a delicious bowl of cereal, and are sitting quietly at your computer with a cup of warm chamomile tea. It’s your old friend So and So’s birthday, and you’ve decided you’d like to make them a greeting card with p5.js. How about simulating some confetti? Purple confetti, pink confetti, star-shaped confetti, square confetti, fast confetti, fluttery confetti—all kinds of confetti, all with different appearances and different behaviors, explode onto the screen all at once. What you’ve got is clearly a particle system: a collection of individual pieces (particles) of confetti. You might be able to cleverly redesign the This is a nice solution: create three different classes to describe the different kinds of pieces of confetti that are part of your particle system. An This is a nice solution: create three different classes to describe the different kinds of confetti that are part of your particle system. The OK, I need to pause for a moment. You’ve done nothing wrong. All you wanted to do was wish your friend a happy birthday and enjoy writing some code. But while the reasoning behind the above approach is quite sound, there’s a problem. Aren’t you going to be copying/pasting a lot of code between the different “confetti” classes? Yes. Even though the kinds of particles are different enough to merit breaking them out into separate classes, there is still a ton of code that they will likely share. They’ll all have vectors to keep track of position, velocity, and acceleration; an This is where inheritance comes in. Inheritance allows you to write a class that inherits variables and functions from another class, all the while implementing its own custom features. Let’s look at this concept in more detail and then create a particle system example that implements inheritance. Let’s take a different example, the world of animals: dogs, cats, monkeys, pandas, wombats, and sea nettles. I’ll start by coding a Let me pause for a moment. You’ve done nothing wrong. All you wanted to do was wish your friend a happy birthday and enjoy writing some code. But while the reasoning behind this approach is quite sound, there’s a problem: aren’t you going to be copy-pasting a lot of code between the different confetti classes? Yes, you probably will be. Even though the kinds of particles are different enough to merit breaking them out into separate classes, there’s still a ton of code that they’ll likely share. For example, they’ll all have vectors to keep track of position, velocity, and acceleration; an This is where inheritance comes in. Inheritance allows you to write a class that takes on (”inherits”) variables and methods from another class, while also implementing its own custom features. I’ll illustrate this concept in more detail, and then create a particle system example that implements inheritance. To demonstrate how inheritance works, I’ll take a different example from the world of animals: dogs, cats, monkeys, pandas, wombats, sea nettles, you name it. I’ll start by coding a Now, let’s move on to cats. Now I’ll make a cat. Inheritance makes this all possible. With inheritance, classes can inherit properties (variables) and functionality (methods) from other classes. A Inheritance makes this all possible, allowing Here is how the syntax works with inheritance. Here’s how the syntax of inheritance works: This brings up two new terms: A subclass can be expanded to include additional functions and properties beyond what is contained in the superclass. For example, let’s assume that a This code uses two new JavaScript features. First, notice the Second, notice the call to You can expand a subclass to include additional methods beyond those contained in the superclass. Here I’ve added the Note how the parent constructor is called via Note how the parent constructor is first called via If a But what if a dog eats the same way as a generic animal, just with some extra functionality? A subclass can both run the code from a parent class and incorporate custom code. But what if a dog eats mostly the same way as a generic animal, just with some extra functionality? A subclass can both run the code from its parent class’s method and incorporate custom code. Now that I’ve covered the theory of inheritance and its syntax, it’s time write a working example in p5.js based on the Let’s review a basic Similar to calling Now that I’ve covered the theory of inheritance and its syntax, I’m ready to write a working example of inheritance in p5.js based on the Next, I‘ll create a subclass that extends The class has variables and methods that any participant in a particle system should have. Next, I’ll create a Let’s make this a bit more sophisticated. Let’s say I want to have the I know a particle has an x position somewhere between 0 and the width of the canvas. What if I said: when the particle’s x position is 0, its rotation should be 0; when its x position is equal to the width, its rotation should be equal to 2\pi? Does this ring a bell? Whenever a value has one range that you want to map to another range, you can use the Let’s make this a bit more sophisticated. Say I want to have each I know a particle has an x position somewhere between 0 and the width of the canvas. What if I said: when the particle’s x position is 0, its rotation should be 0; when its x position is equal to the width, its rotation should be equal to 2\pi? Does this ring a bell? As discussed in the Introduction, whenever a value has one range that you want to map to another range, you can use the And just to give it a bit more spin, I can actually map the angle’s range from 0 to Here’s how this code fits into the Just to give the confetti a bit more spin, I’ve actually mapped the angle’s range from Instead of using Instead of using Now that I have a Can you spot how this example is also taking advantage of polymorphism? It's right there is how both Create a particle system with more than two “kinds” of particles. Try varying the behavior of the particles in addition to the design. So far in this chapter, I’ve focused on structuring code in an object-oriented way to manage a collection of particles. While I did keep the So far in this chapter, I’ve focused on structuring code in an object-oriented way to manage a collection of particles. While I did keep the Now that the Now that the I’d like to consider a more generic solution that would likely apply to more broadly and allow different forces to be applied to individual particles in a system. For example. what if I were to apply a force globally every time through I’d like to now consider a broader, more generic solution that will allow different forces to be applied to individual particles in a system. For example, what if I were to apply a force globally every time through Well, it seems there‘s a small problem. Well, it seems there’s a small problem. The Of course, if I call a new function on the Now in code: Of course, if I call an It almost seems silly to write this function. What the code says is “apply a force to a particle system so that the system can apply that force to all of the individual particles.” Nevertheless, it’s actually quite reasonable. After all, the emitter is in charge of managing the particles, so if you want to talk to the particles, you’ve got to talk to them through their manager. (Also, here’s a chance for the enhanced loop since no particles are being deleted!) Here is the full example (assuming the existence of the It almost seems silly to write this method. The code is essentially saying, “Apply a force to a particle system so that the system can apply that force to all of the individual particles.” Although this may sound a bit roundabout, this approach is actually quite reasonable. After all, the emitter is in charge of managing the particles, so if you want to talk to the particles, you’ve got to talk to them through their manager. (Also, here’s a chance to use an enhanced Here’s the full example, including this change. (The code assumes the existence of the What if I wanted to take this example one step further and add a While this example only demonstrates a hard-coded gravity force, it's worth considering how other forces from previous chapters, such as wind or drag, could come into play. You could also experiment with varying how and when forces are applied. Instead of a force acting on particles continuously every frame, what if a force only kicked in under certain conditions or at specific moments? There's a lot of room here for creativity and interactivity in how you design your particle systems! What if I wanted to take my code one step further and add a Let’s start by examining how I might incorporate a new To incorporate a new The more difficult question is, how do I write the The more difficult task is writing the The functions are almost identical. There are only two differences. One I mentioned before—a The methods are almost identical, but there are only two differences. I mentioned one of them before: the argument to Notice how throughout this entire process of adding a repeller to the environment, I never once considered editing the Now look at this example in its entirety, again leaving out the I’m now ready to write this example in its entirety, again leaving out the You may have noticed the addition of the Expand the above example to include more than one repeller (using an array). Expand Example 4.7 to include multiple repellers and attractors! Create a particle system in which each particle responds to every other particle. (Note that I’ll be going through this in detail in Chapter 6.) Create a particle system in which each particle responds to every other particle. (I’ll explain how to do this in detail in Chapter 5.) Even though this book is almost exclusively focused on behaviors and algorithms rather than computer graphics and design, I don’t think I would be able to live with myself if I finished a discussion of particle systems and never once looked at an example that involves texturing each particle with an image. The way you a particle is rendered is a key piece of the puzzle in designing certain types of visual effects. Consider a smoke simulation. Take a look at the following two images: Even though this book is almost exclusively focused on behaviors and algorithms rather than computer graphics and design, I don’t think I would be able to live with myself if I finished a discussion of particle systems without ever once looking at an example that involves texturing each particle with an image. After all, the way you render a particle is a key piece of the puzzle in designing certain types of visual effects. For example, compare the two smoke simulations shown in Figure 4.4. Both of these images were generated from identical algorithms. The only difference is that a white circle is drawn in image A for each particle and a “fuzzy” blob is drawn for each in B. Both of these images were generated from identical algorithms. The only difference is that each particle is drawn as a plain white circle in the image on the left, whereas each particle is drawn as a fuzzy blob in the image on the right. Figure 4.5 shows the two kinds of particle textures. The good news here is that you get a lot of bang for very little buck. Before you write any code, however, you’ve got to make your image texture! I recommend using PNG format, as p5.js will retain the alpha channel (i.e. transparency) when drawing the image, which is needed for blending the texture as particles layer on top of each other. Once you’ve made a PNG and deposited it in your sketch’s “data” folder, you are on your way with just a few lines of code. Using an image to texture your particles can give you a lot of bang for very little buck in terms of the realism of the visual effect. Before you write any code, however, you have to make your image texture. I recommend using the PNG format, as p5.js will retain the alpha channel (transparency) when drawing the image, which is needed for blending the texture as particles are layered on top of each other. There are numerous ways to create these textures: you can indeed make them programmatically within p5.js (I'll include an example with the book's supplementary materials), but you can also other open-source or commercial graphic editing tools. Once you’ve made a PNG and deposited it in your sketch’s data folder, you only need a few extra lines of code. First, we’ll need to a variable to store the image and load it in First, declare a variable to store the image. Load the image in Then, load the image in And when it comes time to draw the particle, use the image variable instead of drawing an ellipse or rectangle. Next, when it comes time to draw the particle, use the Incidentally, this smoke example is a nice excuse to revisit the Gaussian distributions from the Introduction. To make the smoke appear a bit more realistic, instead of launching allow the particles in a purely random direction initial velocity vectors mostly around a mean value (with a lower probability of outliers) can produce an effect that appears less fountain-like and more like smoke (or fire). Using This smoke example is also a nice excuse to revisit the Gaussian distributions from the Introduction. Instead of launching the particles in a purely random direction, which produces a fountain-like effect, the result will appear more smoke-like if the initial velocity vectors cluster mostly around a mean value, with a lower probability of outlying velocities. Using the Finally, in this example, a wind force is applied to the smoke mapped from the mouse’s horizontal position. Finally, in this example I’ve applied a wind force to the smoke, mapped from the mouse’s horizontal position. In addition to designing the texture itself, you should also consider its resolution. Rendering a large number of high-resolution textures can significantly impact performance, especially if the code is required to resize them dynamically. The ideal scenario for optimal speed is to size the texture precisely to the resolution you intend to draw the particles in the canvas. If you do want particles of varying sizes, then ideally you should create the texture at the maximum size of any particle. Try creating your own textures for different types of effects. Can you make it look like fire, instead of smoke? Try creating other textures for different types of effects. Can you make your particle system look like fire instead of smoke? Use an array of images and assign each Use an array of images and assign each Finally, it’s worth noting that there are many different algorithms for blending colors in computer graphics. These are often referred to as “blend modes.” By default, when you draw something on top of something else in p5, you only see the top layer—this is the default “blend” behavior. When pixels have alpha transparency (as they do in the smoke example), p5.js uses an alpha compositing algorithm that combines a percentage of the background pixels with the new foreground pixels based on those alpha values themselves. However, it’s possible to draw using other blend modes, and a much loved blend mode for particle systems is “additive.” Additive blending in Processing with particle systems was pioneered by Robert Hodgin in his famous particle system and forces exploration, Magnetosphere, which later became the iTunes visualizer. For more see: Magnetosphere. Additive blending is a simple blend algorithm that involves adding the pixel values of one layer to another, capping all values at 255. This results in a space-age glow effect, as the colors get brighter and brighter the more layers that add together. Finally, it’s worth noting that there are many different algorithms for blending colors in computer graphics. These are often referred to as blend modes. Normally, when you draw something on top of something else in p5.js, you only see the top layer—this is the default “blend” behavior of not blending at all. Meanwhile, when pixels have alpha transparency values (as they do in the smoke example), p5.js automatically uses an alpha compositing algorithm that combines a percentage of the background pixels with the new foreground pixels, based on those alpha values themselves. However, it’s possible to draw using other blend modes. For example, a much-loved technique for particle systems is additive blending. This mode was pioneered by Robert Hodgin in his famous particle system and forces exploration, Magnetosphere, which later became the iTunes visualizer. Additive blending is a simple blend algorithm that involves adding the pixel values of one layer to another, capping all values at 255. This results in a space-age glow effect, with the colors getting brighter and brighter as more layers are added together. Then, before you go to draw anything, you set the blend mode using Before you go to draw anything, set the blend mode using Additive blending and particle systems provide an opportunity to discuss renderers in computer graphics. A renderer is the part of the code that’s responsible for drawing on the screen. The p5.js library's default renderer, which you’ve so far been using without realizing it, is built on top of the standard 2D drawing and animation renderer included in modern web browsers. However, there’s an additional rendering option called Typically, the WebGL renderer is only necessary if you’re drawing 3D shapes in your p5.js sketch. However, even for 2D sketches, the WebGL renderer can be useful in some cases—depending on your computer's hardware and the specific details of your sketch, it can significantly improve drawing performance. A particle system (especially one with additive blending enabled) is exactly one of these scenarios where many more particles can be drawn without slowing down the sketch in Use — Valentino Braitenberg Let’s think for a moment. Why are you here? The nature of code, right? What have I been demonstrating so far? Inanimate objects. Lifeless shapes sitting in canvas that flop around when affected by forces in their environment. What if you could breathe life into those shapes? What if those shapes could live by their own rules? Can shapes have hopes and dreams and fears? This is the domain on this chapter—autonomous agents. The term autonomous agent generally refers to an entity that makes its own choices about how to act in its environment without any influence from a leader or global plan. For the context here, “acting” will mean moving. This addition is a significant conceptual leap. Instead of a box sitting on a boundary waiting to be pushed by another falling box, I would like to now design a box that has the ability and “desire” to leap out of the way of that other falling box, if it so chooses. While the concept of forces that come from within is a major shift in design thinking, the code base will barely change, as these desires and actions are simply that—forces. Here are three key components of autonomous agents to keep in mind as I build the examples. In the late 1980s, computer scientist Craig Reynolds developed algorithmic steering behaviors for animated characters. These behaviors allowed individual elements to navigate their digital environments in a “lifelike” manner with strategies for fleeing, wandering, arriving, pursuing, evading, and more. Used in the case of a single autonomous agent, these behaviors are fairly simple to understand and implement. In addition, by building a system of multiple characters that steer themselves according to simple, locally based rules, surprising levels of complexity emerge. The most famous example is Reynolds’s “boids” model for “flocking/swarming” behavior. Now that I‘ve discussed the core concepts behind autonomous agents, it‘s time to begin writing the code. There are many places where I could start. Artificial simulations of ant and termite colonies are fantastic demonstrations of systems of autonomous agents. (For more on this topic, I encourage you to read Turtles, Termites, and Traffic Jams by Mitchel Resnick.) However, I want to begin by examining agent behaviors that build on the work in the first four chapters of this book: modeling motion with vectors and forces. And so it’s time to once again rename the class that describes an entity moving about a canvas. What was once Ultimately, the most important layer for you to consider is #1—Action Selection. What are the elements of your system and what are their goals? In this chapter, I am going to cover a series of steering behaviors (i.e. actions): seek, flee, follow a path, follow a flow field, flock with your neighbors, etc. It’s important to realize, however, that the point of understanding how to write the code for these behaviors is not because you should use them in all of your projects. Rather, these are a set of building blocks, a foundation from which you can design and develop vehicles with creative goals and new and exciting behaviors. And even though the examples will be highly literal in this chapter (follow that pixel!), you should allow yourself to think more abstractly (like Braitenberg). What would it mean for your vehicle to have “love” or “fear” as its goal, its driving force? Finally (and I’ll address this later in the chapter), you won’t get very far by developing simulations with only one action. Yes, the first example will be “seek a target.” But for you to be creative—to make these steering behaviors your own—it will all come down to mixing and matching multiple actions within the same vehicle. So view these examples not as singular behaviors to be emulated, but as pieces of a larger puzzle that you will eventually assemble. I could entertain you by discussing the theoretical principles behind autonomous agents and steering as much as you like, but we won’t get anywhere without first understanding the concept of a steering force. Consider the following scenario: a vehicle with a current velocity seeks a target. And let’s think of the vehicle as a bug-like creature who desires to savor a delicious strawberry. After working for a bit with the seeking behavior, you probably are asking yourself, “What if I want the vehicle to slow down as it approaches the target?” Before I can even begin to answer this question, I should look at the reasons behind why the seek behavior causes the vehicle to fly past the target so that it has to turn around and go back. Let’s consider the brain of a seeking vehicle. What is it thinking? The first two examples I’ve covered—seek and arrive—boil down to calculating a single vector for each behavior: the desired velocity. And in fact, every single one of Reynolds’s steering behaviors follows this same pattern. In this chapter, I’m going to walk through several more of Reynolds’s behaviors—flow field, path-following, flocking. First, however, I want to emphasize again that these are examples—demonstrations of common steering behaviors that are useful in procedural animation. They are not the be-all and end-all of what you can do. As long as you can come up with a vector that describes a vehicle’s desired velocity, then you have created your own steering behavior. Let’s see how Reynolds defines the desired velocity for his wandering behavior. Come up with your own arbitrary scheme for calculating a desired velocity. Now back to the task at hand. Let’s examine another Craig Reynolds’s steering behavior: flow field following. What is a flow field? Think of the canvas as a grid. In each cell of the grid lives an arrow pointing in some direction—you know, a vector. As a vehicle moves around the canvas, it asks, “Hey, what arrow is beneath me? That’s my desired velocity!” In a moment, I’m going to work through the algorithm (along with accompanying mathematics) and code for another of Craig Reynolds’s steering behaviors: Path Following. Before I can do this, however, I would like to spend some time discussing a piece of vector math that I skipped over in Chapter 1—the dot product. I haven’t needed it yet, but it’s neccessary here and likely will prove quite useful for you beyond just this example. Remember all the vector math covered in Chapter 1? Add, subtract, multiply, and divide? The previous two exercises hint at what is perhaps the most important aspect of this chapter. After all, what is a p5.js sketch with one steering force compared to many? How could I even begin to simulate emergence in a sketch with only one rule? The most exciting and intriguing behaviors will come from mixing and matching multiple steering forces, and I’ll need a mechanism for doing so. You may be thinking, “This is nothing new. We do this all the time.” You would be right. In fact, this technique appeared as early as Chapter 2. Redo Example 5.8 so that the behavior weights change over time. For example, what if the weights were calculated according to a sine wave or Perlin noise? Or if some vehicles are more concerned with seeking and others more concerned with separating? Can you introduce other steering behaviors as well? Flocking is a group animal behavior that is characteristic of many living creatures, such as birds, fish, and insects. In 1986, Craig Reynolds created a computer simulation of flocking behavior and documented the algorithm in his paper, “Flocks, Herds, and Schools: A Distributed Behavioral Model.” Recreating this simulation in p5.js will bring together all the concepts in this chapter. Finally, I am ready for cohesion. Here the code is virtually identical to that for alignment—only instead of calculating the average velocity of the boid’s neighbors, I want to calculate the average position of the boid’s neighbors (and use that as a target to seek). It’s also worth taking the time to write a class called Visualize the flock in an entirely different way. I would like to hide the dark truth behind what I’ve just done, because I would like you to be happy and live a fulfilling and meaningful life. But I also would like to be able to sleep at night without worrying about you so much. So it is with a heavy heart that I must bring up this topic. yes, group behaviors are wonderful. But they can be slow, and the more elements in the group, the slower they can be. Usually, when I talk about p5.js sketches running slowly, it’s because drawing to the canvas can be slow—the more you draw, the slower your sketch runs. This is actually a case, however, where the slowness derives from the algorithm itself. Let’s discuss. Computer scientists classify algorithms with something called “Big O notation,” which describes the efficiency of an algorithm: how many computational cycles does e it require to complete? Let’s consider a simple analog search problem. You have a basket containing one hundred chocolate treats, only one of which is pure dark chocolate. That’s the one you want to eat. To find it, you pick the chocolates out of the basket one by one. Sure, you might be lucky and find it on the first try, but in the worst-case scenario you have to check all one hundred before you find the dark chocolate. To find one thing in one hundred, you have to check one hundred things (or to find one thing in N things, you have to check N times.) Your Big O Notation is N. This, incidentally, is the Big O Notation that describes the simple particle system. If you have N particles, you have to run and display those particles N times. Now, let’s think about a group behavior (such as flocking). For every This technique is known as “bin-lattice spatial subdivision” and is outlined in more detail in (surprise, surprise) Reynolds’s 2000 paper, “Interaction with Groups of Autonomous Characters”. How do you implement such an algorithm in p5.js? The sollution I’ll describe below uses multiple arrays. One array to track of all the boids, just like in the flocking example. This technique is known as “bin-lattice spatial subdivision” and is outlined in more detail in (surprise, surprise) Reynolds’s 2000 paper, “Interaction with Groups of Autonomous Characters”. How do you implement such an algorithm in p5.js? The solution I’ll describe below uses multiple arrays. One array to track of all the boids, just like in the flocking example. In addition to that array, I’ll store an additional reference to each In the Then when it comes time to have the boids check for neighbors, they can look at only those in their particular cell (in truth, I also need to check neighboring cells to deal with border cases). In the Then when it comes time to have the boids check for neighbors, they can look at only those in their particular cell (though in truth, I also need to check neighboring cells to deal with border cases). I’re only covering the basics here; for the full code, check the book’s website. Now, there are certainly flaws with this system. What if all the boids congregate in the corner and live in the same cell? Then don’t we have to check all 2,000 against all 2,000? The good news is that this need for optimization is a common one and there are a wide variety of similar techniques out there. For us, it’s likely that a basic approach will be good enough (in most cases, you won’t need one at all.) I cover another, more sophisticated approach, in my [tutorial series about building a QuadTree](https://thecodingtrain.com/CodingChallenges/098.1-quadtree.html) in p5.js. A QuadTree employs the exact same methodology as bin-lattice spatial subdivison, only the cells themselves are not sized equally but rather according to the distribution density of the elements themselves. TODO: QUAD TREE DIAGRAM https://github.com/nature-of-code/noc-book-2/issues/101 The accompanying [QuadTree p5.js library](https://github.com/CodingTrain/QuadTree) also includes a flocking example. . The bin-lattice spatial subdivision is best suited for when the elements are evenly distributed throughout the canvas. A data structure known as a QuadTree, however, can handle unevenly distributed systems, preventing the worst-case scenario of all the boids crowding into a single cell. The QuadTree expands upon the spatial subdivision strategy by dynamically adapting the grid according to the distribution of the boids. Instead of a fixed grid, a QuadTree starts with a single large cell that encompasses the entire space. If too many boids are found within this cell, it splits into four smaller cells. This process can repeat for each new cell that gets too crowded, creating a flexible grid that provides finer resolution where it's needed. Additionally, the QuadTree strategy is key to the Barnes-Hut algorithm, which I referenced briefly when building an n-body simulation in Chapter 2. This method uses a QuadTree to approximate groups of bodies into a single one when calculating gravitational forces. This drastically reduces the number of calculations needed, allowing simulations with large numbers of bodies to run more efficiently. You can learn how to build a QuadTree and apply it to a flocking system as part of Coding Challenge #98 on thecodingtrain.com. Expand the bin-lattice spatial subdivision example to use a QuadTree. This is something of a momentous occasion. The end of Chapter 5 marks the end of the story of motion (in the context of this book, that is). I started with the concept of a vector, moved on to forces, designed systems of many elements, examined physics libraries, built entities with hopes and dreams and fears, and simulated emergence. The story doesn’t end here, but it does take a bit of a turn. The next two chapters won’t focus on moving bodies, but rather on systems of rules. Before you get there, I have a few quick items I’d like to mention that are important when working with the examples in Chapters 1 through 5. They also relate to optimizing your code, which fits in with the previous section. What is magnitude squared and when should you use it? Let’s revisit how the magnitude of a vector is calculated. And how is magnitude squared calculated? Same as magnitude, but without the square root. In the case of a single vector, this will never make a significant difference on a p5.js sketch. However, if you are computing the magnitude of thousands of vectors each time through There’s a pattern here. What kinds of functions are slow to compute? Square root. Sine. Cosine. Tangent. Again, if you just need a sine or cosine value here or there in your code, you are never going to run into a problem. But what if you had something like this?Example 1.7: Motion 101 (Velocity)
}
}
p5.Vector
class, and this class is the template for making the position
object and the velocity
object. So what are those objects doing inside of yet another object, the Mover
object?ParticleSystem
object will include a list of Particle
objects . . . and each Particle
object will have as its data several p5.Vector
objects!ParticleSystem
object will include a list of Particle
objects . . . and each Particle
object will have as its data several p5.Vector
objects!acceleration
.Acceleration
Chapter 2. Forces
- What is a physics engine?
- A physics engine is a computer program (or code library) that simulates the behavior of objects in a physical environment. In our case, the objects are two-dimensional shapes, and the environment is a rectangular canvas. Physics engines can be developed to be highly precise (requiring high-performance computing) or real-time (using simple and fast algorithms).
-
-Forces and Newton’s Laws of Motion
Example 3.4: Polar to Cartesian
let x = r * cos(theta);
let y = r * sin(theta);
- ellipseMode(CENTER);
fill(127);
stroke(0);
strokeWeight(2);
@@ -350,6 +349,13 @@ Example 3.4: Polar to Cartesian
// Increase the angle over time
theta += 0.02;
}
+p5.Vector
class called fromAngle()
. It takes an angle in radians and creates a unit vector in Cartesian space that points in the direction specified by the angle. Here’s how that would look in Example 3.4. // Creating a unit vector pointing in the direction of an angle
+ let position = p5.Vector.fromAngle(theta);
+ // To complete Polar to Cartesian conversion the vector is scaled by r
+ position.mult(r);
+ // Drawing the circle using the x,y components of the vector
+ circle(position.x, position.y, 48);
Exercise 3.4
diff --git a/content/04_particles.html b/content/04_particles.html
index 2ddb0be3..60d28045 100644
--- a/content/04_particles.html
+++ b/content/04_particles.html
@@ -4,17 +4,18 @@ Chapter 4. Particle Systems
4.1 Why You Need Particle Systems
-Why You Need Particle Systems
+// Ah, isn’t this main program so simple and lovely?
let system;
@@ -27,14 +28,12 @@
-4.1 Why You Need Particle Systems
background(255);
system.run();
}4.2 A Single Particle
-Mover
class from Chapter 2 serves as the perfect template. A particle is an independent body that moves about the canvas. It has position
, velocity
, and acceleration
, a constructor to initialize those variables, and functions to display()
itself and update()
its position.ParticleSystem
class, which itself holds references to lots of instances of the Particle
class. Getting used to this technique of writing sketches with multiple classes, including classes that keep lists of instances of other classes, will prove very useful as you get to later chapters in this book.A Single Particle
+Mover
class from Chapter 2 serves as the perfect template. A particle is an independent body that moves about the canvas, so just like a mover it has position
, velocity
, and acceleration
variables; a constructor to initialize those variables; and methods to display()
itself and update()
its position.class Particle {
- //{!5} A “Particle” object is just another name for our “Mover.” It has position, velocity, and acceleration.
+ //{!5} A Particle object is just another name for a Mover. It has position, velocity, and acceleration.
Particle(x, y) {
this.position = createVector(x, y);
this.acceleration = createVector();
@@ -52,34 +51,35 @@
-4.2 A Single Particle
circle(this.position.x, this.position.y, 8);
}
}applyForce()
function to affect the particle’s behavior (I’ll do precisely this in a future example). I could also add variables to describe color and shape, or load a p5.Image
to draw the particle. For now, however, I’ll focus on adding just one additional detail: lifespan.Particle
class, I’ll choose to add a lifespan
variable that acts like a countdown timer. The timer will start at 255 and count down to 0, when the particle will be considered “dead.” The code for this in the Particle
class as:applyForce()
method to affect the particle’s behavior (I’ll do precisely this in a future example). I could also add variables to describe color and shape, or load a p5.Image
to draw the particle in a more interesting way. For now, however, I’ll focus on adding just one additional detail: lifespan.lifespan
variable that acts like a timer. It will start at 255 and count down to 0 as the sketch progresses, at which point the particle will be considered dead. Here’s the added code in the Particle
class:class Particle {
constructor(x, y) {
this.position = createVector(x, y);
this.acceleration = createVector();
this.velocity = createVector();
- //{!1 .bold} A new variable to keep track of how long the particle has been “alive”. We start at 255 and count down for convenience.
+ //{!1 .bold} A new variable to keep track of how long the particle has been “alive.” It starts at 255 and counts down to 0.
this.lifespan = 255;
}
update() {
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
- //{!1 .bold} Lifespan decreases
+ //{!1 .bold} Lifespan decreases.
this.lifespan -= 2.0;
}
show() {
- //{!2 .bold} Since the life ranges from 255 to 0 it can be used also for alpha
+ //{!2 .bold} Since the life ranges from 255 to 0, it can also be used also for alpha.
stroke(0, this.lifespan);
fill(175, this.lifespan);
circle(this.position.x, this.position.y, 8);
}
}
-lifespan
as the alpha transparency for the circle as well. When the particle is “dead” it will also have faded away.lifespan
property, I’ll need one additional function—a function that can be queried (for a true or false answer) as to whether the particle is alive or dead. This will come in handy when writing the a class to manage the list of particles themselves. Writing this function is pretty easy. I just need to check whether the value of lifespan
is less than 0. If it is, return true
; otherwise, return false
.lifespan
ranging from 255 to 0, it can conveniently double as the alpha transparency for the circle representing the particle. This way, when the particle is dead, it will have literally faded away.lifespan
property, I’ll need one more method, one that can be queried (for a true or false answer) to determine whether the particle is alive or dead. This will come in handy when I write a separate class to manage the list of particles itself. Writing this method is pretty easy: I just need to check whether the value of lifespan
is less than 0. If it is, return true
; otherwise, return false
. isDead() {
//{!5} Is the particle still alive?
if (this.lifespan < 0.0) {
@@ -88,14 +88,14 @@
-4.2 A Single Particle
return false;
}
} isDead() {
//{!1} Is the particle still alive?
return (this.lifespan < 0.0);
}
-Particle
object. Here is the full code below, with one small addition–giving the particle a random initial velocity as well as adding applyForce()
to simulate gravity.Particle
object at a time. Here’s the full code, with a few small additions: giving the particle a random initial velocity, as well as adding applyForce()
to simulate gravity.Example 4.1: A single particle
+ Example 4.1: A Single Particle
Example 4.1: A single particle
function draw() {
background(255);
- //{!2} Operating the single Particle
+ //{!2} Operating the single particle.
particle.update();
particle.show();
- //{!2} Applying a gravity force
+ //{!2} Applying a gravity force.
let gravity = createVector(0, 0.1);
particle.applyForce(gravity);
- //{!4} Checking the particle's state and making a new particle
- if (p.isDead()) {
+ //{!4} Checking the particle's state and making a new particle.
+ if (particle.isDead()) {
particle = new Particle(width / 2, 20);
console.log("Particle dead!");
}
}
class Particle {
-
constructor(x,y) {
this.position = createVector(x, y);
//{!1 .offset-top} For demonstration purposes the Particle has a random velocity.
@@ -142,43 +141,40 @@ Example 4.1: A single particle
this.acceleration.mult(0);
}
- applyForce(force) {
- this.acceleration.add(force);
- }
-
show() {
stroke(0, this.lifespan);
fill(0, this.lifespan);
circle(this.position.x, this.position.y, 10);
}
- //{!3} Keeping the same physics model as with previous chapters
+ //{!3} Keeping the same physics model as in previous chapters.
applyForce(force) {
this.acceleration.add(force);
}
- //{!3} Is the Particle alive or dead?
+ //{!3} Is the particle alive or dead?
isDead() {
return (this.lifespan < 0.0);
}
}
+particle
variable is overwritten with a new instance of the Particle
class. This effectively replaces the previous particle object. It's important to understand that the previous particle object isn't so much "deleted" as it is no longer accessible or used within the code. The sketch essentially forgets the old particle and starts anew with the freshly created one.Exercise 4.1
- run()
function in the Particle
class that handles update()
, show()
, and applyForce()
. What are the pros and cons of this approach?run()
method in the Particle
class that handles update()
, show()
, and applyForce()
. What are the pros and cons of this approach?Exercise 4.2
- 4.3 The Array
-Array
has all the functionality we need for managing a list of Particle
objects. The the built-in JavaScript functions available in the JavaScript class Array
allow for adding and removing particles and manipulating the arrays in all sorts of powerful ways. Although there are some cons to this approach, in order to keep the subsequent code examples more concise, I'm using a solution to Exercise 4.1 and assume a run()
method that handles all of the particle's functionality. JavaScript Array Documentation.An Array of Particles
+Array
class, and so it comes with many built-in methods. These methods supply all the functionality I need for maintaining a list of Particle
objects, including adding particles, removing particles, or otherwise manipulating them. For a refresher on arrays, see the JavaScript Array Documentation on the MDN Web Docs.Particle.run()
method that manages all of an individual particle's functionality. While there are also some cons to this approach, it will keep the subsequent code examples more concise. To begin, I’ll use a for
loop in setup()
to populate an array with particles, then use another for
loop in draw()
to run each particle.let total = 10;
-//{!1} Starting with an empty array
+//{!1} Start with an empty array.
let particles = [];
function setup() {
- //{!3} This is what you’re probably used to, accessing elements on the array via an index and brackets—[].
+ //{!3} This is what you’re probably used to--accessing elements on the array via an index and brackets: [i].
for (let i = 0; i < total; i++) {
particles[i] = new Particle(width/2, height/2);
}
@@ -190,20 +186,23 @@
-4.3 The Array
particle.run();
}
}for
loop demonstrates how to call functions on every element of an array by accessing each index. I initialize a variable i
with value 0 and count up by 1, accessing each element of the array until I reach the end. However, this is a good time to mention the JavaScript for of
loop, which is a bit more concise. The for of
loop works with arrays as follows:for
loop in draw()
demonstrates how to call a method on every element of an array by accessing each index. I initialize a variable i
to 0
and increment it by 1
, accessing each element of the array until i
hits particles.length
and so reaches the end. As it happens, there are actually a few different ways to do this, and that’s something that I both love and hate about coding in JavaScript—there are so many different styles and options to consider. On the one hand, this makes JavaScript a highly flexible and adaptable language, but on the other hand, the abundance of choice can be overwhelming and lead to a lot of confusion when learning.
+
+for
loop, as just demonstrated. This is probably what you’re most used to, and it follows a similar syntax as other programming languages like Java and C.for...in
loop. This kind of loop allows you to iterate over all the properties of an object. It's not particularly useful for arrays, so I won't cover it here.forEach()
loop. This is a great one, and I encourage you to explore it! It’s an example of a higher-order function, something I’ll explain later in this chapter.for...of
loop. This is the technique I’ll expand upon next. It provides a clean and concise syntax compared to the traditional for
loop when working with arrays of objects.for...of
loop looks:function draw() {
for (let particle of particles) {
particle.run();
}
}
-for (let particle of particles) {
- particle.run();
-}
-for of
loop and I will get to use it in examples. But not just yet.draw()
. I’ll skip rehashing the Particle
class code here, as it doesn’t need to change. What I have so far is:for...of
loops, take a moment and breathe, because I have some bad news: they won’t work in every situation. Yes, I love for...of
loops, and I’ll get to use them in some of the examples to come where I need to iterate over the items in an array, but not just yet. Ultimately, I want to create a continuous stream of particles, with one new particle added to the array each cycle through draw()
and old particles removed from the array as they die. As you’ll soon see, this is where the for...of
loop lets me down.Array
class’s push()
method during draw()
to add a new Particle
object to the end of the array. This eliminates the need to create any particles during setup()
.let particles = [];
function setup() {
@@ -215,43 +214,67 @@
-4.3 The Array
//{!1 .offset-top} A new Particle object is added to the array every cycle through draw().
particles.push(new Particle(width / 2, 50));
- for (let particle of particles) {
- particle.run();
+ for (let i = 0; i < particles.length; i++) {
+ particles[i].run();
}
}for of
loop; this loop provides no means for deleting elements while iterating. Instead, I can use the Array splice()
method. (Yes, an array in JavaScript is actually an object created from the class Array
with many methods!) The splice()
method removes one or more elements from an array starting from a given index.splice()
method to get rid of particles as they die. It removes one or more elements from an array starting from a given index. And this is why I can’t use a for...of
loop here; splice()
needs a reference to the index of the particle being removed, but for...of
loops don’t provide such a reference. I’m stuck using a regular for
loop instead. for (let i = 0; i < particles.length; i++) {
+ //{!1} Improve readability by assigning array element to a variable
let particle = particles[i];
particle.run();
if (particle.isDead()) {
- //{!1} Remove one particle at index i
+ //{!1} Remove one particle at index i.
particles.splice(i, 1);
}
}
- for (let i = 0; i < particles.length; i++) {
let particle = particles[i];
particle.run();
//{!1 .offset-top} Adding a new Particle to the list while iterating?
particles.push(new Particle(width / 2, 50));
}
-length
of the array increases). This will result in an infinite loop, as I can never increment past the size of the array!splice()
, all subsequent elements are shifted to the left. Figure 4.1 shows what happens when particle C (index 2) is removed. Particles A and B keep the same index, while particles D and E shift from 3 and 4 to 2 and 3, respectively.length
of the array increases. This will result in an infinite loop, as I can never increment past the size of the array!splice()
, all subsequent elements are shifted to the left. Figure 4.1 shows what happens when particle C (index 2) is removed. Particles A and B keep the same index, while particles D and E shift from 3 and 4 to 2 and 3, respectively.i
iterating over the elements of the array.
-
-i
has already moved on to slot #3. This is not a total disaster, since particle D will get checked the next time around through draw()
. Still, the expectation is that the code should iterate through every single element of the array. Skipping one is unacceptable!i
iterates over the elements of the array:
+
+
+
+
+
+
+ Value of
+ i
Particle
+ Action
+
+
+ 0
+ particle A
+ Don’t delete!
+
+
+ 1
+ particle B
+ Don’t delete!
+
+
+ 2
+ particle C
+ Delete! Slide particles D and E back from slots 3 and 4 to 2 and 3.
+
+
+
+3
+ particle E
+ Don’t delete!
+ i
has already moved on to slot 3. In practice, this may not be a total disaster, since particle D will get checked the next time around through draw()
. Still, the expectation is that the code should iterate through every single element of the array. Skipping an element is unacceptable! //{!1 .bold} Looping through the list backwards
for (let i = particles.length - 1; i >= 0; i--) {
let particle = particles[i];
@@ -260,16 +283,16 @@
-4.3 The Array
particles.splice(i, 1);
}
}sort()
which takes as its argument a function that defines how to compare two elements of the array and then sorts the array according to that comparison. With the array of particles, I can make use of the higher order function filter()
. filter()
checks each item in the specified array and keeps only the item(s) where the given condition is true (removing those that return false).sort()
, which takes as its argument a function that defines how to compare two elements of the array and then sorts the array according to that comparison. With the array of particles, I can use filter()
, a higher-order function that takes a function specifying some kind of condition as an argument, checks each item in an array for that condition, and keeps only the item(s) where the given condition is true (removing those items that return false). particles = particles.filter(function(particle) {
//{!1} Keep particles that are not dead!
return !particle.isDead();
});
- particles = particles.filter(particle => !particle.isDead());
-splice()
method, but I encourage you to explore writing your code with higher-order functions and arrow notation.splice()
method, but I encourage you to explore writing your code with higher-order functions and arrow notation.Example 4.2: Array of particles
+ Example 4.2: An Array of Particles
Example 4.2: Array of particles
}
}
}
-4.4 A Particle Emitter
-Particle
object. I’ve conquered the array and used it to manage a list of many particles (with the ability to add and delete at will).Particle
objects itself, a “particle system.” Rather than “system”, however, I’m going to call this classEmitter
, named for its functionality of “emitting” particles. The Emitter
class will allow me to remove the bulky logic of looping through all particles from draw()
, as well as open up the possibility of having multiple particle emitters.setup()
and draw()
without referencing any individual particles. Here it is again, only now with the naming convention of “emitter.”shift()
which automatically removes the first element of an array. However, in many particle systems, other conditions or interactions may cause particles to “die” (reach the end of their lifespan) before others, even if they were created later. Checking isDead()
in combination withsplice()
is a nice comprehensive solution that offers flexibility in managing particles across a variety of scenarios.A Particle Emitter
+Particle
objects, with the ability to add and delete particles at will. I could stop here and rest on my laurels, but there’s an additional step that I can and should take: writing a class describing the list of Particle
objects itself. I’ll call this class, which represents the overall particle system, Emitter
, after its functionality of “emitting” particles. The Emitter
class will allow me to clean up the draw()
function, removing the bulky logic of looping through all the particles. As an added bonus, it will also open up the possibility of having multiple particle emitters.setup()
and draw()
without referencing any individual particles. In laying out that goal, I teased the possibility of a beautifully simple main sketch file. Here it is again, only now with the Emitter
naming convention://{!1} Just one Particle Emitter!
let emitter;
@@ -311,7 +334,7 @@
-4.4 A Particle Emitter
background(255);
emitter.run();
}setup()
and draw()
can fit into the Emitter
class.setup()
and draw()
from Example 4.2 and think about how it can fit into the Emitter
class instead. Nothing about the behavior of the code should change—the only difference is how it’s organized.
@@ -359,51 +382,52 @@ 4.4 A Particle Emitter
Emitter
class to keep track of an origin point where particles are born. The origin point could be initialized in the constructor.Example 4.3: Single Particle Emitter
+ Example 4.3: A Single Particle Emitter
class Emitter {
-
Emitter(x, y) {
- //{!1 .bold} This particular ParticleSystem implementation includes an origin point where each Particle begins.
+ //{!1 .bold} This particular particle system implementation includes an origin point where each particle begins.
this.origin = createVector(x, y);
this.particles = [];
}
addParticle() {
- //{!1 .bold} The origin is passed to each Particle when it is added.
+ //{!1 .bold} The origin is passed to each particle when it's added to the array.
this.particles.add(new Particle(origin.x, origin.y));
- }
+ }
+}
+Exercise 4.3
- Exercise 4.4
- 4.5 A System of Emitters
-Particle
object. I also described a system of Particle
objects, and this we call a “particle system” and organized the code into an Emitter
class. I’ve defined a particle system as a collection of independent objects. But isn’t a particle system itself an object? If that’s the case (which it is), there’s no reason why I couldn’t also build a collection of many particle emitters, i.e. a system of systems.A System of Emitters
+Particle
class. I’ve also described a system of particles and organized the code into an Emitter
class. This particle system is nothing more than a collection of independent Particle
objects. But as an instance of the Emitter
class, isn’t a particle system itself an object? If that’s the case (and it is), there’s no reason why I couldn’t also build a collection of many particle emitters: a system of systems!Emitter
object in the variable emitter
.Emitter
object in the variable emitter
.let emitter;
function setup() {
@@ -416,46 +440,47 @@
-4.5 A System of Emitters
emitter.addParticle();
emitter.run();
}Array
to keep track of multiple instances of emitters themselves. When the sketch begins, i.e. in setup()
, the Array
is empty.emitters
plural and make it an array so I can keep track of multiple Emitter
objects. When the sketch begins, the array is empty.Example 4.4: System of Systems
+ Example 4.4: A System of Systems
//{!1} This time, the type of thing we are putting in the array is a particle emitter itself!
-let emitter = [];
+
//{!1} This time, the type of thing we're putting in the array is a particle emitter itself!
+let emitters = [];
function setup() {
createCanvas(640, 240);
}
-Emitter
object is created and placed into the Array
.Emitter
object is created and placed into the array.function mousePressed() {
emitters.push(new Emitter(mouseX, mouseY));
}
-draw()
, instead of referencing a single Emitter
object, I can now iterate over all the emitters and call run()
on each of them.draw()
, instead of referencing a single Emitter
object, I now iterate over all the emitters and call run()
on each of them.function draw() {
background(255);
- //{!4} Since we aren’t deleting elements, we can use the for of loop!
+ //{!4} No emitters are removed a for...of loop can work heree!
for (let emitter of emitters) {
emitter.run();
emitter.addParticle();
}
}
+for...of
loop since no elements are being removed from the emitters
array.Exercise 4.5
- emitters
.emitters
array.Exercise 4.6
- 4.6 Inheritance and Polymorphism: An Introduction
-Particle
class to have variables that store color, shape, behavior, and more. To create a variety of particles, you might initialize those variables with random values. But what if your particles are drastically different? This could become very messy, having all sorts of code for different ways of being a particle in the same class. Another option might be to do the following:Inheritance and Polymorphism
+Particle
class to have variables that store color, shape, behavior, and more. To create a variety of particles, you might initialize those variables with random values. But what if some of your particles are drastically different? This could become very messy, having all sorts of code for different ways of being a particle in the same class. Another option might be to do the following:class HappyConfetti {
}
@@ -467,7 +492,7 @@
-4.6 Inheritance and Pol
class WackyConfetti {
}
Emitter
constructor could then have some code to pick randomly from the three classes when filling the array. (Note that this probabilistic method is the same one I employed in the random walk examples in the Introduction.)Emitter
constructor could then have some code to pick randomly from the three classes when filling the array. (Note that this probabilistic method is the same one I employed in the random walk examples in the Introduction.)class Emitter {
constructor(num) {
this.particles = [];
@@ -484,13 +509,11 @@
-4.6 Inheritance and Pol
}
}
}
update()
function that implements the motion algorithm; and more.4.7 Inheritance Basics
-Dog
class. A Dog
object will have an age variable (an integer), as well as eat()
, sleep()
, and bark()
functions.update()
function that implements the motion algorithm; and more.Inheritance Basics
+Dog
class. A Dog
object will have an age
variable (an integer), as well as eat()
, sleep()
, and bark()
methods.class Dog {
constructor() {
@@ -509,10 +532,10 @@
-4.7 Inheritance Basics
print("WOOF");
}
}class Cat {
- // Dogs and cats have the same variables (age) and functions (eat, sleep).
+ // Dogs and cats have many of the same variables (age) and methods (eat, sleep).
constructor() {
this.age = 0;
}
@@ -535,13 +558,13 @@
4.7 Inheritance Basics
Dog
class is a child (subclass) of an Animal
class. Children will automatically inherit all variables and functions from the parent (superclass), but can also include functions and variables not found in the parent. Like a phylogenetic "tree of life," inheritance follows a tree structure. Dogs inherit from mammals, which inherit from animals, and so on.Dog
and Cat
to be designated as children (subclasses) of the Animal
class. Children automatically inherit all variables and methods from the parent (superclass), but they can also include methods and variables not found in the parent. Like a phylogenetic “tree of life,” inheritance follows a tree structure: dogs inherit from mammals, which inherit from animals, and so on.//{!1} The Animal class is the parent (or super) class.
+
//{!1} The Animal class is the parent (or superclass).
class Animal {
constructor() {
@@ -559,7 +582,7 @@
-4.7 Inheritance Basics
}
}
-//{!1 .bold} The Dog class is the child (or sub) class, indicated by the code "extends Animal".
+//{!1 .bold} The Dog class is the child (or subclass), indicated by the code "extends Animal".
class Dog extends Animal {
constructor() {
//{!1 .bold} super() executes code found in the parent class.
@@ -579,14 +602,10 @@ 4.7 Inheritance Basics
print("MEOW!");
}
}
-
-Dog extends Animal
, Terrier extends Dog
. Everything is inherited all the way down the line.super()
. super()
can also receive arguments if there is a parent constructor defined with matching arguments.Dog
object has a “hair color” variable in addition to age. The class would now look like this:extends
keyword, which specifies a parent for the class being defined. A subclass can only extend one superclass. However, classes can extend classes that extend other classes, for example Dog extends Animal
, Terrier extends Dog
. Everything is inherited all the way down the line from Animal
to Terrier
.super()
in the Dog
and Cat
constructors. This calls the constructor in the parent class. In other words, whatever you do in the parent constructor, do so in the child constructor as well. In this case it isn’t necessary, but super()
can also receive arguments if there’s a parent constructor defined that takes matching arguments.bark()
method to Dog
and the meow()
method to Cat
. You can also include additional code, besides the call to super()
, in a subclass’s constructor to give that subclass extra variables. For example, let’s assume that in addition to age
, a Dog
object should have a haircolor
variable. The class would now look like this:class Dog extends Animal {
-
constructor() {
super();
//{!1} A child class can introduce new variables not included in the parent.
@@ -597,15 +616,15 @@
-4.7 Inheritance Basics
print("WOOF!");
}
}super()
, which sets the age to 0, but haircolor
is set inside the Dog
constructor itself. If a Dog
object eats differently than a generic Animal
object, parent functions can be overridden by rewriting the function inside the subclass.super()
, which sets age
to 0
, and then haircolor
is set inside the Dog
constructor itself.Dog
object eats differently than a generic Animal
object, the parent method can be overridden by creating a different definition for the method inside the subclass.class Dog extends Animal {
-
constructor() {
super();
- this.haircolor = color(random(255));
+ this.haircolor = color(210, 105, 30);
}
- // A child can override a parent function if necessary.
+ // A child can override a parent method if necessary.
eat() {
//{!1} A Dog's specific eating characteristics
print("Woof! Woof! Slurp.")
@@ -615,12 +634,11 @@
-4.7 Inheritance Basics
print("WOOF!");
}
}class Dog extends Animal {
-
constructor() {
super();
- this.haircolor = color(random(255));
+ this.haircolor = color(210, 105, 30);
}
eat() {
@@ -634,56 +652,62 @@
-4.7 Inheritance Basics
print("WOOF!");
}
}4.8 Particles with Inheritance
-Particle
class.Particle
implementation, adapted from Example 4.1:super()
in the constructor, calling super.eat()
inside the Dog
class’s eat()
method results in calling the Animal
class’s eat()
method. Then the subclass’s method definition can continue with any additional, custom code.Particles with Inheritance
+Particle
class. First, take another look at a basic Particle
implementation, adapted from Example 4.1:class Particle {
-
- Particle(x, y) {
+ constructor(x, y) {
this.acceleration = createVector(0, 0);
this.velocity = createVector(random(-1, 1), random(-2, 0));
this.position = createVector(x, y);
+ this.lifespan = 255.0;
}
run() {
- let gravity = createVector(0, 0.05);
- this.applyForce(gravity);
this.update();
- this.display();
+ this.show();
}
update() {
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
+ this.lifespan -= 2.0;
this.acceleration.mult(0);
}
-
+
+ applyForce(force) {
+ this.acceleration.add(force);
+ }
+
+ isDead() {
+ return (this.lifespan < 0);
+ }
+
show() {
- fill(0);
+ fill(0, this.lifespan);
circle(this.position.x, this.position.y, 8);
}
}
-Particle
(I’ll call it Confetti
). It will inherit all the instance variables and methods from Particle
. I‘ll also include a constructor and execute the code from the parent class with super()
.Confetti
subclass that extends Particle
. It will use super()
to execute the code from the parent class’s constructor, and it will inherit most of the Particle
class’s methods as well. However, I’ll give Confetti
its own show()
method, overriding that of its parent, so Confetti
objects will be drawn as squares rather than circles.class Confetti extends Particle {
-
constructor(x, y) {
super(x, y);
// I could add variables for only Confetti here.
}
- //{inline} There is no code here because update() is inherited from the parent.
+ //{inline} There's no code here because methods lke update() are inherited from the parent.
- //{!6} Override the show method.
+ //{!6} Override the show() method.
show() {
rectMode(CENTER);
fill(0);
square(this.position.x, this.position.y, 8);
}
}
-Confetti
particle rotate as it flies through the air. One option is to model angular velocity and acceleration as described in Chapter 3. For ease, however, I’ll implement something less formal.map()
function (see the Introduction!)Confetti
particle rotate as it flies through the air. One option is to model angular velocity and acceleration, as described in Chapter 3. For ease, however, I’ll implement something less formal.map()
function.let angle = map(this.position.x, 0, width, 0, TWO_PI);
-TWO_PI * 2
. Here’s how this code fits into the show()
function.show()
function. show() {
let angle = map(this.position.x, 0, width, 0, TWO_PI * 2);
@@ -698,21 +722,20 @@
+4.8 Particles with Inheritance
square(0, 0, 8);
pop();
}0
to TWO_PI * 2
.Exercise 4.7
- map()
to calculate theta, try modeling angular velocity and acceleration?map()
to calculate angle
, try modeling angular velocity and acceleration.Confetti
subclass that extends the base Particle
class, the next step is to also add Confetti
objects to the array of particles defined in the Emitter
class.4.10 Particle Systems with Inheritance
Example 4.5: Particle system with inheritance
+ Example 4.5: A Particle System with Inheritance
class Emitter {
-
constructor(x, y) {
this.origin = createVector(x, y);
//{!1 .bold} One list, for anything that is a Particle or extends Particle
@@ -731,39 +754,39 @@
+Example 4.5: Particle syste
run() {
for (let i = this.particles.length - 1; i >= 0; i--) {
- let p = this.particles[i];
- p.run();
- if (p.isDead()) {
+ let particle = this.particles[i];
+ particle.run();
+ if (particle.isDead()) {
this.particles.splice(i, 1);
}
}
}
}
Particle
and Confetti
objects co-mingle in the same particles
array within the Emitter
class. Thanks to the inheritance relationship, they can both be both considered the same type: Particle
. Inheritance, along with polymorphism, enables a variety of particle types to be managed together in the one array regardless of their original class.Exercise 4.8
4.11 Particle Systems with Forces
-applyForce()
function, I took a couple shortcuts to keep things simple. Let’s now add back in a mass property changing the constructor
and applyForce()
function. (The rest of the class stays the same)Particle Systems with Forces
+applyForce()
function in my Particle
class, I took a couple shortcuts to keep the code simple. Now I’ll add a mass
property back in, changing the constructor()
and applyForce()
methods in the process. (The rest of the class stays the same.)class Particle {
-
constructor(x, y) {
- //{!1} Now start with acceleration of 0,0.
this.position = createVector(x, y);
this.velocity = createVector(random(-1, 1),random(-2, 0));
+ //{!1} Now start with acceleration of 0,0.
this.acceleration = createVector(0, 0);
this.lifespan = 255.0;
- //{!1} Add a mass property. Trying varying mass for different interesting results!
+ //{!1} Add a mass property. Trying varying mass for different, interesting results!
this.mass = 1;
}
applyForce(force) {
- // {!2} Dividing force by mass
+ // {!2} Divide force by mass.
const f = force.copy();
f.div(this.mass);
this.acceleration.add(f);
}
-Particle
class is complete, I have a very important question to ask. Where should I call the applyForce()
function? Where in the code is it appropriate to apply a force to a particle? In my view there’s no right or wrong answer; it really depends on the exact functionality and goals of a particular p5.js sketch. My “quick and dirty” solution in the previous examples was to just create a gravity
force in the run()
function itself of each particle.Particle
class is complete, I have a very important question to ask: where should I call the applyForce()
method? Where in the code is it appropriate to apply a force to a particle? In my view, there’s no right or wrong answer; it really depends on the exact functionality and goals of a particular p5.js sketch. My quick and dirty solution in the previous examples was to just create and apply a gravity
force in the run()
method of each particle. run() {
// {!2} Create a hard-coded vector and apply as a force
let gravity = createVector(0, 0.05);
@@ -771,7 +794,7 @@
-4.11 Particle Systems with Forces
this.update();
this.show();
}draw()
to all particles.draw()
to all particles globally?function draw() {
background(100);
@@ -780,27 +803,26 @@
-4.11 Particle Systems with Forces
emitter.addParticle();
emitter.run();
}applyForce()
is a method written inside the Particle
class, but there is no reference to the individual particles themselves, only the Emitter
object emitter
. Since I want all particles to receive the force, however, I can pass the force to the emitter which is responsible for managing all the individual particles.applyForce()
method is written inside the Particle
class, but there’s no reference to the individual particles themselves, only to emitter
, the Emitter
object. Since I want all particles to receive the force, however, I can pass the force to the emitter and let it manage all the individual particles.function draw() {
background(255);
let gravity = createVector(0, 0.1);
- //{!1} Applying a force to the emitter
+ //{!1} Apply a force to the emitter.
emitter.applyForce(gravity);
emitter.addParticle();
emitter.run();
}
-Emitter
object in draw()
, I then have to define that function in the Emitter
class. Let’s describe the job that function needs to perform: receive a force as a p5.Vector
and apply that force to all the particles.applyForce()
method on the Emitter
object in draw()
, I then have to define that method in the Emitter
class. In English, that method needs to be able to receive a force as a p5.Vector
and apply that force to all the particles. Translating that into code: applyForce(force) {
for (let particle of this.particles) {
particle.applyForce(force);
}
}
-Particle
class written above; no need to include it again since nothing has changed):for...of
loop, since no particles are being deleted!)Particle
class written earlier; no need to show it again, since nothing has changed.)Example 4.6: Particle system with forces
+ Example 4.6: A Particle System with Forces
Example 4.6: Particle system wit
class Emitter {
-
constructor(x, y) {
this.origin = createVector(x, y);
this.particles = [];
@@ -837,7 +858,7 @@
Example 4.6: Particle system wit
}
applyForce(force) {
- //{!3} Using a for of loop to apply the force to all particles
+ //{!3} Using a for...of loop to apply the force to all particles
for (let particle of this.particles) {
particle.applyForce(force);
}
@@ -854,23 +875,24 @@
Example 4.6: Particle system wit
}
}
}
-
4.12 Particle Systems with Repellers
-Repeller
object—the inverse of the Attractor
object covered in Chapter 2 that pushes any particles away that get close? This requires a bit more sophistication because, unlike the gravity force, each force an attractor or repeller exerts on a particle must be calculated for each particle.Particle Systems with Repellers
+Repeller
object—the inverse of the Attractor
object covered in Chapter 2—that pushes any particles away that get too close? This requires a bit more sophistication than uniformly applying the gravity force, because the force the repeller exerts on a particular particle is unique and must be calculated separately for each particle (see Figure 4.3).Repeller
object into a particle system example. I’m going to need two major additions to the code:Repeller
object into a particle system sketch. I’m going to need two major additions to the code:
Repeller
object (declared, initialized, and displayed).Repeller
object into the particle emitter so that it can apply a force to each particle object.Repeller
object into the particle emitter so that the repeller can apply a force to each particle object.let emitter;
//{!1 .bold} New thing: we declare a Repeller object.
@@ -890,7 +912,7 @@
-4.12 Particle Systems with Repeller
let gravity = createVector(0, 0.1);
emitter.applyForce(gravity);
- //{!1 .bold} New thing: a function to apply a force from a repeller.
+ //{!1 .bold} New thing: a method to apply a force from a repeller.
emitter.applyRepeller(repeller);
emitter.run();
@@ -912,7 +934,7 @@
4.12 Particle Systems with Repeller
circle(this.position.x, this.position.y, this.r * 2);
}
}
applyRepeller()
function? Instead of passing a p5.Vector
into a function like with applyForce()
, I need to instead pass a Repeller
object into applyRepeller()
and ask that function to do the work of calculating the force between the repeller and all particles. Let’s look at both of these functions side by side.applyRepeller()
method. Instead of passing a p5.Vector
object as an argument, like with applyForce()
, I need to instead pass a Repeller
object into applyRepeller()
and ask that method to do the work of calculating the force between the repeller and each particle. Take a look at both of these methods side by side.
-
@@ -938,32 +960,32 @@
4.12 Particle Systems with Repeller
Repeller
object is the argument, not a p5.Vector
. The second difference is the more important one. I must calculate a custom p5.Vector
force for each and every particle and apply that force. How is that force calculated? In a function called repel()
, the inverse of the attract()
function from the Attractor
class.applyRepeller()
is a Repeller
object, not a p5.Vector
object. The second difference is the more important one. I must calculate a custom p5.Vector
force for each and every particle and apply that force. How is that force calculated? In a Repeller
class method called repel()
, the inverse of the attract()
method from the Attractor
class. // All the same steps to calculate an attractive force, only pointing in the opposite direction.
repel(particle) {
- // 1) Get force direction.
+ // 1) Get the force direction.
let force = p5.Vector.sub(this.position, particle.position);
- //{!2} 2) Get distance (constrain distance).
+ //{!2} 2) Get and constrain the distance.
let distance = force.mag();
distance = constrain(distance, 5, 50);
- // 3) Calculate magnitude, use a "power" variable for G
+ // 3) Calculate the magnitude, using a "power" variable for G.
let strength = -1 * this.power / (distance * distance);
- // 4) Make a vector out of direction and magnitude.
+ // 4) Make a vector out of the direction and magnitude.
force.setMag(strength);
return force;
}
Particle
class itself. A particle doesn’t actually have to know anything about the details of its environment; it simply needs to manage its position, velocity, and acceleration, as well as have the ability to receive an external force and act on it.Particle
class, which hasn’t changed.Particle
class, which hasn’t changed.Example 4.7: ParticleSystem with repeller
+ Example 4.7: A Particle System with a Repeller
// One ParticleSystem
+
+// One particle system.
let emitter;
-//{!1} One repeller
+//{!1} One repeller.
let repeller;
function setup() {
@@ -975,10 +997,10 @@
+Example 4.7: ParticleSystem wit
function draw() {
background(100);
emitter.addParticle();
- // We’re applying a universal gravity.
+ // Apply a universal gravity.
let gravity = createVector(0, 0.1);
emitter.applyForce(gravity);
- //{!1} Applying the repeller
+ //{!1} Apply the repeller.
emitter.applyRepeller(repeller);
emitter.run();
@@ -1032,7 +1054,6 @@
Example 4.7: ParticleSystem wit
show() {
stroke(0);
- strokeWeight(2);
fill(127);
circle(this.position.x, this.position.y, 32);
}
@@ -1047,65 +1068,65 @@
Example 4.7: ParticleSystem wit
return force;
}
}
power
variable in the Repeller
class, which controls the strength of the repulsion force exerted. This property becomes especially interesting when you have multiple attractors and repellers, each with different power values. For example, strong attractors and weak repellers might result in particles clustering around the attractors, while more powerful repellers might reveal patterns reminiscent of paths or channels between them. These are hints of what is to come in Chapter 5 when I’ll further explore the concept of a "complex system."Exercise 4.9
- Exercise 4.10
- 4.13 Image Textures and Additive Blending
-Image Textures and Additive Blending
+Example 4.8: Image texture particle system
+ Example 4.8: An Image Texture Particle System
preload()
.let img;
-preload()
.preload()
.function preload() {
//{!1} Loading the PNG
img = loadImage("texture.png");
}
-img
variable instead of drawing a circle or rectangle. show() {
imageMode(CENTER);
//{!1} Note how tint() is the image equivalent of shape’s fill().
tint(255, this.lifespan);
image(img, this.position.x, this.position.y);
}
-randomGaussian()
velocities can be initialized as follows:randomGaussian()
function, the particle velocities can be initialized as follows: let vx = randomGaussian(0, 0.3);
let vy = randomGaussian(-1, 0.3);
- let vel = createVector(vx, vy);
-function draw() {
background(0);
- //{!2} Wind force direction based on mouseX.
+ //{!2} The wind force direction is based on mouseX.
let dx = map(mouseX, 0, width, -0.2, 0.2);
let wind = createVector(dx, 0);
emitter.applyForce(wind);
@@ -1115,43 +1136,45 @@
+Example 4.8: Image texture par
emitter.addParticle();
}
}
Exercise 4.11
- Exercise 4.12
- Particle
object a different image. Even though single images are drawn by multiple particles, make sure you don’t call loadImage()
any more than you need to (once for each image file!)Particle
object a different image. Multiple particles will be drawing the same image, so make sure you don’t call loadImage()
any more than you need to. (Once for each image file is enough!)Example 4.9: Additive blending
+ Example 4.9: Additive Blending
function setup() {
- createCanvas(640, 240);
-}
-blendMode()
:blendMode()
:function draw() {
- //{!1} Additive blending
+ //{!1} Use additive blending.
blendMode(ADD);
- //{!1} Also need to clear() since the background is added and does not cover what was previously drawn.
+ //{!1} Call clear(), since the background is added and doesn't cover what was previously drawn.
clear();
- // Also note that the “glowing” effect of additive
- // blending will not work with a white
- // (or very bright) background.
+ // {!} The “glowing” effect of additive blending won't work with a white (or very bright) background.
background(0);
// {inline} All other particle stuff goes here.
}
+WEBGL
. WebGL, which stands for Web Graphics Library, is a browser-based high-performance renderer for both 2D and 3D graphics. It utilizes additional features available from your computer's graphics card. To enable it, add a third argument to createCanvas()
.function setup() {
+ // Enabling the WEBGL renderer
+ createCanvas(640, 240, WEBGL);
+}
+WEBGL
mode. However,WEBGL
mode also changes how some functions behave, and it may alter the quality of the rendering itself. Additionally, older devices or browsers may not support the renderer, in which case your sketch won’t work for as wide an audience.Exercise 4.13
tint()
in combination with additive blending to create a rainbow effect.Chapter 5. Autonomous Agents
5.1 Forces from Within
+Forces from Within
@@ -14,7 +14,7 @@
5.1 Forces from Within
5.2 Vehicles and Steering
+Vehicles and Steering
Walker
became Mover
which became Particle
. In his 1999 paper “Steering Behaviors for Autonomous Characters,” Reynolds uses the word “vehicle” to describe his autonomous agents, so I will follow suit calling the class Vehicle
. class Vehicle {
@@ -36,7 +36,7 @@
Why Vehicle?
5.3 The Steering Force
+The Steering Force
5.4 Arriving Behavior
+Arriving Behavior
Example 5.2: Arrive steering behavi
5.5 Your Own Desires: Desired Velocity
+Your Own Desires: Desired Velocity
@@ -332,7 +334,7 @@
Example 5.3: “Stay wit
Exercise 5.5
5.6 Flow Fields
+Flow Fields
5.7 The Dot Product
+The Dot Product
5.12 Combinations
+Combinations
const wind = createVector(0.001, 0);
@@ -1127,7 +1129,7 @@
Example 5.8:
Exercise 5.14
5.13 Flocking
+Flocking
Exercise 5.15
cohesion(boids) {
let neighbordist = 50;
@@ -1258,7 +1260,6 @@
Exercise 5.15
}Flock
, which will be virtually identical to the ParticleSystem
class in Chapter 4 with only one tiny change: When I call run()
on each Boid
object (as I did to each Particle
object), I’ll pass in a reference to the entire array of boids.Flock {
-
constructor() {
this.boids = [];
}
@@ -1279,7 +1280,7 @@
Exercise 5.15
Example 5.9: Flocking
Exercise 5.18
Exercise 5.19
5.14 Algorithmic Efficiency (or: Why does my sketch run so slowly?)
+Algorithmic Efficiency (or: Why does my sketch run so slowly?)
Boid
object, you have to check every other Boid
object (for its velocity and position). Let’s say you have one hundred boids. For boid #1, you need to check one hundred boids; for boid #2, you need to check one hundred boids, and so on and so forth. For one hundred boids, you need to perform one hundred times one hundred checks, or ten thousand. No problem: computers are fast and can do things ten thousand times pretty easily. Let’s try one thousand.5.14 Alg
let boids = [];
Boid
object in a two-dimensional array (repurposing the make2DArray
function from Example X.X: Flow Field Following). For each cell in the grid, an additional array tracks the objects in that particular cell.let grid = make2Darray(this.cols, this.rows);
-draw()
, each Boid
object then registers itself in the appropriate cell according to its position.let column = floor(boid.x) / this.resolution;
-let row = floor(boid.y) / this.resolution;
-grid[column][row] = boid;
-// Each cell is 40x40 pixels
+let resolution = 40;
+// How many columns and rows based on the width and height?
+let columns = floor(width / resolution);
+let rows = floor(height / resolution);
+// Create the 2D array
+let grid = make2Darray(columns, rows);
+draw()
function, each Boid
then registers itself in the appropriate cell according to its position.function draw() {
+ // Each frame, the grid is reset to empty arrays
+ for (let i = 0; i < cols; i++) {
+ for (let j = 0; j < rows; j++) {
+ grid[i][j] = [];
+ }
+ }
+
+ // Place each boid into the appropriate cell in the grid
+ for (let boid of flock.boids) {
+ // Find the right column and row
+ let column = floor(boid.position.x / resolution);
+ let row = floor(boid.position.y / resolution);
+ // Constrain to limits of array
+ column = constrain(column, 0, columns - 1);
+ row = constrain(row, 0, rows - 1);
+ // Add the boid
+ grid[column][row].push(boid);
+ }
+Example 5.10: Bin-lattice spatial subdivision
+ let column = floor(boid.x) / this.resolution;
-let row = floor(boid.y) / this.resolution;
-// Instead of looking at all
-// the boids, just this cell
-boid.flock(grid[column][row]);
+ run(boids) {
+ let column = floor(this.position.x / resolution);
+ let row = floor(this.position.y / resolution);
+ column = constrain(column, 0, columns - 1);
+ row = constrain(row, 0, rows - 1);
+ // Only these boids will be checked. See the code online for how neighboring cells are also included.
+ let neighbors = grid[column][row];
+ this.flock(neighbors);
+ this.update();
+ this.borders();
+ this.render();
+ }
5.15 A Few Last Notes: Optimization Tricks
+Example 5.11: QuadTree
+ Exercise 5.x
+ A Few Last Notes: Optimization Tricks
1) Magnitude squared (or sometimes distance squared)
1) Magnitude squared
function magSq() {
return x * x + y * y;
-}
+}draw()
, using magSq()
instead of mag()
could help your code run a wee bit faster.2) Sine and cosine lookup tables
+2) Sine and cosine lookup tables
function draw() {
for (let i = 0; i < 10000; i++) {
@@ -1400,8 +1447,14 @@
2) Sine and cosine lookup tables
Now, what if you need the value of sine of pi (or 180 degrees)?
let angle = 180; let answer = sinvalues[angle];-
A full example using this technique can be found TODO: https://github.com/nature-of-code/noc-book-2/issues/102.
-I have to admit, I am perhaps the biggest culprit of this last note. In fact, in the interest of writing clear and understandable examples, I often choose to make extra p5.Vector
objects when I absolutely do not need to. For the most part, this is not a problem at all. But sometimes, it can be. Let’s take a look at an example.
function draw() { for (let v of vehicles) { diff --git a/content/examples/04_particles/noc_4_01_single_particle/particle.js b/content/examples/04_particles/noc_4_01_single_particle/particle.js index da07c7f8..4eae3991 100644 --- a/content/examples/04_particles/noc_4_01_single_particle/particle.js +++ b/content/examples/04_particles/noc_4_01_single_particle/particle.js @@ -23,9 +23,6 @@ class Particle { this.acceleration.mult(0); } - applyForce(force) { - this.acceleration.add(force); - } show() { stroke(0, this.lifespan); @@ -40,6 +37,6 @@ class Particle { //{!3} Is the Particle alive or dead? isDead() { - return (this.lifespan < 0.0); + return (this.lifespan < 0); } } \ No newline at end of file diff --git a/content/examples/05_steering/example_5_9_flocking/boid.js b/content/examples/05_steering/example_5_9_flocking/boid.js new file mode 100644 index 00000000..67d36b01 --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking/boid.js @@ -0,0 +1,173 @@ +// The Nature of Code +// Daniel Shiffman +// http://natureofcode.com + +// Boid class +// Methods for Separation, Cohesion, Alignment added + +class Boid { + constructor(x, y) { + this.acceleration = createVector(0, 0); + this.velocity = createVector(random(-1, 1), random(-1, 1)); + this.position = createVector(x, y); + this.r = 3.0; + this.maxspeed = 3; // Maximum speed + this.maxforce = 0.05; // Maximum steering force + } + + run(boids) { + this.flock(boids); + this.update(); + this.borders(); + this.render(); + } + + applyForce(force) { + // We could add mass here if we want A = F / M + this.acceleration.add(force); + } + + // We accumulate a new acceleration each time based on three rules + flock(boids) { + let sep = this.separate(boids); // Separation + let ali = this.align(boids); // Alignment + let coh = this.cohesion(boids); // Cohesion + // Arbitrarily weight these forces + sep.mult(1.5); + ali.mult(1.0); + coh.mult(1.0); + // Add the force vectors to acceleration + this.applyForce(sep); + this.applyForce(ali); + this.applyForce(coh); + } + + // Method to update location + update() { + // Update velocity + this.velocity.add(this.acceleration); + // Limit speed + this.velocity.limit(this.maxspeed); + this.position.add(this.velocity); + // Reset accelertion to 0 each cycle + this.acceleration.mult(0); + } + + // A method that calculates and applies a steering force towards a target + // STEER = DESIRED MINUS VELOCITY + seek(target) { + let desired = p5.Vector.sub(target, this.position); // A vector pointing from the location to the target + // Normalize desired and scale to maximum speed + desired.normalize(); + desired.mult(this.maxspeed); + // Steering = Desired minus Velocity + let steer = p5.Vector.sub(desired, this.velocity); + steer.limit(this.maxforce); // Limit to maximum steering force + return steer; + } + + render() { + // Draw a triangle rotated in the direction of velocity + let theta = this.velocity.heading() + radians(90); + fill(127); + stroke(0); + push(); + translate(this.position.x, this.position.y); + rotate(theta); + beginShape(); + vertex(0, -this.r * 2); + vertex(-this.r, this.r * 2); + vertex(this.r, this.r * 2); + endShape(CLOSE); + pop(); + } + + // Wraparound + borders() { + if (this.position.x < -this.r) this.position.x = width + this.r; + if (this.position.y < -this.r) this.position.y = height + this.r; + if (this.position.x > width + this.r) this.position.x = -this.r; + if (this.position.y > height + this.r) this.position.y = -this.r; + } + + // Separation + // Method checks for nearby boids and steers away + separate(boids) { + let desiredseparation = 25.0; + let steer = createVector(0, 0); + let count = 0; + // For every boid in the system, check if it's too close + for (let i = 0; i < boids.length; i++) { + let d = p5.Vector.dist(this.position, boids[i].position); + // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself) + if ((d > 0) && (d < desiredseparation)) { + // Calculate vector pointing away from neighbor + let diff = p5.Vector.sub(this.position, boids[i].position); + diff.normalize(); + diff.div(d); // Weight by distance + steer.add(diff); + count++; // Keep track of how many + } + } + // Average -- divide by how many + if (count > 0) { + steer.div(count); + } + + // As long as the vector is greater than 0 + if (steer.mag() > 0) { + // Implement Reynolds: Steering = Desired - Velocity + steer.normalize(); + steer.mult(this.maxspeed); + steer.sub(this.velocity); + steer.limit(this.maxforce); + } + return steer; + } + + // Alignment + // For every nearby boid in the system, calculate the average velocity + align(boids) { + let neighbordist = 50; + let sum = createVector(0, 0); + let count = 0; + for (let i = 0; i < boids.length; i++) { + let d = p5.Vector.dist(this.position, boids[i].position); + if ((d > 0) && (d < neighbordist)) { + sum.add(boids[i].velocity); + count++; + } + } + if (count > 0) { + sum.div(count); + sum.normalize(); + sum.mult(this.maxspeed); + let steer = p5.Vector.sub(sum, this.velocity); + steer.limit(this.maxforce); + return steer; + } else { + return createVector(0, 0); + } + } + + // Cohesion + // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location + cohesion(boids) { + let neighbordist = 50; + let sum = createVector(0, 0); // Start with empty vector to accumulate all locations + let count = 0; + for (let i = 0; i < boids.length; i++) { + let d = p5.Vector.dist(this.position, boids[i].position); + if ((d > 0) && (d < neighbordist)) { + sum.add(boids[i].position); // Add location + count++; + } + } + if (count > 0) { + sum.div(count); + return this.seek(sum); // Steer towards the location + } else { + return createVector(0, 0); + } + } +} \ No newline at end of file diff --git a/content/examples/05_steering/example_5_9_flocking/flock.js b/content/examples/05_steering/example_5_9_flocking/flock.js new file mode 100644 index 00000000..d0f56010 --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking/flock.js @@ -0,0 +1,24 @@ +// The Nature of Code +// Daniel Shiffman +// http://natureofcode.com + +// Flock object +// Does very little, simply manages the array of all the boids + +class Flock { + + constructor() { + // An array for all the boids + this.boids = []; // Initialize the array + } + + run() { + for (let boid of this.boids) { + boid.run(this.boids); // Passing the entire list of boids to each boid individually + } + } + + addBoid(b) { + this.boids.push(b); + } +} \ No newline at end of file diff --git a/content/examples/05_steering/example_5_9_flocking/index.html b/content/examples/05_steering/example_5_9_flocking/index.html new file mode 100644 index 00000000..05a3e37d --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking/index.html @@ -0,0 +1,14 @@ + + + + + +The Nature of Code Example 6.9 Flocking + + + + + + + + diff --git a/content/examples/05_steering/example_5_9_flocking/sketch.js b/content/examples/05_steering/example_5_9_flocking/sketch.js new file mode 100644 index 00000000..4d21a943 --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking/sketch.js @@ -0,0 +1,31 @@ +// The Nature of Code +// Daniel Shiffman +// http://natureofcode.com + +// Demonstration of Craig Reynolds' "Flocking" behavior +// See: http://www.red3d.com/cwr/ +// Rules: Cohesion, Separation, Alignment + +// Click mouse to add boids into the system + +let flock; + +function setup() { + createCanvas(640, 240); + flock = new Flock(); + // Add an initial set of boids into the system + for (let i = 0; i < 120; i++) { + let boid = new Boid(width / 2, height / 2); + flock.addBoid(boid); + } +} + +function draw() { + background(255); + flock.run(); +} + +// Add a new boid into the System +function mouseDragged() { + flock.addBoid(new Boid(mouseX, mouseY)); +} diff --git a/content/examples/05_steering/example_5_9_flocking/style.css b/content/examples/05_steering/example_5_9_flocking/style.css new file mode 100644 index 00000000..130b7d69 --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking/style.css @@ -0,0 +1,7 @@ +html, body { + margin: 0; + padding: 0; +} +canvas { + display: block; +} diff --git a/content/examples/05_steering/example_5_9_flocking_with_binning/boid.js b/content/examples/05_steering/example_5_9_flocking_with_binning/boid.js new file mode 100644 index 00000000..a10fd6bc --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking_with_binning/boid.js @@ -0,0 +1,190 @@ +// The Nature of Code +// Daniel Shiffman +// http://natureofcode.com + +// Boid class +// Methods for Separation, Cohesion, Alignment added + +class Boid { + constructor(x, y) { + this.acceleration = createVector(0, 0); + this.velocity = createVector(random(-1, 1), random(-1, 1)); + this.position = createVector(x, y); + this.r = 3.0; + this.maxspeed = 3; // Maximum speed + this.maxforce = 0.05; // Maximum steering force + } + + run() { + let col = Math.floor(this.position.x / resolution); + let row = Math.floor(this.position.y / resolution); + let neighbors = []; + + // Check cells in a 3x3 block around the current boid + for (let i = -1; i <= 1; i++) { + for (let j = -1; j <= 1; j++) { + let newCol = col + i; + let newRow = row + j; + // Make sure this is a valid cell + if (newCol >= 0 && newCol < cols && newRow >= 0 && newRow < rows) { + // Add all boids in this cell to neighbors + neighbors = neighbors.concat(grid[newCol][newRow]); + } + } + } + + this.flock(neighbors); + this.update(); + this.borders(); + this.render(); + } + + applyForce(force) { + // We could add mass here if we want A = F / M + this.acceleration.add(force); + } + + // We accumulate a new acceleration each time based on three rules + flock(boids) { + let sep = this.separate(boids); // Separation + let ali = this.align(boids); // Alignment + let coh = this.cohesion(boids); // Cohesion + // Arbitrarily weight these forces + sep.mult(1.5); + ali.mult(1.0); + coh.mult(1.0); + // Add the force vectors to acceleration + this.applyForce(sep); + this.applyForce(ali); + this.applyForce(coh); + } + + // Method to update location + update() { + // Update velocity + this.velocity.add(this.acceleration); + // Limit speed + this.velocity.limit(this.maxspeed); + this.position.add(this.velocity); + // Reset accelertion to 0 each cycle + this.acceleration.mult(0); + } + + // A method that calculates and applies a steering force towards a target + // STEER = DESIRED MINUS VELOCITY + seek(target) { + let desired = p5.Vector.sub(target, this.position); // A vector pointing from the location to the target + // Normalize desired and scale to maximum speed + desired.normalize(); + desired.mult(this.maxspeed); + // Steering = Desired minus Velocity + let steer = p5.Vector.sub(desired, this.velocity); + steer.limit(this.maxforce); // Limit to maximum steering force + return steer; + } + + render() { + // Draw a triangle rotated in the direction of velocity + let theta = this.velocity.heading() + radians(90); + fill(127); + stroke(0); + push(); + translate(this.position.x, this.position.y); + rotate(theta); + beginShape(); + vertex(0, -this.r * 2); + vertex(-this.r, this.r * 2); + vertex(this.r, this.r * 2); + endShape(CLOSE); + pop(); + } + + // Wraparound + borders() { + if (this.position.x < -this.r) this.position.x = width + this.r; + if (this.position.y < -this.r) this.position.y = height + this.r; + if (this.position.x > width + this.r) this.position.x = -this.r; + if (this.position.y > height + this.r) this.position.y = -this.r; + } + + // Separation + // Method checks for nearby boids and steers away + separate(boids) { + let desiredseparation = 25.0; + let steer = createVector(0, 0); + let count = 0; + // For every boid in the system, check if it's too close + for (let i = 0; i < boids.length; i++) { + let d = p5.Vector.dist(this.position, boids[i].position); + // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself) + if (d > 0 && d < desiredseparation) { + // Calculate vector pointing away from neighbor + let diff = p5.Vector.sub(this.position, boids[i].position); + diff.normalize(); + diff.div(d); // Weight by distance + steer.add(diff); + count++; // Keep track of how many + } + } + // Average -- divide by how many + if (count > 0) { + steer.div(count); + } + + // As long as the vector is greater than 0 + if (steer.mag() > 0) { + // Implement Reynolds: Steering = Desired - Velocity + steer.normalize(); + steer.mult(this.maxspeed); + steer.sub(this.velocity); + steer.limit(this.maxforce); + } + return steer; + } + + // Alignment + // For every nearby boid in the system, calculate the average velocity + align(boids) { + let neighbordist = 50; + let sum = createVector(0, 0); + let count = 0; + for (let i = 0; i < boids.length; i++) { + let d = p5.Vector.dist(this.position, boids[i].position); + if (d > 0 && d < neighbordist) { + sum.add(boids[i].velocity); + count++; + } + } + if (count > 0) { + sum.div(count); + sum.normalize(); + sum.mult(this.maxspeed); + let steer = p5.Vector.sub(sum, this.velocity); + steer.limit(this.maxforce); + return steer; + } else { + return createVector(0, 0); + } + } + + // Cohesion + // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location + cohesion(boids) { + let neighbordist = 50; + let sum = createVector(0, 0); // Start with empty vector to accumulate all locations + let count = 0; + for (let i = 0; i < boids.length; i++) { + let d = p5.Vector.dist(this.position, boids[i].position); + if (d > 0 && d < neighbordist) { + sum.add(boids[i].position); // Add location + count++; + } + } + if (count > 0) { + sum.div(count); + return this.seek(sum); // Steer towards the location + } else { + return createVector(0, 0); + } + } +} diff --git a/content/examples/05_steering/example_5_9_flocking_with_binning/flock.js b/content/examples/05_steering/example_5_9_flocking_with_binning/flock.js new file mode 100644 index 00000000..63f42fb9 --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking_with_binning/flock.js @@ -0,0 +1,24 @@ +// The Nature of Code +// Daniel Shiffman +// http://natureofcode.com + +// Flock object +// Does very little, simply manages the array of all the boids + +class Flock { + + constructor() { + // An array for all the boids + this.boids = []; // Initialize the array + } + + run() { + for (let boid of this.boids) { + boid.run(this.boids); // Passing the entire list of boids to each boid individually + } + } + + addBoid(boid) { + this.boids.push(boid); + } +} \ No newline at end of file diff --git a/content/examples/05_steering/example_5_9_flocking_with_binning/index.html b/content/examples/05_steering/example_5_9_flocking_with_binning/index.html new file mode 100644 index 00000000..05a3e37d --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking_with_binning/index.html @@ -0,0 +1,14 @@ + + + + + +The Nature of Code Example 6.9 Flocking + + + + + + + + diff --git a/content/examples/05_steering/example_5_9_flocking_with_binning/sketch.js b/content/examples/05_steering/example_5_9_flocking_with_binning/sketch.js new file mode 100644 index 00000000..1004bb2e --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking_with_binning/sketch.js @@ -0,0 +1,97 @@ +// The Nature of Code +// Daniel Shiffman +// http://natureofcode.com + +// Demonstration of Craig Reynolds' "Flocking" behavior +// See: http://www.red3d.com/cwr/ +// Rules: Cohesion, Separation, Alignment + +// Click mouse to add boids into the system + +let flock; + +// bin-lattice spatial subdivision +let grid; +let cols; +let rows; +let resolution = 40; // adjust as necessary + +function make2DArray(cols, rows) { + let arr = new Array(cols); + for (let i = 0; i < arr.length; i++) { + arr[i] = new Array(rows); + } + return arr; +} + +function setup() { + createCanvas(640, 240); + cols = floor(width / resolution); + rows = floor(height / resolution); + grid = make2DArray(cols, rows); + flock = new Flock(); + // Add an initial set of boids into the system + for (let i = 0; i < 800; i++) { + let boid = new Boid(random(width), random(height)); + flock.addBoid(boid); + } +} + +function draw() { + background(255); + + // Reset grid at the beginning of each frame + for (let i = 0; i < cols; i++) { + for (let j = 0; j < rows; j++) { + grid[i][j] = []; + } + } + + // Place each boid into the appropriate cell in the grid + for (let boid of flock.boids) { + let col = floor(boid.position.x / resolution); + let row = floor(boid.position.y / resolution); + col = constrain(col, 0, cols - 1); + row = constrain(row, 0, rows - 1); + grid[col][row].push(boid); + } + + // Draw the grid + stroke(200); + strokeWeight(1); + + // Draw vertical lines + for (let i = 0; i <= cols; i++) { + let x = i * resolution; + line(x, 0, x, height); + } + + // Draw horizontal lines + for (let j = 0; j <= rows; j++) { + let y = j * resolution; + line(0, y, width, y); + } + + // Highlight the 3x3 neighborhood the mouse is over + let mouseCol = floor(mouseX / resolution); + let mouseRow = floor(mouseY / resolution); + noStroke(); + fill(255, 50, 50, 100); // Semi-transparent red + for (let i = -1; i <= 1; i++) { + for (let j = -1; j <= 1; j++) { + let col = mouseCol + i; + let row = mouseRow + j; + // Check if the cell is within the grid + if (col >= 0 && col < cols && row >= 0 && row < rows) { + rect(col * resolution, row * resolution, resolution, resolution); + } + } + } + + flock.run(); +} + +// Add a new boid into the System +function mouseDragged() { + flock.addBoid(new Boid(mouseX, mouseY)); +} diff --git a/content/examples/05_steering/example_5_9_flocking_with_binning/style.css b/content/examples/05_steering/example_5_9_flocking_with_binning/style.css new file mode 100644 index 00000000..130b7d69 --- /dev/null +++ b/content/examples/05_steering/example_5_9_flocking_with_binning/style.css @@ -0,0 +1,7 @@ +html, body { + margin: 0; + padding: 0; +} +canvas { + display: block; +} diff --git a/content/examples/05_steering/quadtree_part_1_copy/index.html b/content/examples/05_steering/quadtree_part_1_copy/index.html new file mode 100644 index 00000000..56637061 --- /dev/null +++ b/content/examples/05_steering/quadtree_part_1_copy/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content/examples/05_steering/quadtree_part_1_copy/quadtree.js b/content/examples/05_steering/quadtree_part_1_copy/quadtree.js new file mode 100644 index 00000000..5bfc8ded --- /dev/null +++ b/content/examples/05_steering/quadtree_part_1_copy/quadtree.js @@ -0,0 +1,140 @@ +// Daniel Shiffman +// http://codingtra.in +// http://patreon.com/codingtrain + +// QuadTree +// 1: https://www.youtube.com/watch?v=OJxEcs0w_kE +// 2: https://www.youtube.com/watch?v=QQx_NmCIuCY + +// For more: +// https://github.com/CodingTrain/QuadTree + +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +class Rectangle { + constructor(x, y, w, h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + contains(point) { + return (point.x >= this.x - this.w && + point.x < this.x + this.w && + point.y >= this.y - this.h && + point.y < this.y + this.h); + } + + intersects(range) { + return !(range.x - range.w > this.x + this.w || + range.x + range.w < this.x - this.w || + range.y - range.h > this.y + this.h || + range.y + range.h < this.y - this.h); + } + + +} + +class QuadTree { + constructor(boundary, n) { + this.boundary = boundary; + this.capacity = n; + this.points = []; + this.divided = false; + } + + subdivide() { + let x = this.boundary.x; + let y = this.boundary.y; + let w = this.boundary.w; + let h = this.boundary.h; + let ne = new Rectangle(x + w / 2, y - h / 2, w / 2, h / 2); + this.northeast = new QuadTree(ne, this.capacity); + let nw = new Rectangle(x - w / 2, y - h / 2, w / 2, h / 2); + this.northwest = new QuadTree(nw, this.capacity); + let se = new Rectangle(x + w / 2, y + h / 2, w / 2, h / 2); + this.southeast = new QuadTree(se, this.capacity); + let sw = new Rectangle(x - w / 2, y + h / 2, w / 2, h / 2); + this.southwest = new QuadTree(sw, this.capacity); + this.divided = true; + } + + insert(point) { + + if (!this.boundary.contains(point)) { + return false; + } + + if (this.points.length < this.capacity) { + this.points.push(point); + return true; + } else { + if (!this.divided) { + this.subdivide(); + } + if (this.northeast.insert(point)) { + return true; + } else if (this.northwest.insert(point)) { + return true; + } else if (this.southeast.insert(point)) { + return true; + } else if (this.southwest.insert(point)) { + return true; + } + } + } + + query(range, found) { + if (!found) { + found = []; + } + if (!this.boundary.intersects(range)) { + return; + } else { + for (let p of this.points) { + if (range.contains(p)) { + found.push(p); + } + } + if (this.divided) { + this.northwest.query(range, found); + this.northeast.query(range, found); + this.southwest.query(range, found); + this.southeast.query(range, found); + } + } + return found; + } + + + show() { + stroke(0); + noFill(); + strokeWeight(1); + rectMode(CENTER); + rect(this.boundary.x, this.boundary.y, this.boundary.w * 2, this.boundary.h * 2); + for (let p of this.points) { + strokeWeight(1); + stroke(0); + point(p.x, p.y); + } + + if (this.divided) { + this.northeast.show(); + this.northwest.show(); + this.southeast.show(); + this.southwest.show(); + } + } + + + + + +} diff --git a/content/examples/05_steering/quadtree_part_1_copy/sketch.js b/content/examples/05_steering/quadtree_part_1_copy/sketch.js new file mode 100644 index 00000000..a673c53c --- /dev/null +++ b/content/examples/05_steering/quadtree_part_1_copy/sketch.js @@ -0,0 +1,46 @@ +// Daniel Shiffman +// http://codingtra.in +// http://patreon.com/codingtrain + +// QuadTree +// 1: https://www.youtube.com/watch?v=OJxEcs0w_kE +// 2: https://www.youtube.com/watch?v=QQx_NmCIuCY + +// For more: +// https://github.com/CodingTrain/QuadTree + +let qtree; + +function setup() { + createCanvas(640, 240); + let boundary = new Rectangle(width / 2, height / 2, width, height); + qtree = new QuadTree(boundary, 8); + for (let i = 0; i < 2000; i++) { + let x = randomGaussian(width / 2, width / 8); + let y = randomGaussian(height / 2, height / 8); + let p = new Point(x, y); + qtree.insert(p); + } +} + +function draw() { + background(255); + qtree.show(); + + rectMode(CENTER); + let range = new Rectangle(mouseX, mouseY, 50, 50); + + // This check has been introduced due to a bug discussed in https://github.com/CodingTrain/website/pull/556 + if (mouseX < width && mouseY < height) { + strokeWeight(2); + stroke(255, 50, 50); + fill(255, 50, 50, 50); + rect(range.x, range.y, range.w * 2, range.h * 2); + let points = qtree.query(range); + for (let p of points) { + strokeWeight(3); + stroke(50, 50, 50); + point(p.x, p.y); + } + } +} diff --git a/content/examples/05_steering/quadtree_part_1_copy/style.css b/content/examples/05_steering/quadtree_part_1_copy/style.css new file mode 100644 index 00000000..9386f1c2 --- /dev/null +++ b/content/examples/05_steering/quadtree_part_1_copy/style.css @@ -0,0 +1,7 @@ +html, body { + margin: 0; + padding: 0; +} +canvas { + display: block; +} diff --git a/content/examples/05_steering/sine_cosine_lookup_table/index.html b/content/examples/05_steering/sine_cosine_lookup_table/index.html new file mode 100644 index 00000000..b9d16ffa --- /dev/null +++ b/content/examples/05_steering/sine_cosine_lookup_table/index.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/content/examples/05_steering/sine_cosine_lookup_table/sketch.js b/content/examples/05_steering/sine_cosine_lookup_table/sketch.js new file mode 100644 index 00000000..a6a71e04 --- /dev/null +++ b/content/examples/05_steering/sine_cosine_lookup_table/sketch.js @@ -0,0 +1,51 @@ +/* +sincoslookup taken from http://wiki.processing.org/index.php/Sin/Cos_look-up_table +archived version http://web.archive.org/web/20130510100827/http://wiki.processing.org/w/Sin/Cos_look-up_table +*/ + +// declare arrays and params for storing sin/cos values +let sinLUT; +let cosLUT; +// set table precision to 0.5 degrees +const SC_PRECISION = 0.5; +// caculate reciprocal for conversions +const SC_INV_PREC = 1 / SC_PRECISION; +// compute required table length +const SC_PERIOD = Math.floor(360 * SC_INV_PREC); + +// init sin/cos tables with values +// should be called from setup() +function initSinCos() { + sinLUT = []; + cosLUT = []; + for (let i = 0; i < SC_PERIOD; i++) { + sinLUT[i] = Math.sin(i * DEG_TO_RAD * SC_PRECISION); + cosLUT[i] = Math.cos(i * DEG_TO_RAD * SC_PRECISION); + } +} + +// circle radius used for example +let radius; + +function setup() { + createCanvas(200, 200); + initSinCos(); // important call to initialize lookup tables +} + +function draw() { + background(255); + // modulate the current radius + radius = 50 + 50 * sinLUT[frameCount % SC_PERIOD]; + + // draw a circle made of points (every 5 degrees) + for (let i = 0; i < 360; i += 5) { + // convert degrees into array index: + // the modulo operator (%) ensures periodicity + let theta = int((i * SC_INV_PREC) % SC_PERIOD); + // draw the circle around mouse pos + point( + mouseX + radius * cosLUT[theta], + mouseY + radius * sinLUT[theta] + ); + } +} \ No newline at end of file diff --git a/content/examples/05_steering/sine_cosine_lookup_table/style.css b/content/examples/05_steering/sine_cosine_lookup_table/style.css new file mode 100644 index 00000000..9386f1c2 --- /dev/null +++ b/content/examples/05_steering/sine_cosine_lookup_table/style.css @@ -0,0 +1,7 @@ +html, body { + margin: 0; + padding: 0; +} +canvas { + display: block; +}