The world around us moves in complicated and wonderful ways. We spend the earlier parts of our lives learning about our environment through perception and interaction. We expect the physical world around us to behave consistently with our perceptual memory, e.g., if we drop a rock, it will fall due to gravity, if a gust of wind blows, lighter objects will be tossed by the wind further. This class focuses on understanding, simulating, and incorporating motion-based elements of our physical world into the digital worlds that we create. Our hope is to create intuitive, rich, and more satisfying experiences by drawing from the perceptual memories of our users.
—James Tu, Dynamic Bodies course description, Spring 2003, ITP
-
In 2003, as a graduate student at the Interactive Telecommunications Program (ITP) in the Tisch School of the Arts at New York University, I enrolled in a course called Dynamic Bodies. The course was taught by interaction designer and ITP adjunct professor James Tu. At the time, my work was focused on a series of software experiments that generated real-time, non-photorealistic imagery. The applications involved capturing images from a live source and “painting” the colors with elements that moved about the screen according to various rules. Tu’s course—which covered vectors, forces, oscillations, particle systems, recursion, steering, and springs—aligned perfectly with my work.
-
I had been using these concepts informally in my own projects but had never taken the time to closely examine the science behind the algorithms or learn object-oriented techniques to formalize their implementation. That very semester, I also enrolled in Foundations of Generative Art Systems, a course taught by Philip Galanter that focused on the theory and practice of generative art, covering topics such as chaos, cellular automata, genetic algorithms, neural networks, and fractals. Both Tu’s and Galanter’s courses opened my eyes to the world of simulation algorithms, techniques that carried me through the next several years of work and teaching and that served as the foundation and inspiration for this book.
+
In 2003, as a graduate student at the Interactive Telecommunications Program (ITP) in the Tisch School of the Arts at New York University, I enrolled in a course called Dynamic Bodies. The course was taught by interaction designer and ITP adjunct professor James Tu. At the time, my work was focused on a series of software experiments that generated real-time, nonphotorealistic imagery. The applications involved capturing images from a live source and “painting” the colors with elements that moved about the screen according to various rules. Tu’s course—which covered vectors, forces, oscillations, particle systems, recursion, steering, and springs—aligned perfectly with my work.
+
I had been using these concepts informally in my own projects but had never taken the time to closely examine the science behind the algorithms or learn object-oriented techniques to formalize their implementation. That very semester, I also enrolled in Foundations of Generative Art Systems, a course taught by Philip Galanter that focused on the theory and practice of generative art, covering topics such as chaos, cellular automata, genetic algorithms, neural networks, and fractals. Both Tu’s and Galanter’s courses opened my eyes to the world of simulation algorithms—techniques that carried me through the next several years of work and teaching and that served as the foundation and inspiration for this book.
But another piece of the puzzle is missing from this story.
Galanter’s course was mostly theory based, while Tu’s was taught using Macromedia Director and the Lingo programming language. That semester, I learned many of the algorithms by translating them into C++ (the language I was using quite awkwardly at the time, well before C++ creative coding environments like openFrameworks and Cinder had arrived). Toward the end of the semester, however, I discovered something called Processing. Processing was in alpha then (version 0055), and having had some experience with Java, I was intrigued enough to ask the question, Could this open source, artist-friendly programming language and environment be the right place to develop a suite of tutorials and examples about programming and simulation? With the support of the ITP and Processing communities, I embarked on what has now been an almost 20-year journey of teaching coding.
I’d like to first thank Red Burns, who led ITP through its first 30 years and passed away in 2013. Red supported and encouraged me in my work for well over 10 years. Dan O’Sullivan, the associate dean of Emerging Media at the Tisch School of the Arts, has been a mentor and was the first to suggest that I try teaching a course on Processing, giving me a reason to start assembling programming tutorials in the first place. Shawn Van Every, current chair of the department, was my officemate during my first year of teaching full-time, and has been a rich source of help and inspiration over the years. I am grateful for the support and encouragement of ITP professor Luisa Pereira. Her work on her upcoming book, Code of Music, was deeply inspiring. Her innovative approach to interactive educational materials helped me rethink and redefine my own writing and publishing process.
@@ -13,19 +13,19 @@
Acknowledgments
The dedicated and tireless staff at ITP and NYU’s Interactive Media Arts (IMA) play such a vital role in keeping the ecosystem thriving and making everything. My thanks go to the many people I’ve worked with over the years: Adrian Mandeville, Brian Kim, Daniel Tsadok, Dante Delgiacco, Edward Gordon, Emma Asumeng, George Agudow, John Duane, Lenin Compres, Luke Bunn, Marlon Evans, Matt Berger, Megan Demarest, Midori Yasuda, Phil Caridi, Rob Ryan, Scott Broussard, and Shirley Lin.
A special note of thanks goes to ITP adjunct faculty members Ellen Nickles and Nuntinee Tansrisakul, who co-taught an online, asynchronous version of the Nature of Code course with me in 2021, amid the peak of a global pandemic. Their contributions and the ideas from that semester greatly enriched the course materials.
The students of ITP and IMA, too numerous to mention, have been an amazing source of feedback throughout this process. Much of the material in this book comes from my course of the same title, which I’ve now taught 17 times. I have stacks of draft printouts of the book with notes scrawled in the margins, as well as a vast archive of student emails with corrections, comments, and generous words of encouragement.
-
I would like to spotlight several students who worked as graduate associates on the Nature of Code materials. Through their work with the ITP/IMA Equitable Syllabus project, Chaski No and Briana Jones provided extraordinary research support that expanded the book’s concepts and references. As the graduate assistant for the inaugural undergraduate version of the Nature of Code class, Gracy Whelihan offered invaluable support and feedback, and always reminded me of the wonder of random numbers.
+
I would like to spotlight several students who worked as graduate associates on the Nature of Code materials. Through their work with the ITP/IMA Equitable Syllabus project, Briana Jones and Chaski No provided extraordinary research support that expanded the book’s concepts and references. As the graduate assistant for the inaugural undergraduate version of the Nature of Code class, Gracy Whelihan offered invaluable support and feedback, and always reminded me of the wonder of random numbers.
Jason Gao and Stuti Mohgaonkar worked on the build systems for the book materials, inventing new workflows for writing and editing. Elias Jarzombek also warrants a mention for his advice and technical support, stemming from the Code of Music book project.
After graduating, Jason Gao continued to develop the Nature of Code website. If you head there now, you will see the fruits of his many talents: a full version of the book that seamlessly integrates with the p5.js web editor. It’s a realization far beyond my initial vision.
The interior of the book along with the website was meticulously designed by Tuan Huang. Tuan began developing layout ideas while taking the Nature of Code class in the spring of 2023. After graduating, Tuan further refined the design, working to develop a consistent visual language across the many elements of the book. Her minimal and elegant aesthetics greatly enhanced the book’s visual appeal and accessibility. A special thanks also to the OpenMoji project—the open source emoji and icon project (Creative Commons license CC BY-SA 4.0)—for providing a delightful and comprehensive set of emojis used throughout this book for various elements.
I’m also indebted to the energetic and supportive creative coding community and the Processing Foundation. I wouldn’t be writing this book if it weren’t for Casey Reas and Ben Fry, who created Processing in 2001 and co-founded the Processing Foundation. They’ve dedicated over 20 years to building and maintaining the software and its community. I’ve learned half of what I know simply from reading through the Processing source code and documentation; the elegant simplicity of the Processing language, website, and IDE is the original source of inspiration for all my teaching and work.
Lauren Lee McCarthy, the creator of p5.js, planted the seed that made everything possible for transforming the book into JavaScript. She’s a tireless champion for inclusion and access in open source, and her approach to community building has been profoundly inspiring to me. Cassie Tarakajian invented the p5.js web editor, a heroic undertaking that has made it possible to collect and organize all the example code in the book.
-
My heartfelt thanks extends to the other current and former members (along with Casey, Ben, and Lauren) of the Processing board of directors: Dorothy Santos, Kate Hollenbach, Xin Xin, and Johanna Hedva. A special acknowledgment to the project leads, staff, and alumni of the foundation, who have each played a pivotal role in shaping and propelling the community and its projects: Andres Colubri, Charles Reinhardt, evelyn masso, Jesse C Thompson, Jonathan Feinberg, Moira Turner, Qianqian Ye, Rachel Lim, Raphaël de Courville, Saber Khan, Suhyun (Sonia) Choi, Toni Pizza, Tsige Tafesse, and Xiaowei R. Wang.
+
My heartfelt thanks extend to the other current and former members (along with Casey, Ben, and Lauren) of the Processing board of directors: Dorothy Santos, Johanna Hedva, Kate Hollenbach, and Xin Xin. A special acknowledgment to the project leads, staff, and alumni of the foundation, who have each played a pivotal role in shaping and propelling the community and its projects: Andres Colubri, Charles Reinhardt, evelyn masso, Jesse C. Thompson, Jonathan Feinberg, Moira Turner, Qianqian Ye, Rachel Lim, Raphaël de Courville, Saber Khan, Suhyun (Sonia) Choi, Toni Pizza, Tsige Tafesse, and Xiaowei R. Wang.
In Chapter 10, I introduce the ml5.js project, a companion library to p5.js that aims to bring machine learning capabilities to creative coders in a friendly and approachable manner. Thank you to the numerous researchers and students at ITP/IMA who contributed to its development: Ashley Lewis, Bomani McClendon, Christina Dacanay, Cristóbal Valenzuela, Lydia Jessup, Miaoye Que, Michael Weinberg, Ozi Chukwukeme, Sam Krystal, Yining Shi, and Ziyuan (Peter) Lin. I’d like to especially thank Joey K. Lee, who provided valuable advice and feedback on the Nature of Code book itself while working on ml5.js.
I would also like to thank AI researcher David Ha, whose research on neuroevolution (see “Additional Resources” on the book’s website) inspired me to create examples implementing the technique with ml5.js and add a new chapter to this book.
For the last 10 years, I’ve spent the bulk of my time making video tutorials on my YouTube channel, the Coding Train. I’m incredibly grateful for the immense support and collaboration from so many people in keeping the engines running and on the tracks (as much as I work very hard to veer off), including Chloe Desaulles, Cy X, David Snyder, Dusk Virkus, Elizabeth Perez, Jason Heglund, Katie Chan, Kline Gareth, Kobe Liesenborgs, and Mathieu Blanchette. A special thanks to Melissa Rodriguez, who helped research and secure permissions for the images you see at the start of each chapter.
-
My thanks also extend to the Nebula streaming platform and its CEO Dave Wiskus for their unwavering support, and to Nebula creator Grady Hillhouse, who recommended I collaborate with No Starch Press to actually print this darn thing. I wouldn’t be able to reach such a wide audience without the YouTube platform itself; a special thanks goes to my illustrious YouTube partner manager Dean Kowalski, as well as to Doreen Tran, who helps lead YouTube Skilling for North America.
-
I have many thoughtful, smart, generous, and kind viewers. I’d like to especially thank Dipam Sen, Francis Turmel, Simon Tiger, and Kathy McGuiness, who offered advice, feedback, corrections, technical support, and more. The book is so much better because of them.
-
I also would like to thank many people who collaborated with me over 10 years ago on the 2012 edition: David Wilson (book cover and design), Rune Madsen and Steve Klise (build system and website), Shannon Fry (editing), Evan Emolo, Miguel Bermudez, and all of the Kickstarter backers who helped fund the work.
+
My thanks also extend to the Nebula streaming platform and its CEO, Dave Wiskus, for their unwavering support, and to Nebula creator Grady Hillhouse, who recommended I collaborate with No Starch Press to actually print this darn thing. I wouldn’t be able to reach such a wide audience without the YouTube platform itself; a special thanks goes to my illustrious YouTube partner manager, Dean Kowalski, as well as to Doreen Tran, who helps lead YouTube Skilling for North America.
+
I have many thoughtful, smart, generous, and kind viewers. I’d like to especially thank Dipam Sen, Francis Turmel, Kathy McGuiness, and Simon Tiger, who offered advice, feedback, corrections, technical support, and more. The book is so much better because of them.
+
I also would like to thank many people who collaborated with me over 10 years ago on the 2012 edition: David Wilson (book cover and design), Rune Madsen and Steve Klise (build system and website), Shannon Fry (editing), Evan Emolo, Miguel Bermudez, and all the Kickstarter backers who helped fund the work.
A special mention goes to Zannah Marsh, who worked tirelessly to create over 100 illustrations for the 2012 version of this book, and by some miracle agreed to do it all again for this new edition. I especially want to thank her for her patience and willingness to go with the flow as I changed my mind on certain illustrations way too many times. And the cats! I smile from ear to ear every time I see them typing.
Now, the real reason we’re all here. If it weren’t for No Starch Press, I’m almost certain you’d never be reading these words. Sure, you might be perusing updated tutorials on the website, but the collaboration, support, and thoughtful and kind deadline setting of the team was the thing that really pushed me over the hump. I want to express my gratitude to editor Nathan Heidelberger, who is responsible for this book making any sense at all, not to mention for all the legitimately funny jokes. (The blame for any bad puns lies squarely with me.) Thank you to Jasper Palfree, the technical editor, who patiently explained to me, as many times as it took for me to grok, the difference between linear and rotational motion (and clarified countless other science and code concepts). I also want to extend special thanks to copyeditor Sharon Wilkey, whose meticulous attention to detail polished every sentence and provided the perfect finishing touches. Thank you to the founder of No Starch, Bill Pollock, who taught me everything I need to know about shopping at Trader Joe’s; managing editor Jill Franklin, for her kind and patient support; and the production team, led by senior production editor Jennifer Kepler and production manager Sabrina Plomitallo-González, who accommodated my unusual Notion → GitHub → PDF workflow.
Finally, a heartfelt thank-you to my wife, Aliki Caloyeras, who is always right. Seriously, it’s like a superpower at this point. I love you. To my children, Elias, who graciously allows me to maintain a semblance of dignity by not utterly obliterating me at basketball and video games, and Olympia, who reminds me “I’m feeling 22” when we play backgammon and cards and laugh together. I’d also like to thank my father, Bernard Shiffman, who generously lent his mathematical expertise and provided feedback along the way, as well as my mother, Doris Yaffe Shiffman, and brother, Jonathan Shiffman, who were always tremendously supportive in asking the question, “How is the book coming along?”
Over a decade ago, I self-published The Nature of Code, an online resource and print book exploring the unpredictable evolutionary and emergent properties of nature in software via the creative coding framework Processing. It’s the understatement of the century to say that much has changed in the world of technology and creative media since then, and so here I am again, with a new and rebooted version of this book built around JavaScript and the p5.js library. The book has a few new coding tricks this time, but it’s the same old nature—birds still flap their wings, and apples still fall on our heads.
What Is This Book?
At ITP/IMA (Tisch School of the Arts, New York University), I’ve been teaching a course titled Introduction to Computational Media since 2004. The origins of this class date back to 1987 and the work of Mike Mills and John Henry Thompson (inventor of the Lingo programming language). In the course, students learn the basics of programming (variables, conditionals, loops, objects, arrays) as well as concepts related to making interactive media projects (pixels, data, sound, networking, 3D, and more). In 2008, I synthesized my materials for this class into an introductory book, Learning Processing, and in 2015, I created a series of video tutorials that follow the same trajectory in JavaScript with the p5.js library.
-
Once a student has learned the basics and seen an array of applications, their next step might be to delve deeply into a particular area. Maybe they want to focus on computer vision, data visualization, or generative poetry. My Nature of Code course (also taught at ITP and IMA since 2008) represents another possible next step. It picks up exactly where my introductory material leaves off, demonstrating a world of programming techniques that focus on algorithms and simulation. The book you’re reading has evolved from that course.
+
Once a student has learned the basics and seen an array of applications, their next step might be to delve deeply into a particular area. Maybe they want to focus on computer vision, data visualization, or generative poetry. My Nature of Code course (also taught at ITP/IMA since 2008) represents another possible next step. It picks up exactly where my introductory material leaves off, demonstrating a world of programming techniques that focus on algorithms and simulation. The book you’re reading has evolved from that course.
My goal for this book is simple: I want to take a look at phenomena that naturally occur in the physical world and figure out how to write code to simulate them.
What, then, is this book exactly? Is it a science book? The answer is a resounding no. True, I might examine topics that come from physics or biology, but I won’t investigate these topics with a particularly high level of academic rigor. Instead, the book is “inspired by actual events.” I’m grabbing the parts from science and mathematics needed to build a software interpretation of nature, and veering off course or skipping details as I see fit.
Is this an art or design book? I would also say no. Regardless of how informal my approach might be, I’m still focusing on algorithms and their related programming techniques. Sure, the resulting demonstrations are visual (manifested as animated p5.js sketches), but they’re literal visualizations of the algorithms and programming techniques themselves, drawn only with basic shapes and grayscale color. It’s my hope, however, that you, dear reader, can use your creativity and visual ideas to make new, engaging work out of the examples. (I won’t complain if you turn every sketch into a rainbow.)
@@ -11,41 +11,41 @@
What Is This Book?
A Word About p5.js
The p5.js library is a reimagining of the Processing creative coding environment for the modern web. I’m using it in this book for a number of reasons. For one, it’s an environment that I’m very familiar with. While the original Processing built on top of Java is my first love and still what I turn to when trying out a new idea, p5.js is what I now use for teaching many of my programming classes. It’s free, open source, and well suited to beginners, and because it’s JavaScript, everything runs right there in the web browser itself—no installation required.
For me, however, Processing and p5.js are first and foremost a community of people, not coding libraries or frameworks. Those people have generously dedicated countless hours to making and sharing the software. I’ve written this book for that community and for anyone and everyone who loves to explore their curiosity and play through code.
-
All that said, nothing that this book’s content strictly to p5.js—or Processing, for that matter. This book could have been written with “vanilla” JavaScript or Java, or with any number of other open source creative coding environments like openFrameworks, Cinder, and so on. It’s my hope that after I’ve completed this book, I’ll be able to release versions of the examples that run in other environments. If anyone is interested in helping to port the examples, please feel free to contact me (daniel@natureofcode.com). Go on, you know you want to port The Nature of Code to PHP!
All that said, nothing ties this book’s content strictly to p5.js—or Processing, for that matter. This book could have been written with “vanilla” JavaScript or Java, or with any number of other open source creative coding environments like openFrameworks, Cinder, and so on. It’s my hope that after I’ve completed this book, I’ll be able to release versions of the examples that run in other environments. If anyone is interested in helping to port the examples, please feel free to contact me (daniel@natureofcode.com). Go on, you know you want to port The Nature of Code to PHP!
The prerequisites for understanding the material in this book could be stated as “one semester of programming instruction with p5.js, Processing, or any other creative coding environment.” That said, there’s no reason you couldn’t read this book having learned programming with a different language or development environment.
-
If you’ve never written any code before, while you could read the book for the concepts and inspiration, you’ll likely struggle with the code because I’m assuming knowledge of the fundamentals: variables, conditionals, loops, functions, objects, and arrays. If these concepts are new to you, my “Code! Programming with p5.js” or “Introduction to Creative Coding with Processing 4” video courses provide the fundamentals of what you need to know.
+
If you’ve never written any code before, while you could read the book for the concepts and inspiration, you’ll likely struggle with the code because I’m assuming knowledge of the fundamentals: variables, conditionals, loops, functions, objects, and arrays. If these concepts are new to you, my “Code! Programming with p5.js” and “Learning Processing” video courses provide the fundamentals of what you need to know.
Are you reading this book on a Kindle? Printed paper? On your laptop in PDF form? On a tablet showing an animated HTML5 version? Are you strapped to a chair, absorbing the content directly into your brain via a series of electrodes, tubes, and cartridges?
My dream has always been to write this book in one single format (in this case, a collection of Notion documents) and then, after pressing a magic button (npm run build), out comes the book in any and all formats you might want—PDF, HTML5, printed hard copy, Kindle, and so on. This was largely made possible by the Magic Book project, an open source framework for self-publishing originally developed at ITP by Rune Madsen and Steve Klise. Everything has been designed and styled using CSS—no manual typesetting or layout.
The reality of putting this book together isn’t quite so clean as that, and the story of how it happened is long. If you’re interested in learning more, make sure to read the book’s acknowledgments, and then go hire the people I’ve thanked to help you publish a book! I’ll also include more details in the associated GitHub repository.
-
The bottom line is that no matter what format you’re reading in, the material is all the same. The only difference will be in how you experience the code examples—more on that in “How to Read the Code” on page XX.
+
The bottom line is that no matter what format you’re reading it in, the material is all the same. The only difference will be in how you experience the code examples—more on that in “How to Read the Code” on page XX.
The Coding Train Connection
Personally, I still love an assembled amalgamation of cellulose pulp, meticulously bound together with a resilient spine, upon which pigmented compounds have been artfully deployed to convey words and ideas. Yet, ever since 2012, when I impulsively recorded my very first video lesson about programming in my office at ITP, I’ve discovered the tremendous value and joy in conveying ideas and lessons through moving pictures.
All this is to say, I have a YouTube channel called the Coding Train. I mentioned it earlier when discussing options for learning the prerequisite material for this book, and if you continue reading, you’ll find I continue to reference related videos. I might allude to one I made about a related algorithm or alternative technique for a particular coding example, or suggest a series on a tangential concept that could provide additional context to a topic I’m exploring.
If video learning is your style, I’m also working on an accompanying set of video tutorials that follow the exact same material as this book. I made a whole bunch 10 years ago with Processing, and more recently I started publishing a series of updated ones with p5.js. At the time of this writing, I’m about halfway through Chapter 5.
Additional Resources
There’s also an abundance of exceptional educational material teaching simulation and generative algorithms that I did not write or record. I always recommend that you explore various perspectives and voices when attempting to learn something new. It’s possible that what I’ve written might not click with you, and even hearing me repeat the same information in video form, regardless of how much mugging I do for the camera, won’t help. Sometimes what’s best is someone else you can relate to writing or saying or demonstrating the same concepts in different words with a different style. To this end, I’m including an “Additional Resources” section on this book’s website. If you create your own materials or have any recommendations for inclusion, please get in touch!
-
Two quick recommendations I have right now are The Computational Beauty of Nature by Gary William Flake (MIT Press, 1998)—it’s where I originally learned a lot of the ideas for this book—and the superbly organized online resource “That Creative Code Page” by Taru Muhonen and Raphaël de Courville.
+
Two quick recommendations I have right now are The Computational Beauty of Nature by Gary William Flake (MIT Press, 1998)—it’s where I originally learned a lot of the ideas for this book—and the superbly organized online resource That Creative Code Page by Taru Muhonen and Raphaël de Courville.
The “Story” of This Book
If you glance over the book’s table of contents, you’ll notice 12 chapters (0–11!), each one covering a different topic. And in one sense, this book is just that—a survey of a dozen concepts and associated code examples. Nevertheless, in putting together the material, I always imagined something of a linear narrative. Before you begin reading, I’d like to walk you through this story.
Part 1: Inanimate Objects
A soccer ball lies in the grass. A kick launches it into the air. Gravity pulls it back down. A heavy gust of wind keeps it afloat a moment longer until it falls and bounces off the head of a jumping player. The soccer ball isn’t alive; it makes no choices as to how it will move through the world. Rather, it’s an inanimate object waiting to be pushed and pulled by the forces of its environment.
How would you model a soccer ball moving in a digital canvas? If you’ve ever programmed a circle moving across a screen, you’ve probably written the following line of code:
x = x + 1;
-
You draw a shape at position x. With each frame of animation, you increment the value of x, redraw the shape, and voila—the illusion of motion! Maybe you took it a step or two further and included a y position, as well as variables for speed along the x- and y-axes:
+
You draw a shape at position x. With each frame of animation, you increment the value of x, redraw the shape, and voilà—the illusion of motion! Maybe you took it a step or two further and included a y position, as well as variables for speed along the x- and y-axes:
x = x + xspeed;
y = y + yspeed;
Part 1 of this story will take this idea even further. After exploring how to use different flavors of randomness to drive an object’s motion (Chapter 0), I’m going to take these xspeed and yspeed variables and demonstrate how together they form a vector (Chapter 1). You won’t get any new functionality out of this, but it will build a solid foundation for programming motion in the rest of the book.
-
Once you know a little something about vectors, you’re going to quickly realize that a force (Chapter 2) is a vector. Kick a soccer ball and you’re applying a force. What does a force cause an object to do? According to Isaac Newton, force equals mass times acceleration, so that force causes an object to accelerate. Modeling forces will allow you to create systems with dynamic motion, in which objects move according to a variety of rules.
+
Once you know a little something about vectors, you’re going to quickly realize that a force (Chapter 2) is a vector. Kick a soccer ball and you’re applying a force. What does a force cause an object to do? According to Sir Isaac Newton, force equals mass times acceleration, so that force causes an object to accelerate. Modeling forces will allow you to create systems with dynamic motion, in which objects move according to a variety of rules.
Now, that soccer ball to which you applied a force might have also been spinning. If an object moves according to its linear acceleration, it can spin according to its angular acceleration (Chapter 3). Understanding the basics of angles and trigonometry will allow you to model rotating objects as well as grasp the principles behind oscillating motion, like a pendulum swinging or a spring bouncing.
Once you’ve tackled the basics of motion and forces for an individual inanimate object, I’ll demonstrate how to make thousands upon thousands of those objects and manage them as a single unit called a particle system (Chapter 4). Particle systems are also a good excuse to look at some additional features of object-oriented programming—namely, inheritance and polymorphism.
Part 2: It’s Alive!
-
What does it mean to model life? Not an easy question to answer, but I’ll begin by building objects that have an ability to perceive their environment. Let’s think about this for a moment. A block that falls off a table moves according to forces, as does a dolphin swimming through the water. But there’s a key difference: the block can’t decide to leap off the table, whereas the dolphin can decide to leap out of the water. The dolphin has dreams and desires. It feels hunger or fear, and those feelings inform its movements. By examining techniques behind modeling autonomous agents (Chapter 5), you’ll learn to breathe life into inanimate objects, allowing them to make decisions about their movements according to their understanding of their environment.
-
In Chapters 1 through 5, all the examples will be written “from scratch”—meaning the code for the algorithms driving the motion of the objects will be written directly in p5.js. I’m certainly not the first programmer ever to consider the idea of simulating physics and life in animation, however, so next I’ll examine how you can use physics libraries (Chapter 6) to model more sophisticated behaviors. I’ll look at the features of two libraries: Matter.js and toxiclibs.js.
+
What does it mean to model life? Not an easy question to answer, but I’ll begin by building objects that have an ability to perceive their environment. Let’s think about this for a moment. A block that falls off a table moves according to forces, as does a dolphin swimming through the water. But there’s a key difference: the block can’t decide to leap off the table, whereas the dolphin can decide to leap out of the water. The dolphin has dreams and desires. It feels hunger and fear, and those feelings inform its movements. By examining techniques behind modeling autonomous agents (Chapter 5), you’ll learn to breathe life into inanimate objects, allowing them to make decisions about their movements according to their understanding of their environment.
+
In Chapters 1 through 5, all the examples will be written “from scratch”—meaning the code for the algorithms driving the motion of the objects will be written directly in p5.js. I’m certainly not the first programmer ever to consider the idea of simulating physics and life in animation, however, so next I’ll examine how you can use physics libraries (Chapter 6) to model more sophisticated behaviors. I’ll look at the features of two libraries: Matter.js and Toxiclibs.js.
The end of Chapter 5 will explore group behaviors that exhibit the properties of complexity. A complex system is typically defined as a system that’s more than the sum of its parts. While the individual elements of the system may be incredibly simple and easily understood, the behavior of the system as a whole can be highly complex, intelligent, and difficult to predict. Chasing complexity will lead you away from thinking purely about modeling motion and into the realm of rule-based systems. What can you model with cellular automata (Chapter 7), systems of cells living on a grid? What types of patterns can you generate with fractals (Chapter 8), the geometry of nature?
Part 3: Intelligence
You made things move. Then you gave those things hopes and dreams and fears, along with rules to live by. The last step in this book will bring intelligent decision-making into your creations. Can you apply the biological process of evolution to computational systems (Chapter 9) in order to evolve the behavior of autonomous agents? Taking inspiration from the human brain, can you program an artificial neural network (Chapter 10)? How can agents make decisions, learn from their mistakes, and adapt to their environment (Chapter 11)?
@@ -79,7 +79,7 @@
Using This Book as a Syllabus
Week 7
-
Mid-semester project about motion?
+
Mid-semester project about motion
Week 8
@@ -109,7 +109,7 @@
Using This Book as a Syllabus
If you’re considering using this text for a course or workshop, please feel free to contact me. I hope to eventually finish the companion set of videos, as well as include helpful slides as supplementary educational materials. If you make your own, I’d love to hear about it!
How to Read the Code
-
Code is the main medium of this book, weaving throughout the narrative as it’s dissected and examined. Sometimes it appears as full, standalone examples, other times it drops in as a single line or two, and often it’s stretched over whole sections in many short snippets, with explanations nestled in between. Whatever form it takes, code will always appear in a monospaced font. Here’s a quick guide on how to navigate the types of code sprinkled throughout the book.
+
Code is the main medium of this book, weaving throughout the narrative as it’s dissected and examined. Sometimes it appears as full, stand-alone examples, other times it drops in as a single line or two, and often it’s stretched over whole sections in many short snippets, with explanations nestled in between. Whatever form it takes, code will always appear in a monospaced font. Here’s a quick guide on how to navigate the types of code sprinkled throughout the book.
Full Examples
Each chapter includes fully functional code examples that are written with the p5.js library. Here’s what they look like:
@@ -128,21 +128,21 @@
Example #.#: Example Title
function draw() {
fill(0, 25);
stroke(0, 50);
- //{!1} Draw a random circle each time through draw().
+ //{!1} Draw a random circle each time through draw().
circle(random(width),random(height), 16)
}
The examples are numbered sequentially within each chapter to help you find the corresponding code online. In the printed version of the book, you’ll see a screenshot right below the example title. The online version has the running sketch embedded right there on the page. For animated examples (which are almost all of them), the screenshots will often show a “trail” of motion. This effect was achieved by adding transparency to the background(255, 10) function, though the accompanying code won’t include that enhancement.
Below the example, you’ll find the code, but it’s not always the complete code. Since many examples are quite long and span multiple files, I make my best effort to include a snippet that highlights the main aspects of the example or whatever new components are being introduced that weren’t already discussed earlier in the section.
You can find the full version of the code on the book’s website. There, you can interact with, modify, and experiment with the code in the p5.js Web Editor. Additionally, everything is included in the book’s GitHub repository. Here are links for all the materials:
-
The website for The Nature of Code book includes the full text of the book, additional reading and references, as well as all code examples.
-
The GitHub repositories for The Nature of Code contain the raw source code for the book's website, the book's build process, and all code examples.
+
The website for the book includes the full text of the book, additional reading and references, and all code examples.
+
The GitHub repositories contain the raw source code for the book’s website, the book’s build process, and all code examples.
In addition to the website and GitHub repositories, you can also access the code by viewing the list of sketches in the p5.js web editor.
-
Notice that I’ve used comments in the example to address what’s happening in the code. These comments float next to the code (though the appearance may vary depending on where you’re reading the book). The background highlighting groups the comments with their corresponding lines of code.
+
Notice that I’ve used comments in the example to address what’s happening in the code. These comments float next to the code (though the appearance may vary depending on where you’re reading the book). The background shading groups the comments with their corresponding lines of code.
Complete Snippets
-
Though rare, “complete” sections of code are occasionally mixed in with the body text. Sometimes, as with the sample “Example #.#” in the previous section, I might actually list all the code associated with a complete p5.js sketch. In most cases, however, I’m considering a “complete” snippet to be the code for an entire function or a class—a fully finished block of code, complete with opening and closing curly brackets and everything in between. Something like this:
-
// The entire draw() function for an example
+
Though rare, “complete” sections of code are occasionally mixed in with the body text. Sometimes, as with the sample Example #.# in the previous section, I might actually list all the code associated with a complete p5.js sketch. In most cases, however, I’m considering a “complete” snippet to be the code for an entire function or a class—a fully finished block of code, complete with opening and closing curly brackets and everything in between. Something like this:
+
// The entire draw() function for an example
function draw() {
background(255);
for (let x = 0; x < width; x += spacing) {
@@ -155,7 +155,7 @@
Context-Free Code
Occasionally, you’ll find lines of code hanging out on the page without a surrounding function or context. These snippets are there to illustrate a point, not necessarily to be run as is. They might represent a concept, a tiny piece of an algorithm, or a coding technique:
// RGB values to make the circles pink
fill(240, 99, 164);
-
Notice that this context-free snippet matches the indentation of fill(255) in the previous “complete” snippet. I’ll do this when the code has been established to be part of a something demonstrated previously. Admittedly, this won’t always work out so cleanly or perfectly, but I’m doing my best!
+
Notice that this context-free snippet matches the indentation of fill(255) in the previous “complete” snippet. I’ll do this when the code has been established to be part of something demonstrated previously. Admittedly, this won’t always work out so cleanly or perfectly, but I’m doing my best!
Snipped Code
Be on the lookout for the scissors! This design element indicates that a code snippet is a continuation of a previous piece or will be continued after some explanatory text. Sometimes it’s not actually being continued but is just cut off because all the code isn’t relevant to the discussion at hand. The scissors are there to say, “Hey, there might be more to this code above or below, or at the very least, this is a part of something bigger!” Here’s how this might play out with some surrounding body text:
The first step to building a p5.js sketch is to create a canvas:
Notice again that I’m keeping the indentation consistent to try to help establish the context, while using the scissors icon to help indicate where code is continued or cut off.
+
Notice that I’m keeping the indentation consistent to try to help establish the context, and again, I’m using the scissors icon to help indicate where code is continued or cut off.
A particular side effect of using snipped code is that you’ll often notice opening curly brackets in one snippet that don’t have a corresponding closing bracket until several snippets later (if at all). If you’re used to looking at JavaScript code, this may initially send you into a mild panic, but hopefully you’ll get used to it.
Exercises
Each chapter includes numbered exercises that serve as your playground to apply, experiment with, and go beyond the concepts and code provided within the chapters. Here’s what an exercise might look like:
@@ -195,8 +195,8 @@
Exercise #.#
Solutions
Solutions for the exercises are provided on the book’s website. Or I should say, I aspire to include solutions for all the exercises on the book’s website. As of this moment, just a handful are available, but hopefully by the time you’re reading this, there will be many more. If you’d like to contribute a solution to an exercise, I would love for you to do so via the book’s GitHub repository!
The Ecosystem Project
-
As much as I’d like to pretend you could learn everything by curling up in a comfy chair and reading some prose, to learn programming, you’re really going to have to do some programming. The exercises scattered throughout each chapter are a start, but you might find it helpful to also keep in mind a more substantial project idea (or two) that you can develop as you go from chapter to chapter. In fact, when teaching my Nature of Code course at ITP, I’ve often found that students enjoy building a single project, step by step, week by week, over the course of the semester.
-
At the end of each chapter, you’ll find a series of prompts for one such project—exercises that build on each other, one topic at a time. This project is based on the following scenario. You’ve been asked by a science museum to develop the software for a new exhibit, the Digital Ecosystem, a world of animated, procedural creatures that live in a computer simulation for visitors to enjoy as they enter the museum. I don’t mean to suggest that this is a particularly innovative or creative concept. Rather, I’ll use this example Ecosystem Project idea as a literal representation of the content in the book, demonstrating how the elements can fit together in a single program. I encourage you to develop your own idea, one that’s perhaps more abstract and nontraditional.
+
As much as I’d like to pretend you could learn everything by curling up in a comfy chair and reading some prose, to learn programming you’re really going to have to do some programming. The exercises scattered throughout each chapter are a start, but you might find it helpful to also keep in mind a more substantial project idea (or two) that you can develop as you go from chapter to chapter. In fact, when teaching my Nature of Code course at ITP, I’ve often found that students enjoy building a single project, step by step, week by week, over the course of the semester.
+
At the end of each chapter, you’ll find a series of prompts for one such project—exercises that build on each other, one topic at a time. This project is based on the following scenario. You’ve been asked by a science museum to develop the software for a new exhibit, the Digital Ecosystem, a world of animated, procedural creatures that live in a computer simulation for visitors to enjoy as they enter the museum. I don’t mean to suggest that this is a particularly innovative or creative concept. Rather, I’ll use this example Ecosystem Project idea as a literal representation of the content in the book, demonstrating how the elements can fit together in a single program. I encourage you to develop your own idea, one that’s perhaps more abstract and nontraditional.
Getting Help and Submitting Feedback
Coding can be tough and frustrating, and the ideas in this book aren’t always straightforward. You don’t have to go it alone. There’s probably someone else reading right now who would love to co-organize a study group or a book club where you can meet, chat, and share your struggles and successes. If you don’t find a local community for traveling this journey together, what about an online one? Two places I’d suggest are the official Processing forums and the Coding Train Discord server.
A Million Random Digits with 100,000 Normal Deviates, RAND Corporation
-
In 1947, the RAND Corporation produced a peculiar book titled A Million Random Digits with 100,000 Normal Deviates. The book wasn’t a work of literature or a philosophical treatise on randomness. Rather, it was a table of random numbers generated using an electronic simulation of a roulette wheel. This book was one of the last in a series of random-number tables produced from the mid-1920s to the 1950s. With the development of high-speed computers, generating pseudorandom numbers became faster than reading them from tables, and so this era of printed random-number tables ultimately came to an end.
+
In 1947, the RAND Corporation produced a peculiar book titled A Million Random Digits with 100,000 Normal Deviates. The book wasn’t a work of literature or a philosophical treatise on randomness. Rather, it was a table of random numbers generated using an electronic simulation of a roulette wheel. This book was one of the last in a series of random-number tables produced from the mid-1920s to the 1950s. With the development of high-speed computers, it became faster to generate pseudorandom numbers than to read them from tables, and so this era of printed random-number tables ultimately came to an end.
Here we are: the beginning. If it’s been a while since you’ve programmed in JavaScript (or done any math, for that matter), this chapter will reacquaint your mind with computational thinking. To start your coding-of-nature journey, I’ll introduce you to some foundational tools for programming simulations: random numbers, random distributions, and noise. Think of this as the first (zeroth!) element of the array that makes up this book—a refresher and a gateway to the possibilities that lie ahead.
-
In Chapter 1, I’m going to talk about the concept of a vector and how it will serve as the building block for simulating motion throughout this book. But before I take that step, let’s think about what it means for something to move around a digital canvas. I’ll begin with one of the best known and simplest simulations of motion: the random walk.
+
In Chapter 1, I’m going to talk about the concept of a vector and how it will serve as the building block for simulating motion throughout this book. But before I take that step, let’s think about what it means for something to move around a digital canvas. I’ll begin with one of the best-known and simplest simulations of motion: the random walk.
Random Walks
-
Imagine you’re standing in the middle of a balance beam. Every 10 seconds, you flip a coin. Heads, take a step forward. Tails, take a step backward. This is a random walk, a path defined as a series of random steps. Stepping (carefully) off that balance beam and onto the floor, you could expand your random walk from one dimension (moving only forward and back) to two dimensions (moving forward, back, left, and right). Now that there are four possibilities, you’d have to flip the same coin twice to determine each next step:
+
Imagine you’re standing in the middle of a balance beam. Every 10 seconds, you flip a coin. Heads, take a step forward. Tails, take a step backward. This is a random walk, a path defined as a series of random steps. Stepping (carefully) off that balance beam and onto the floor, you could expand your random walk from one dimension (moving only forward and back) to two dimensions (moving forward, back, left, and right). Now that there are four possibilities, you’d have to flip the same coin twice to determine each next step.
@@ -82,7 +82,7 @@
The Random Walker Class
}
Notice the use of the keyword this to attach the properties to the newly created object itself: this.x and this.y.
-
In addition to data, classes can be defined with functionality. In this example, a Walker object has two functions, known as methods in an OOP context. While methods are essentially functions, the distinction is that methods are defined inside a class and therefore are associated with an object or class, whereas functions aren’t. The function keyword is a nice clue: you’ll see it when defining standalone functions, but it won’t appear inside a class. I’ll try my best to use the terms consistently in this book, but it’s common for programmers to use the terms function and method interchangeably.
+
In addition to data, classes can be defined with functionality. In this example, a Walker object has two functions, known as methods in an OOP context. While methods are essentially functions, the distinction is that methods are defined inside a class and therefore are associated with an object or class, whereas functions aren’t. The function keyword is a nice clue: you’ll see it when defining stand-alone functions, but it won’t appear inside a class. I’ll try my best to use the terms consistently in this book, but it’s common for programmers to use the terms function and method interchangeably.
The first method, show(), includes the code to draw the object (as a black dot). Once again, never forget the this. when referencing the properties (variables) of that object:
// Objects have methods.
@@ -94,7 +94,7 @@
The Random Walker Class
The next method, step(), directs the Walker object to take a step. This is where things get a bit more interesting. Remember taking steps in random directions on a floor? Now I’ll use a p5.js canvas to represent that floor. There are four possible steps. A step to the right can be simulated by incrementing x with x++; to the left by decrementing x with x--; forward by going up a pixel (y--); and backward by going down a pixel (y++). But how can the code pick from these four choices?
Earlier I stated that you could flip two coins. In p5.js, however, when you want to randomly choose from a list of options, you can simply generate a random number with the random() function. It picks a random floating-point (decimal) value within any range you want. Here, I use 4 to indicate a range of 0 to 4:
let choice = floor(random(4));
-
I declare a variable choice and assign it a random integer (whole number) by using floor() to remove the decimal places from the random floating-point number using floor(). Technically speaking, the number generated by random(4) lies within the range of 0 (inclusive) to 4 (exclusive), meaning it can never actually be 4.0. The highest possible number it could generate is just below 4—3.999999999 (with as many 9s as JavaScript will allow), which floor() then truncates down to 3, removing the decimal part. Therefore, I’ve effectively assigned choice a value of 0, 1, 2, or 3.
+
I declare a variable choice and assign it a random integer (whole number) by using floor() to remove the decimal places from the random floating-point number. Technically speaking, the number generated by random(4) lies within the range of 0 (inclusive) to 4 (exclusive), meaning it can never actually be 4.0. The highest possible number it could generate is just below 4—3.999999999 (with as many 9s as JavaScript will allow), which floor() then truncates down to 3, removing the decimal part. Therefore, I’ve effectively assigned choice a value of 0, 1, 2, or 3.
Coding Conventions
In JavaScript, variables can be declared using either let or const. A typical approach is to declare all variables with const and change to let when needed. In this first example, const would be appropriate for declaring choice as it’s never reassigned a new value over the course of its life inside each call to step(). While this differentiation is important, I’m choosing to follow the p5.js example convention and declare all variables with let.
@@ -122,7 +122,7 @@
Coding Conventions
}
Now that I’ve written the class, it’s time to make an actual Walker object in the sketch itself. Assuming you’re looking to model a single random walk, start with a single global variable:
-
// A Walker object
+
// A Walker object
let walker;
Then create the object in setup() by referencing the class name with the new operator:
//{!1} Remember how p5.js works? setup() is executed once when the sketch starts.
@@ -144,7 +144,7 @@
Coding Conventions
Example 0.1: A Traditional Random Walk
I could make a couple of adjustments to the random walker. For one, this Walker object’s steps are limited to four options: up, down, left, and right. But any given pixel in the canvas could have eight possible neighbors, including diagonals (see Figure 0.1). A ninth possibility, to stay in the same place, could also be an option.
@@ -162,7 +162,7 @@
Example 0.1: A Traditional Random
}
Taking this further, I could get rid of floor() and use the random() function’s original floating-point numbers to create a continuous range of possible step lengths from –1 to 1:
step() {
- //{!2} Any floating-point number from –1.0 to 1.0
+ //{!2} Any floating-point number from –1 to 1
let xstep = random(–1, 1);
let ystep = random(–1, 1);
this.x += xstep;
@@ -236,7 +236,7 @@
Exercise 0.2
//{!1} Pick a random element from an array.
let value = random(stuff);
print(value);
-
The five-member array has two 1s, so running this code will produce a two-out-of-five chance, or 40 percent chance, of printing the value 1. Likewise, there’s a 20 percent chance of printing 2, and a 40 percent chance of printing 3.
+
The five-member array has two 1s, so running this code will produce a two-out-of-five chance, or 40 percent chance, of printing the value 1. Likewise, there’s a 20 percent chance of printing 2 and a 40 percent chance of printing 3.
You can also ask for a random number (let’s make it simple and just consider random floating-point values from 0 to 1) and allow an event to occur only if the random number is within a certain range. For example:
// A probability of 10%
let probability = 0.1;
@@ -314,7 +314,7 @@
A Normal Distribution of Random
Figure 0.2: Two example bell curves of a normal distribution, with a low (left) and high (right) standard deviation
-
The numbers work out as follows: given a population, 68 percent of its members will have values in the range of one standard deviation from the mean, 95 percent within two standard deviations, and 99.7 percent within three standard deviations. Given a standard deviation of 5 pixels, only 0.3 percent of the monkey heights will be less than 235 pixels (three standard deviations below the mean of 250) or greater than 265 pixels (three standard deviations above the mean of 250). Meanwhile, 68 percent of monkey heights will be from 245 to 255 pixels.
+
The numbers work out as follows: given a population, 68 percent of its members will have values in the range of one standard deviation from the mean, 95 percent within two standard deviations, and 99.7 percent within three standard deviations. Given a standard deviation of 5 pixels, only 0.3 percent of the monkey heights will be less than 235 pixels (three standard deviations below the mean of 250) or greater than 265 pixels (three standard deviations above the mean of 250). Meanwhile, 68 percent of the monkey heights will be from 245 to 255 pixels.
Calculating Mean and Standard Deviation
Consider a class of 10 students who receive the following scores (out of 100) on a test: 85, 82, 88, 86, 85, 93, 98, 40, 73, and 83.
@@ -415,7 +415,7 @@
A Custom Distribution of Random
If r2 isn’t less than p, go back to step 1 and start over.
Here, the likelihood that a random value will qualify is equal to the random number itself, just as you saw in Figure 0.3. If r1 equals 0.1, for example, r1 will have a 10 percent chance of qualifying. If r1 equals 0.83, it will have an 83 percent chance of qualifying. The higher the number, the greater the likelihood that it gets used.
-
This process is called the accept-reject algorithm, a type of Monte Carlo method (named for the Monte Carlo Casino). Here’s a function that implements the accept-reject algorithm, returning a random value from 0 to 1.
+
This process is called the accept-reject algorithm, a type of Monte Carlo method (named for the Monte Carlo Casino). The following example features a function that implements the accept-reject algorithm, returning a random value from 0 to 1.
Example 0.5: An Accept-Reject Distribution
A Smoother Approach with Perlin Noise
-
A good random-number generator produces numbers that have no relationship to one another and show no discernible pattern. As I’ve hinted, however, while a little bit of randomness can be a good thing when programming organic, lifelike behaviors, uniform randomness as the single guiding principle isn’t necessarily natural. An algorithm known as Perlin noise, named for its inventor Ken Perlin, takes this concept into account by producing a naturally ordered sequence of pseudorandom numbers, where each number in the sequence is quite close in value to the one before it. This creates a “smooth” transition between the random numbers and a more organic appearance than pure noise, making Perlin noise well suited for generating various effects with natural qualities, such as clouds, landscapes, and patterned textures like marble.
+
A good random-number generator produces numbers that have no relationship to one another and show no discernible pattern. As I’ve hinted, however, while a little bit of randomness can be a good thing when programming organic, lifelike behaviors, uniform randomness as the single guiding principle isn’t necessarily natural. An algorithm known as Perlin noise, named for its inventor, Ken Perlin, takes this concept into account by producing a naturally ordered sequence of pseudorandom numbers, where each number in the sequence is quite close in value to the one before it. This creates a “smooth” transition between the random numbers and a more organic appearance than pure noise, making Perlin noise well suited for generating various effects with natural qualities, such as clouds, landscapes, and patterned textures like marble.
To illustrate the difference between Perlin noise and uniform randomness, consider Figure 0.4. The graph on the left shows Perlin noise over time, with the x-axis representing time; note the smoothness of the curve. The graph on the right shows noise in the form of purely random numbers over time; the result is much more jagged. (The code for generating these graphs is available on the book’s website.)
@@ -473,12 +473,12 @@
A Smoother Approach with Perlin N
let x = random(0, width);
circle(x, 180, 16);
Now, instead of a random x-position, you want a smoother Perlin noise x-position. You might think that all you need to do is replace random() with an identical call to noise(), like so:
-
// Replace random() with noise()?
+
// Replace random() with noise()?
let x = random(0, width);
// Tempting, but this is not correct!
let x = noise(0, width);
circle(x, 180, 16);
-
Conceptually, this is exactly what you want to do—calculate an x-value that ranges from 0 to the width according to Perlin noise—but this isn’t the correct implementation. While the arguments to the random() function specify a range of values between a minimum and a maximum, noise() doesn’t work this way. Instead, its output range is fixed: it always returns a value from 0 to 1. You’ll see in a moment that you can get around this easily with p5’s map() function, but first let’s examine what exactly noise() expects you to pass in as an argument.
+
Conceptually, this is exactly what you want to do—calculate an x-value that ranges from 0 to the width according to Perlin noise—but this isn’t the correct implementation. While the arguments to the random() function specify a range of values between a minimum and a maximum, noise() doesn’t work this way. Instead, its output range is fixed: it always returns a value from 0 to 1. You’ll see in a moment that you can get around this easily with p5.js’s map() function, but first let’s examine what exactly noise() expects you to pass in as an argument.
One-dimensional Perlin noise can be thought of as a linear sequence of values over time. For example:
@@ -521,7 +521,7 @@
A Smoother Approach with Perlin N
print(n);
}
Close, but not quite. This code just prints the same value over and over because it keeps asking for the result of the noise() function at the same point in time, 3. If the time variable t increments, however, you’ll get a different noise value each time you call the function:
-
// It’s convention to start with an offset of t = 0, though this is arbitrary.
+
// It’s conventional to start with an offset of t = 0, though this is arbitrary.
let t = 0;
function draw() {
@@ -537,7 +537,7 @@
A Smoother Approach with Perlin N
In the upcoming code examples that utilize Perlin noise, pay attention to how the animation changes with varying values of t.
Noise Ranges
-
Once you have noise values that range from 0 to 1, it’s up to you to map that range to whatever size suits your purpose. The easiest way to do this is with p5’s map() function (Figure 0.6). It takes five arguments. First is the value you want to map—in this case, n. This is followed by the value’s current range (minimum and maximum), followed by the desired range.
+
Once you have noise values that range from 0 to 1, it’s up to you to map that range to whatever size suits your purpose. The easiest way to do this is with p5.js’s map() function (Figure 0.6). It takes five arguments. First is the value you want to map—in this case, n. This is followed by the value’s current range (minimum and maximum), followed by the desired range.
Figure 0.6: Mapping a value from one range to another
@@ -586,7 +586,7 @@
Example 0.6: A Perlin Noise Walker
In truth, no actual concept of time is at play here. It’s a useful metaphor to help describe how the noise function works, but really, what you have is space, rather than time. The graph in Figure 0.7 depicts a linear sequence of noise values in a 1D space—that is, arranged along a line. Values are retrieved at a specific x-position, which is why you’ll often see a variable named xoff in examples to indicate the x-offset along the noise graph, rather than t for time.
Exercise 0.7
-
In the Perlin noise random walker, the result of the noise() function is mapped directly to the walker’s position. Create a random walker but map the result of the noise() function to the walker’s step size instead.
+
In the Perlin noise random walker, the result of the noise() function is mapped directly to the walker’s position. Create a random walker, but map the result of the noise() function to the walker’s step size instead.
Two-Dimensional Noise
Having explored the concept of noise values in one dimension, let’s consider how they can also exist in a two-dimensional (2D) space. With 1D noise, there’s a sequence of values in which any given value is similar to its neighbor. Imagine a piece of graph paper (or a spreadsheet!) with the values for 1D noise written across a single row, one value per cell. Because these values live in one dimension, each has only two neighbors: a value that comes before it (to the left) and one that comes after it (to the right), as shown on the left in Figure 0.8.
@@ -598,14 +598,14 @@
Two-Dimensional Noise
If you were to visualize this graph paper with each value mapped to the brightness of a color, you would get something that looks like clouds. White sits next to light gray, which sits next to gray, which sits next to dark gray, which sits next to black, which sits next to dark gray, and so on (Figure 0.9).
This effect is why noise was originally invented. If you tweak the parameters and play with color, the resulting images look more like marble, wood, or any other organic texture.
If you wanted to color every pixel of a canvas randomly using the random() function, you would need a nested loop to cycle through the rows and columns of pixels and pick a random brightness for each. Note that in p5, the pixels are arranged in an array with four spots for each: red, green, blue, and alpha. For details, see the pixel array video in the “Pixels" track on the Coding Train website.
+
If you wanted to color every pixel of a canvas randomly using the random() function, you would need a nested loop to cycle through the rows and columns of pixels and pick a random brightness for each. Note that in p5.js, the pixels are arranged in an array with four spots for each: red, green, blue, and alpha. For details, see the pixel array video in the “Pixels” track on the Coding Train website.
loadPixels();
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
@@ -626,27 +626,27 @@
Noise Detail
let bright = map(noise(x, y), 0, 1, 0, 255);
This is a nice start conceptually—the code calculates a noise value for every (x, y) position in a 2D space. The problem is that this won’t have the smooth, cloudy quality you want. Incrementing by 1 through the noise space from one pixel to the next is too large a jump. Remember, with 1D noise, I incremented the time variable by 0.01 each frame, not by 1!
A pretty good solution to this problem is to just use different variables for the noise arguments than those you’re using to access the pixels on the canvas. For example, you can increment a variable called xoff by 0.01 each time x increases horizontally by 1, and a yoff variable by 0.01 each time y increases vertically by 1 through the nested loops:
-
// Start xoff at 0.
+
// Start xoff at 0.
let xoff = 0.0;
for (let x = 0; x < width; x++) {
- // For every xoff, start yoff at 0.
+ // For every xoff, start yoff at 0.
let yoff = 0.0;
for (let y = 0; y < height; y++) {
- // Use xoff and yoff for noise().
+ // Use xoff and yoff for noise().
let bright = map(noise(xoff, yoff), 0, 1, 0, 255);
- // Use x and y for the pixel position.
+ // Use x and y for the pixel position.
let index = (x + y * width) * 4;
// Set the red, green, blue, and alpha values.
pixels[index] = bright;
pixels[index + 1] = bright;
pixels[index + 2] = bright;
pixels[index + 3] = 255;
- // Increment yoff.
+ // Increment yoff.
yoff += 0.01;
}
- // Increment xoff.
+ // Increment xoff.
xoff += 0.01;
}
I have to confess, I’ve done something rather confusing. I used 1Dnoise to set two variables (this.x and this.y) controlling the 2D motion of a walker. Then, I promptly moved on to using 2D noise to set one variable (bright) controlling the brightness of each pixel in the canvas.
The stick chart is a navigational tool crafted by the indigenous people of the Marshall Islands, located in the central Pacific Ocean. This ancient tool was made by carefully tying together the midribs of coconut fronds. Shell markings on the chart signify the locations of islands in the region. The layout of the fronds and shells serves as a geographical guide, offering an abstract representation of vectors that capture the ocean swell patterns and their directional flow.
@@ -257,7 +257,7 @@
Addition Properties with Vectors
this.y = this.y + v.y;
}
}
-
The function looks up the x and y-components of the two vectors and adds them separately. This is exactly how the built-in p5.Vector class’s add() function is written, too. Knowing how it works, I can now return to the bouncing ball example with its position + velocity algorithm and implement vector addition:
+
The function looks up the x- and y-components of the two vectors and adds them separately. This is exactly how the built-in p5.Vector class’s add() method is written too. Knowing how it works, I can now return to the bouncing ball example with its position + velocity algorithm and implement vector addition:
// This does not work!
position = position + velocity;
// Add the velocity to the position.
@@ -285,7 +285,7 @@
Example 1.2: Bouncing Ball with V
background(255);
position.add(velocity);
- // You still sometimes need to refer to the individual components of a p5.Vector and can do so using the dot syntax: position.x, velocity.y, and so forth.
+ // You still sometimes need to refer to the individual components of a p5.Vector and can do so using the dot syntax: position.x, velocity.y, and so forth.
if (position.x > width || position.x < 0) {
velocity.x = velocity.x * -1;
}
@@ -302,7 +302,7 @@
Example 1.2: Bouncing Ball with V
The circle() function doesn’t allow for a p5.Vector object as an argument. A circle can be drawn with only two scalar values, an x-coordinate and a y-coordinate. And so I must dig into the p5.Vector object and pull out the x- and y-components by using object-oriented dot syntax:
The same issue arises when testing if the circle has reached the edge of the window. In this case, I need to access the individual components of both vectors, position and velocity:
+
The same issue arises when testing whether the circle has reached the edge of the window. In this case, I need to access the individual components of both vectors, position and velocity:
Extend Example 1.2 into 3D. Can you get a sphere to bounce around a box?
More Vector Math
-
Addition was really just the first step. Many mathematical operations are commonly used with vectors. Here’s a comprehensive table of the operations available as methods in the p5.Vector class. Remember, these are not standalone functions, but rather methods associated with the p5.Vector class. When you see the word this in the following table, it refers to the specific vector the method is operating on:
+
Addition was really just the first step. Many mathematical operations are commonly used with vectors. Here’s a comprehensive table of the operations available as methods in the p5.Vector class. Remember, these are not stand-alone functions, but rather methods associated with the p5.Vector class. When you see the word this in the following table, it refers to the specific vector the method is operating on.
@@ -510,7 +510,7 @@
Vector Multiplication and Division
}
Implementing multiplication in code is as simple as the following:
let u = createVector(-3, 7);
-// This p5.Vector is now three times the size and is equal to (–9, 21). See Figure 1.9.
+// This p5.Vector is now three times the size and is equal to (–9, 21). See Figure 1.9.
u.mult(3);
Example 1.4 illustrates vector multiplication by drawing a line between the mouse and the center of the canvas, as in the previous example, and then scaling that line by 0.5.
The resulting vector is half its original size. Rather than multiplying the vector by 0.5, I could also achieve the same effect by dividing the vector by 2, as in Figure 1.10.
+
The resulting vector is half its original size. Rather than multiplying the vector by 0.5, I could achieve the same effect by dividing the vector by 2, as in Figure 1.10.
In Figure 1.16, you see that the acceleration vector (dx, dy) can be calculated by subtracting the object’s position from the mouse’s position:
-
\text{dx} = \text{mouseX} - x
-
\text{dy} = \text{mouseY} - y
+
dx = mouseX - x
+
dy = mouseY - y
Let’s implement that by using p5.Vector syntax. Assuming the code will live inside the Mover class and thus have access to the object’s position, I can write this:
let mouse = createVector(mouseX, mouseY);
-// Look! I’m using the static reference to sub() because I want a new p5.Vector!
+// Look! I’m using the static reference to sub() because I want a new p5.Vector!
let direction = p5.Vector.sub(mouse, this.position);
I’ve used the static version of sub() to create a new vector direction that points from the mover’s position to the mouse. If the object were to actually accelerate using that vector, however, it would appear instantaneously at the mouse position, since the magnitude of direction is equal to the distance between the object and the mouse. This wouldn’t make for a smooth animation, of course. The next step, therefore, is to decide how quickly the object should accelerate toward the mouse by changing the vector’s magnitude.
To set the magnitude (whatever it may be) of the acceleration vector, I must first ______ the vector. That’s right, you said it: normalize! If I can shrink the vector to its unit vector (of length 1), I can easily scale it to any other value, because 1 multiplied by anything equals anything:
@@ -1013,7 +1013,7 @@
Algorithm 3: Interactive Motion
I have a confession to make. Normalization and then scaling is such a common vector operation that p5.Vector includes a function that does both, setting the magnitude of a vector to a given value with a single function call. That function is setMag():
let anything = ?????
dir.setMag(anything);
-
In this next example, to emphasize the math, I’m going to write the code using normalize() and mult(), but this is likely the last time I‘ll do that. You‘ll find setMag() in examples going forward.
+
In this next example, to emphasize the math, I’m going to write the code using normalize() and mult(), but this is likely the last time I’ll do that. You’ll find setMag() in examples going forward.
Example 1.10: Accelerating Toward the Mouse
@@ -1039,7 +1039,7 @@
Example 1.10: Accelerating To
this.velocity.limit(this.topSpeed);
this.position.add(this.velocity);
}
-
You may be wondering why the circle doesn’t stop when it reaches the target. It’s important to note that the moving object has no knowledge about trying to stop at a destination; it knows only where the destination’s position. The object tries to accelerate there at a fixed rate, regardless of how far away it is. This means it will inevitably overshoot the target and have to turn around, again accelerating toward the destination, overshooting it again, and so forth. Stay tuned; in later chapters, I’ll show you how to program an object to arrive at a target (slow down on approach).
+
You may be wondering why the circle doesn’t stop when it reaches the target. It’s important to note that the moving object has no knowledge about trying to stop at a destination; it knows only the destination’s position. The object tries to accelerate there at a fixed rate, regardless of how far away it is. This means it will inevitably overshoot the target and have to turn around, again accelerating toward the destination, overshooting it again, and so forth. Stay tuned; in later chapters, I’ll show you how to program an object to arrive at a target (slow down on approach).
Exercise 1.8
Example 1.10 is remarkably close to the concept of gravitational attraction, with the object being attracted to the mouse position. In the example, however, the attraction magnitude is constant, whereas with a real-life gravitational force, the magnitude is inversely proportional to distance: the closer the object is to the attraction point, the faster it accelerates. I’ll cover gravitational attraction in more detail in the next chapter, but for now, try implementing your own version of Example 1.10 with a variable magnitude of acceleration, stronger when it’s either closer or farther away.
Alexander Calder was a 20th-century American artist known for his kinetic sculptures that balance form and motion. His “constellations” were sculptures consisting of interconnected shapes and wire that demonstrate tension, balance, and the ever-present pull of gravitational attraction.
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 a 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 2D 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 focuses on building a rudimentary physics engine, with an emphasis on speed and ease of understanding.
+
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 2D 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 focuses on building a rudimentary physics engine, with an emphasis on speed and ease of understanding.
Forces and Newton’s Laws of Motion
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.
@@ -28,7 +28,7 @@
Newton’s First Law
However, this is missing an important element related to forces. I could expand the definition by stating:
An object at rest stays at rest, and an object in motion stays in motion, at a constant speed and direction unless acted upon by an unbalanced force.
When Newton came along, the prevailing theory of motion—formulated by Aristotle—was nearly 2,000 years old. It stated that if an object is moving, some sort of force is required to keep it moving. Unless that moving thing is being pushed or pulled, it will slow down or stop. This theory was borne out through observation of the world. For example, if you toss a ball, it falls to the ground and eventually stops moving, seemingly because the force of the toss is no longer being applied.
-
This older theory, of course, isn’t true. As Newton established, in the absence of any forces, no force is required to keep an object moving. When an object (such as the aforementioned ball) is tossed in Earth’s atmosphere, its velocity changes because of unseen forces such as air resistance and gravity. An object’s velocity will remain constant only in the absence of any forces or if the forces that act on it cancel each other out, meaning the net force adds up to zero. This is often referred to as equilibrium (see Figure 2.1). The falling ball will reach a terminal velocity (which stays constant) once the force of air resistance equals the force of gravity.
+
This older theory, of course, isn’t true. As Newton established, in the absence of any forces, no force is required to keep an object moving. When an object (such as the aforementioned ball) is tossed in Earth’s atmosphere, its velocity changes because of unseen forces such as air resistance and gravity. An object’s velocity will remain constant only in the absence of any forces or only if the forces that act on it cancel each other out, meaning the net force adds up to zero. This is often referred to as equilibrium (see Figure 2.1). The falling ball will reach a terminal velocity (which stays constant) once the force of air resistance equals the force of gravity.
Figure 2.1: The toy mouse doesn’t move because all the forces cancel one another out (add up to a net force of zero).
@@ -43,7 +43,7 @@
Newton’s Third Law
Let’s say you push against a wall. The wall doesn’t actively decide to push you back, and yet it still provides resistance with an equal force in the opposite direction. There’s no “origin” force. Your push simply includes both forces, referred to as an action/reaction pair. A better way of stating Newton’s third law might therefore be the following:
Forces always occur in pairs. The two forces are of equal strength but in opposite directions.
This still causes confusion because it sounds like these forces would always cancel each other out. This isn’t the case. Remember, the forces act on different objects. And just because the two forces are equal doesn’t mean that the objects’ movements are equal (or that the objects will stop moving).
-
Consider pushing on a stationary truck. Although the truck is far more massive than you, a stationary truck (unlike a moving one) will never overpower you and send you flying backwards. The force your hands exert on the truck is equal and opposite to the force exerted by the truck on your hands. The outcome depends on a variety of other factors. If the truck is small and parked on an icy street, you’ll probably be able to get it to move. On the other hand, if it’s very large and on a dirt road and you push hard enough (maybe even take a running start), you could injure your hand.
+
Consider pushing on a stationary truck. Although the truck is far more massive than you, a stationary truck (unlike a moving one) will never overpower you and send you flying backward. The force your hands exert on the truck is equal and opposite to the force exerted by the truck on your hands. The outcome depends on a variety of other factors. If the truck is small and parked on an icy street, you’ll probably be able to get it to move. On the other hand, if it’s very large and on a dirt road and you push hard enough (maybe even take a running start), you could injure your hand.
And what if, as in Figure 2.2, you are wearing roller skates when you push on that truck?
@@ -52,9 +52,9 @@
Newton’s Third Law
You’ll accelerate away from the truck, sliding along the road while the truck stays put. Why do you slide but not the truck? For one, the truck has a much larger mass (which I’ll get into with Newton’s second law). Other forces are at work too—namely, the friction of the truck’s tires and your roller skates against the road.
Considering p5.js again, I could restate Newton’s third law as follows:
If you calculate a p5.Vector called f that represents a force of object A on object B, you must also apply the opposite force that object B exerts on object A. You can calculate this other force as p5.Vector.mult(f, -1).
-
You’ll soon see that in the world of coding simulation, it’s often not necessary to stay true to Newton’s third law. Sometimes, such as in the case of gravitational attraction between bodies (see Example 2.8), I’ll want to model equal and opposite forces in my example code. Other times, such as a scenario where I‘ll say, “Hey, there’s some wind in the environment,” I’m not going to bother to model the force that a body exerts back on the air. In fact, I’m not going to bother modeling the air at all! Remember, the examples in this book are taking inspiration from the physics of the natural world for the purposes of creativity and interactivity. They don’t require perfect precision.
+
You’ll soon see that in the world of coding simulation, it’s often not necessary to stay true to Newton’s third law. Sometimes, such as in the case of gravitational attraction between bodies (see Example 2.8), I’ll want to model equal and opposite forces in my example code. Other times, such as a scenario where I’ll say, “Hey, there’s some wind in the environment,” I’m not going to bother to model the force that a body exerts back on the air. In fact, I’m not going to bother modeling the air at all! Remember, the examples in this book are taking inspiration from the physics of the natural world for the purposes of creativity and interactivity. They don’t require perfect precision.
Newton’s Second Law
-
Now it‘s time for most important law for you, the p5.js coder: Newton’s second law. It’s stated as follows:
+
Now it’s time for most important law for you, the p5.js coder: Newton’s second law. It’s stated as follows:
Force equals mass times acceleration.
Or:
\vec{F} = M \times \vec{A}
@@ -69,7 +69,7 @@
Weight vs. Mass
In the world of p5.js, what is mass anyway? Aren’t we dealing with pixels? Let’s start simple and say that in a pretend pixel world, all objects have a mass equal to 1. Anything divided by 1 equals itself, and so, in this simple world, we have this:
\vec{A} = \vec{F}
-
I’ve effectively removed mass from the equation, making the acceleration of an object equal to force. This is great news. After all, Chapter 1 described acceleration as the key to controlling the movement of objects in a canvas. I said that position changes according to velocity, and velocity according to acceleration. Acceleration seemed to be where it all began. Now you can see that force is truly where it all begins.
+
I’ve effectively removed mass from the equation, making the acceleration of an object equal to force. This is great news. After all, Chapter 1 described acceleration as the key to controlling the movement of objects in a canvas. I said that the position changes according to the velocity, and the velocity according to acceleration. Acceleration seemed to be where it all began. Now you can see that force is truly where it all begins.
Let’s take the Mover class, with position, velocity, and acceleration:
class Mover {
constructor() {
@@ -95,7 +95,7 @@
Weight vs. Mass
this.velocity.add(this.acceleration);
If you run this code, you won’t see an error in the console, but zoinks! There’s a major problem. What’s the value of acceleration when it’s added to velocity? It’s equal to the gravity vector, meaning wind has been left out! Anytime applyForce() is called, acceleration is overwritten. How can I handle more than one force?
Force Accumulation
-
The answer is that the forces must accumulate, or be added together. This is stated in the full definition of Newton’s second law itself, I now confess to having simplified. Here’s a more accurate way to put it:
+
The answer is that the forces must accumulate, or be added together. This is stated in the full definition of Newton’s second law itself, which I now confess to having simplified. Here’s a more accurate way to put it:
Net force equals mass times acceleration.
In other words, acceleration is equal to the sum of all forces divided by mass. At any given moment, there might be 1, 2, 6, 12, or 303 forces acting on an object. As long as the object knows how to add them together (accumulate them), it doesn’t matter how many forces there are. The sum total will give you the object’s acceleration (again, ignoring mass). This makes perfect sense. After all, as you saw in Newton’s first law, if all the forces acting on an object add up to zero, the object experiences an equilibrium state (that is, no acceleration).
I can now revise the applyForce() method to take force accumulation into account:
@@ -137,7 +137,7 @@
Factoring In Mass
Units of Measurement
Now that I’m introducing mass, it’s important to make a quick note about units of measurement. In the real world, things are measured in specific units: two objects are 3 meters apart, the baseball is moving at a rate of 90 miles per hour, or this bowling ball has a mass of 6 kilograms. Sometimes you do want to take real-world units into consideration. In this chapter, however, I’m going to stick with units of measurement in pixels (“These two circles are 100 pixels apart”) and frames of animation (“This circle is moving at a rate of 2 pixels per frame,” the aforementioned time step).
-
In the case of mass, p5.js doesn't have any unit of measurement to use. How much mass is in any given pixel? You might enjoy inventing your own p5.js unit of mass to associate with those values, like “10 pixeloids” or “10 yurkles.”
+
In the case of mass, p5.js doesn’t have any unit of measurement to use. How much mass is in any given pixel? You might enjoy inventing your own p5.js unit of mass to associate with those values, like “10 pixeloids” or “10 yurkles.”
For demonstration purposes, I’ll tie mass to pixels (the larger a circle’s diameter, the larger the mass). This will allow me to visualize the mass of an object, albeit inaccurately. In the real world, size doesn’t indicate mass. A small metal ball could have a much higher mass than a large balloon because of its higher density. And for two circular objects with equal density, I’ll also note that mass should be tied to the formula for the area of a circle: \pi r^2. (This will be addressed in Exercise 2.11, and I’ll say more about \pi and circles in Chapter 3.)
Mass is a scalar, not a vector, as it’s just one number describing the amount of matter in an object. I could get fancy and compute the area of a shape as its mass, but it’s simpler to begin by saying, “Hey, the mass of this object is . . . um, I dunno . . . how about 10?”
@@ -183,13 +183,13 @@
Units of Measurement
Now you move on to object moverB. It also receives the wind force—(1, 0). Wait, hold on a second. What’s the value of the wind force? Taking a closer look, it’s actually now (0.1, 0)! Remember that when you pass an object (in this case, p5.Vector) into a function, you’re passing a reference to that object. It’s not a copy! So if a function makes a change to that object (which, in this case, it does by dividing by the mass), that object is permanently changed. But I don’t want moverB to receive a force divided by the mass of object moverA. I want it to receive the force in its original state—(1, 0). And so I must protect the original vector and make a copy of it before dividing by mass.
Fortunately, the p5.Vector class has a convenient method for making a copy: copy(). It returns a new p5.Vector object with the same data. And so I can revise applyForce() as follows:
applyForce(force) {
- //{!1} Make a copy of the vector before using it!
+ //{!1} Make a copy of the vector before using it.
let f = force.copy();
- //{!1} Divide the copy by mass!
+ //{!1} Divide the copy by mass.
f.div(this.mass);
this.acceleration.add(f);
}
-
Let’s take a moment to recap what I‘ve covered so far. I've defined what a force is (a vector), and I’ve shown how to apply a force to an object (divide it by mass and add it to the object’s acceleration vector). What’s missing? Well, I have yet to figure out how to calculate a force in the first place. Where do forces come from?
+
Let’s take a moment to recap what I’ve covered so far. I’ve defined what a force is (a vector), and I’ve shown how to apply a force to an object (divide it by mass and add it to the object’s acceleration vector). What’s missing? Well, I have yet to figure out how to calculate a force in the first place. Where do forces come from?
Exercise 2.2
You could write applyForce() in another way, using the static method div() instead of copy(). Rewrite applyForce() by using the static method. For help with this exercise, review static methods in “Static vs. Nonstatic Methods” on page XX.
@@ -199,7 +199,7 @@
Exercise 2.2
}
Creating Forces
-
This section presents two ways to create for creating forces in a p5.js world:
+
This section presents two ways to create forces in a p5.js world:
Make up a force! After all, you’re the programmer, the creator of your world. There’s no reason you can’t just make up a force and apply it.
Model a force! Forces exist in the physical world, and physics textbooks often contain formulas for these forces. You can take these formulas and translate them into source code to model real-world forces in JavaScript.
@@ -207,7 +207,7 @@
Creating Forces
To begin, I’ll focus on the first approach. The easiest way to make up a force is to just pick a number (or two numbers, really). Let’s start with simulating wind. How about a wind force that points to the right and is fairly weak? Assuming an object mover, the code would read as follows:
let wind = createVector(0.01, 0);
mover.applyForce(wind);
-
The result isn’t terribly interesting but is a good place to start. I create a p5.Vector object, initialize it, and pass it into a Mover object (which in turn will apply it to its own acceleration). To finish off this example, I‘ll add one more force, gravity (pointing down), and engage the wind force only when the mouse is pressed.
+
The result isn’t terribly interesting but is a good place to start. I create a p5.Vector object, initialize it, and pass it into a Mover object (which in turn will apply it to its own acceleration). To finish off this example, I’ll add one more force, gravity (pointing down), and engage the wind force only when the mouse is pressed.
Example 2.1: Forces
@@ -223,7 +223,7 @@
Example 2.1: Forces
mover.applyForce(wind);
}
Now I have two forces, pointing in different directions and with different magnitudes, both applied to the object mover. I’m beginning to get somewhere. I’ve built a world, an environment with forces that act on objects!
-
Let’s look at what happens now when I add a second object with a variable mass. To do this, you’ll probably want to do a quick review of OOP. Again, I’m not covering all the basics of programming here (for that you can check out any of the intro p5.js books or video tutorials listed in “The Coding Train Connection” on page XX). However, since the idea of creating a world filled with objects is fundamental to all the examples in this book, it’s worth taking a moment to walk through the steps of going from one object to many.
+
Let’s look at what happens now when I add a second object with a variable mass. To do this, you’ll probably want to do a quick review of OOP. Again, I’m not covering all the basics of programming here (for that, you can check out any of the intro p5.js books or video tutorials listed in “The Coding Train Connection” on page XX). However, since the idea of creating a world filled with objects is fundamental to all the examples in this book, it’s worth taking a moment to walk through the steps of going from one object to many.
This is where I left the Mover class. Notice that it’s identical to the Mover class created in Chapter 1, with two additions, mass and a new applyForce() method:
class Mover {
constructor() {
@@ -293,12 +293,12 @@
Notice that the mass and position are no longer set to hardcoded numbers, but rather are initialized via the x, y, and mass arguments passed to the constructor. This means I can create a variety of Mover objects: big ones, small ones, ones that start on the left side of the canvas, ones that start on the right, and everywhere in between:
+
Notice that the mass and position are no longer set to hardcoded numbers, but rather are initialized via the x, y, and mass arguments passed to the constructor. This means I can create a variety of Mover objects—big ones, small ones, ones that start on the left side of the canvas, ones that start on the right, and everywhere in between:
// A large mover on the left side of the canvas
let moverA = new Mover(100, 30, 10);
// A smaller mover on the right side of the canvas
let moverB = new Mover(400, 30, 2);
-
I could choose to initialize the values in all sorts of ways (random, Perlin noise, in a grid, and so on). Here I’ve just picked some numbers for demonstration purposes. I‘ll introduce other techniques for initializing a simulation throughout this book.
+
I could choose to initialize the values in all sorts of ways (random, Perlin noise, in a grid, and so on). Here I’ve just picked some numbers for demonstration purposes. I’ll introduce other techniques for initializing a simulation throughout this book.
Once the objects are declared and initialized, the rest of the code follows as before. For each object, pass the forces in the environment to applyForce() and enjoy the show!
Example 2.2: Forces Acting on Two Objects
@@ -333,7 +333,7 @@
Example 2.2: Forces Acting on T
Notice that every operation in the code is written twice, once for moverA and once for moverB. In practice, an array would make more sense than separate variables to manage multiple Mover objects, particularly as their number increases. That way, I’d have to write each operation only once and use a loop to apply it to each Mover in the array. I’ll demonstrate this later in the chapter and cover arrays in greater detail in Chapter 4.
Exercise 2.3
-
Instead of objects bouncing off the edge of the wall, create an example that includes an invisible force pushing back on the objects to keep them in the window. Can you weight the force according to the object’s distance from an edge, so that the closer it is, the stronger the force?
+
Instead of objects bouncing off the edge of the wall, create an example that includes an invisible force pushing back on the objects to keep them in the window. Can you weight the force according to the object’s distance from an edge so that the closer it is, the stronger the force?
Exercise 2.4
@@ -344,7 +344,7 @@
Exercise 2.5
Create a wind force that’s variable. Can you make it interactive? For example, think of a fan located where the mouse is and pointed toward the circles.
When you run the code in Example 2.2, notice that the small circle responds more dramatically to the forces applied to it than the large one. This is because of the formula acceleration = force divided by mass. Mass is in the denominator, so the larger it is, the smaller the acceleration. This makes sense for the wind force—the more massive an object, the harder it should be for the wind to push it around—but is it accurate for a simulation of Earth’s gravitational pull?
-
If you were to climb to the top of the Leaning Tower of Pisa and drop two balls of different masses, which one would hit the ground first? According to legend, Galileo performed this exact test in 1589, discovering that they fell with the same acceleration, hitting the ground at the same time. Why? I‘ll dive deeper into this shortly, but the quick answer is that even though the force of gravity is calculated relative to an object’s mass—so that the bigger the object, the stronger the force—that force is canceled out when you divide by the mass to determine the acceleration. Therefore, the acceleration of gravity for different objects is equal.
+
If you were to climb to the top of the Leaning Tower of Pisa and drop two balls of different masses, which one would hit the ground first? According to legend, Galileo performed this exact test in 1589, discovering that they fell with the same acceleration, hitting the ground at the same time. Why? I’ll dive deeper into this shortly, but the quick answer is that even though the force of gravity is calculated relative to an object’s mass—so that the bigger the object, the stronger the force—that force is canceled out when you divide by the mass to determine the acceleration. Therefore, the acceleration of gravity for different objects is equal.
A quick fix to the sketch—one that moves a step closer to realistically modeling a force rather than simply making up a force—is to implement this scaling by multiplying the gravity force by mass.
Example 2.3: Gravity Scaled by Mass
@@ -366,7 +366,7 @@
Example 2.3: Gravity Scaled by Mass
The objects now fall at the same rate. I’m still basically making up the gravity force by arbitrarily setting it to 0.1, but by scaling the force according to the object’s mass, I’m making it up in a way that’s a little truer to Earth’s actual force of gravitational attraction. Meanwhile, because the strength of the wind force is independent of mass, the smaller circle still accelerates to the right more quickly when the mouse is pressed. (The online code for this example also includes a solution to Exercise 2.4, with the addition of a radius variable in the Mover class.)
Modeling a Force
Making up forces will actually get you quite far—after all, I just made up a pretty good approximation of Earth’s gravity. Ultimately, the world of p5.js is an orchestra of pixels, and you’re the conductor, so whatever you deem appropriate to be a force, well by golly, that’s the force it should be! Nevertheless, there may come a time when you find yourself wondering, “But how does it all really work?” That’s when modeling forces, instead of just making them up, enters the picture.
-
Open up any high-school physics textbook and you’ll find diagrams and formulas describing various forces—gravity, electromagnetism, friction, tension, elasticity, and more. For the rest of this chapter, I’m going to consider three forces—friction, drag, and gravitational attraction—and show how to model them with p5.js. The point I’d like to make here is not that these are fundamental forces that you always need in your simulations. Rather, I want to demonstrate these forces as case studies for the following process:
+
Open up any high school physics textbook and you’ll find diagrams and formulas describing various forces—gravity, electromagnetism, friction, tension, elasticity, and more. For the rest of this chapter, I’m going to consider three forces—friction, drag, and gravitational attraction—and show how to model them with p5.js. The point I’d like to make here is not that these are fundamental forces that you always need in your simulations. Rather, I want to demonstrate these forces as case studies for the following process:
Understanding the concept behind a force
Deconstructing the force’s formula into two parts:
@@ -377,10 +377,10 @@
Modeling a Force
Translating that formula into p5.js code that calculates a vector to be passed through a Mover object’s applyForce() method
-
If you can follow these steps with the example forces I‘ll provide here, then hopefully when you find yourself googling atomic nuclei weak nuclear force at 3 AM, you’ll have the skills to take what you find and adapt it for p5.js.
+
If you can follow these steps with the example forces I’ll provide here, then hopefully when you find yourself googling atomic nuclei weak nuclear force at 3 AM, you’ll have the skills to take what you find and adapt it for p5.js.
Parsing Formulas
-
In a moment, I’m going to write out the formula for friction. This won’t be the first time you’ve seen a formula in this book; I just finished up the discussion of Newton’s second law, \vec{F} = M \times \vec{A} (or force equals mass times acceleration). You hopefully didn’t spend a lot of time worrying about that formula, because it’s just a few characters and symbols. Nevertheless, it’s a scary world out there. Just take a look at the equation for a normal distribution, which I covered (without presenting the formula) in “A Normal Distribution of Random Numbers” on page XX.
+
In a moment, I’m going to write out the formula for friction. This won’t be the first time you’ve seen a formula in this book; I just finished up the discussion of Newton’s second law, \vec{F} = M \times \vec{A} (or force equals mass times acceleration). You hopefully didn’t spend a lot of time worrying about that formula, because it’s just a few characters and symbols. Nevertheless, it’s a scary world out there. Just take a look at the equation for a normal distribution, which I covered (without presenting the formula) in “A Normal Distribution of Random Numbers” on page XX:
Formulas are regularly written with many symbols (often with letters from the Greek alphabet). Here’s the formula for friction (as indicated by \vec{f}):
\vec{f} = -\mu N \hat{v}
@@ -388,13 +388,12 @@
Parsing Formulas
Evaluate the right side; assign to the left side. This is just like in code! In the preceding case, the left side represents what I want to calculate—the force of friction—and the right side elaborates on how to do it.
Am I talking about a vector or a scalar? It’s important to realize that in some cases, you’ll be calculating a vector; in others, a scalar. For example, in this case, the force of friction is a vector. That is indicated by the arrow above the f. It has a magnitude and direction. The right side of the equation also has a vector, as indicated by the symbol \hat{v}, which in this case stands for the velocity unit vector.
-
When symbols are placed next to each other, this typically means multiply them. The right side of the friction formula has four elements: -, μ, N, and \hat{v}. They should be multiplied together, reading the formula as \vec{f} = -1 \times \mu \times N \times \hat{v}.
+
When symbols are placed next to each other, this typically means multiply them. The right side of the friction formula has four elements: –, μ, N, and \hat{v}. They should be multiplied together, reading the formula as \vec{f} = -1 \times \mu \times N \times \hat{v}.
Friction
Let’s begin with friction and follow the preceding steps. Whenever two surfaces come into contact, they experience friction. Friction is a dissipative force, meaning it causes the kinetic energy of an object to be converted into another form, giving the impression of loss, or dissipation.
-
Let’s say you’re driving a car. When you press your foot on the brake pedal, the car’s brakes use friction to slow the motion of the tires. Kinetic energy (motion) is converted into thermal energy (heat). A complete model of friction would include separate cases for static friction (a body at rest against a surface) and kinetic friction (a body in motion against a surface), but for simplicity here, I’m going to work through only the kinetic case.
-
Figure 2.3 shows the formula for friction.
+
Let’s say you’re driving a car. When you press your foot on the brake pedal, the car’s brakes use friction to slow the motion of the tires. Kinetic energy (motion) is converted into thermal energy (heat). A complete model of friction would include separate cases for static friction (a body at rest against a surface) and kinetic friction (a body in motion against a surface), but for simplicity here, I’m going to work through only the kinetic case. Figure 2.3 shows the formula for friction.
Figure 2.3: Friction is a force that points in the opposite direction of the sled’s velocity when the sled is sliding in contact with the hill.
@@ -406,7 +405,7 @@
Friction
// (a unit vector in the opposite direction of velocity).
friction.mult(-1);
Notice two additional steps here. First, it’s important to make a copy of the velocity vector, as I don’t want to reverse the object’s direction by accident. Second, the vector is normalized. This is because the magnitude of friction isn’t associated with the speed of the object, and I want to start with a vector of length 1 so it can easily be scaled.
-
According to the formula, the magnitude is \mu \times N. The Greek letter mu (\mu, pronounced mew), is used here to describe the coefficient of friction. The coefficient of friction establishes the strength of a friction force for a particular surface. The higher it is, the stronger the friction; the lower, the weaker. A block of ice, for example, will have a much lower coefficient of friction than, say, sandpaper. Since this is a pretend p5.js world, I can arbitrarily set the coefficient to scale the strength of the friction:
+
According to the formula, the magnitude is \mu \times N. The Greek letter mu (\mu, pronounced mew) is used here to describe the coefficient of friction. The coefficient of friction establishes the strength of a friction force for a particular surface. The higher it is, the stronger the friction; the lower, the weaker. A block of ice, for example, will have a much lower coefficient of friction than, say, sandpaper. Since this is a pretend p5.js world, I can arbitrarily set the coefficient to scale the strength of the friction:
let c = 0.01;
Now for the second part. N refers to the normal force, the force perpendicular to the object’s motion along a surface. Think of a vehicle driving along a road. The vehicle pushes down against the road with gravity, and Newton’s third law tells us that the road, in turn, pushes back against the vehicle. That’s the normal force. The greater the gravitational force, the greater the normal force.
As you’ll see in the next section, gravitational attraction is associated with mass, and so a lightweight sports car would experience less friction than a massive tractor trailer truck. In Figure 2.3, however, because the object is moving along a surface at an angle, computing the magnitude and direction of the normal force is a bit more complex because it doesn’t point in the opposite direction of gravity. You’d need to know something about angles and trigonometry.
@@ -425,7 +424,7 @@
Friction
// Take the unit vector and multiply it by the magnitude. This is the force vector!
friction.mult(frictionMag);
-
This code calculates a friction force but doesn‘t answer the question of when to apply it. There’s no answer to this question, of course, given this is all a made-up world visualized in a 2D p5.js canvas! I‘ll make the arbitrary, but logical, decision to apply friction when the circle comes into contact with the bottom of the canvas, which I can detect by adding a function to the Mover class called contactEdge():
+
This code calculates a friction force but doesn’t answer the question of when to apply it. There’s no answer to this question, of course, given this is all a made-up world visualized in a 2D p5.js canvas! I’ll make the arbitrary, but logical, decision to apply friction when the circle comes into contact with the bottom of the canvas, which I can detect by adding a function to the Mover class, called contactEdge():
contactEdge() {
// The mover is touching the edge when it’s within 1 pixel.
return (this.position.y > height - this.radius - 1);
@@ -433,7 +432,7 @@
Friction
This is a good time for me to also mention that the actual bouncing off the edge here simulates an idealized elastic collision, meaning no kinetic energy is lost when the circle and the edge collide. This is rarely true in the real world; pick up a tennis ball and drop it against any surface, and the height at which it bounces will slowly lower until it rests against the ground. Many factors are at play here (including air resistance, which I’ll cover in the next section), but a quick way to simulate an inelastic collision is to reduce the magnitude of velocity by a percentage with each bounce:
bounceEdges() {
// A new variable to simulate an inelastic collision:
- // 10% of the velocity’s x- or y-component is lost
+ // 10% of the velocity’s x- or y-component is lost.
let bounce = -0.9;
if (this.position.y > height - this.radius) {
@@ -477,7 +476,7 @@
Example 2.4: Including Friction
mover.show();
}
-
Running this example, you’ll notice that the circle eventually comes to rest. You can make this happen more or less quickly by varying the coefficient of friction as well as the percentage speed loss in the bounceEdges() method.
+
Running this example, you’ll notice that the circle eventually comes to rest. You can make this happen more or less quickly by varying the coefficient of friction as well as the percentage of speed lost in the bounceEdges() method.
Exercise 2.6
Add a second object to Example 2.4. How do you handle having two objects of different masses? What if each object has its own coefficient of friction relative to the bottom surface? Does it make sense to encapsulate the friction force calculation into a Mover method?
@@ -527,7 +526,7 @@
Air and Fluid Resistance
this.y = y;
this.w = w;
this.h = h;
- // The Liquid object includes a variable defining its coefficient of drag.
+ // The Liquid object includes a variable defining its coefficient of drag.
this.c = c;
}
@@ -541,10 +540,10 @@
Air and Fluid Resistance
let liquid;
function setup() {
- // Initialize a Liquid object. I’m choosing a low coefficient (0.1) for a weaker effect. Try a stronger one!
+ // Initialize a Liquid object. I’m choosing a low coefficient (0.1) for a weaker effect. Try a stronger one!
liquid = new Liquid(0, height / 2, width, height / 2, 0.1);
}
-
Now comes an interesting question: how does the Mover object talk to the Liquid object? I want to implement the following:
+
Now comes an interesting question: How does the Mover object talk to the Liquid object? I want to implement the following:
When a mover passes through a liquid, that mover experiences a drag force.
Translating that into object-oriented speak:
// If the liquid contains the mover, apply the drag force.
@@ -555,26 +554,26 @@
Air and Fluid Resistance
This code serves as instructions for what I need to add to the Liquid class: (1) a contains() method that determines whether a Mover object is inside the Liquid object’s area, and (2) a drag() method that calculates and returns the appropriate drag force to be applied to the Mover.
The first is easy; I can use a Boolean expression to determine whether the position vector rests inside the rectangle defined by the liquid:
contains(mover) {
- // Store position in a separate variable to make the code more readable.
+ // Store position in a separate variable to make the code more readable.
let pos = mover.position;
- // This Boolean expression determines whether the position vector is contained within the rectangle defined by the Liquid class.
+ // This Boolean expression determines whether the position vector is contained within the rectangle defined by the Liquid class.
return (pos.x > this.x && pos.x < this.x + this.w &&
pos.y > this.y && pos.y < this.y + this.h);
}
The calculateDrag() method is pretty easy too: I basically already wrote the code for it when I implemented the simplified drag formula! The drag force is equal to the coefficient of drag multiplied by the speed of the mover squared, in the opposite direction of velocity:
calculateDrag(mover) {
let speed = mover.velocity.mag();
- // Calculate the force’s magnitude
+ // Calculate the force’s magnitude.
let dragMagnitude = this.c * speed * speed;
- // Calculate the force’s direction
+ // Calculate the force’s direction.
let dragForce = mover.velocity.copy();
dragForce.mult(-1);
- // Finalize the force: Set the magnitude and direction together.
+ // Finalize the force: set the magnitude and direction together.
dragForce.setMag(dragMagnitude);
// Return the force.
return dragForce;
}
-
With these two methods added to the Liquid class, I’m ready to put all the code together! In the following example, I‘ll expand the code to use an array of evenly spaced Mover objects in order to demonstrate how the drag force behaves with objects of variable mass. This also illustrates an alternate way to initialize a simulation other than randomly. Look for 40 + i * 70 in the code. An initial offset of 40 provides a small margin from the edge of the canvas, and i * 70 uses the index of the object to evenly space the movers. The margin and multiplier are arbitrary; you might try other values or consider other ways to calculate the spacing based on the canvas dimensions.
+
With these two methods added to the Liquid class, I’m ready to put all the code together! In the following example, I’ll expand the code to use an array of evenly spaced Mover objects in order to demonstrate how the drag force behaves with objects of variable mass. This also illustrates an alternate way to initialize a simulation other than randomly. Look for 40 + i * 70 in the code. An initial offset of 40 provides a small margin from the edge of the canvas, and i * 70 uses the index of the object to evenly space the movers. The margin and multiplier are arbitrary; you might try other values or consider other ways to calculate the spacing based on the canvas dimensions.
Example 2.5: Fluid Resistance
@@ -588,11 +587,11 @@
Example 2.5: Fluid Resistance
function setup() {
createCanvas(640, 240);
- // Initialize an array of Mover objects.
+ // Initialize an array of Mover objects.
for (let i = 0; i < 9; i++) {
// Use a random mass for each one.
let mass = random(0.1, 5);
- // The x-values are spaced out evenly according to i.
+ // The x-values are spaced out evenly according to i.
movers[i] = new Mover(40 + i * 70, 0, mass);
}
liquid = new Liquid(0, height / 2, width, height / 2, 0.1);
@@ -644,7 +643,7 @@
Gravitational Attraction
Figure 2.6: The gravitational force between two bodies is proportional to the mass of those bodies and inversely proportional to the square of the distance between them.
-
Probably the most famous force of all is gravitational attraction. We humans on Earth think of gravity as stuff falling down, like an apple hitting Isaac Newton on the head. But this is only our experience of gravity. The reality is more complicated.
+
Probably the most famous force of all is gravitational attraction. We humans on Earth think of gravity as stuff falling down, like an apple hitting Sir Isaac Newton on the head. But this is only our experience of gravity. The reality is more complicated.
In truth, just as Earth pulls the apple toward it because of a gravitational force, the apple pulls Earth as well (this is Newton’s third law). Earth is just so freaking massive that it overwhelms all the other gravity interactions. In fact, every object with mass exerts a gravitational force on every other object. The formula for calculating the strengths of these forces is depicted in Figure 2.6.
Let’s examine this formula a bit more closely:
@@ -685,7 +684,7 @@
Gravitational Attraction
The only problem is that I don’t know the distance. The values of G, mass1, and mass2 are all givens, but I need to calculate distance before the preceding code will work. But wait, didn’t I just make a vector that points all the way from one object’s position to the other? The length of that vector should be the distance between the two objects (see Figure 2.8).
-
Indeed, if I add one more line of code and grab the magnitude of that vector before normalizing it, I’ll have the distance. And this time, I‘ll skip the normalize() step and use setMag():
+
Indeed, if I add one more line of code and grab the magnitude of that vector before normalizing it, I’ll have the distance. And this time, I’ll skip the normalize() step and use setMag():
// The vector that points from one object to another
let force = p5.Vector.sub(position2, position1);
@@ -710,10 +709,10 @@
Gravitational Attraction
A single Attractor object (a new class that will have a fixed position)
The Mover object will experience a gravitational pull toward the Attractor object, as illustrated in Figure 2.9.
-
I‘ll start by creating a basic Attractor class, giving it a position and a mass, along with a method to draw itself (tying mass to size):
+
I’ll start by creating a basic Attractor class, giving it a position and a mass, along with a method to draw itself (tying mass to size):
class Attractor {
constructor() {
- // The Attractor is an object that doesn’t move. It needs just a mass and a position.
+ // The attractor is an object that doesn’t move. It needs just a mass and a position.
this.position = createVector(width / 2, height / 2);
this.mass = 20;
}
@@ -731,14 +730,14 @@
Gravitational Attraction
function setup() {
createCanvas(640, 360);
mover = new Mover(300, 100, 5);
- // Initialize the Attractor object.
+ // Initialize the Attractor object.
attractor = new Attractor();
}
function draw() {
background(255);
- // Draw the Attractor object.
+ // Draw the Attractor object.
attractor.show();
mover.update();
@@ -780,7 +779,7 @@
Gravitational Attraction
-
It’s good to consider a range of options, and you could probably make arguments for each of these approaches. I’d like to at least discard the first one, since I tend to prefer an object-oriented approach rather than an arbitrary function not tied to either the Mover or Attractor class. Whether you pick option 2 or option 3 is the difference between saying, “The attractor attracts the mover” or “The mover is attracted to the attractor.” Option 4 is really my favorite, though. I spent a lot of time working out the applyForce() method, and I think the examples are clearer continuing with the same technique of using this method to apply the forces.
+
It’s good to consider a range of options, and you could probably make arguments for each of these approaches. I’d like to at least discard the first one, since I tend to prefer an object-oriented approach rather than an arbitrary function not tied to either the Mover or Attractor class. Whether you pick option 2 or option 3 is the difference between saying, “The attractor attracts the mover” and “The mover is attracted to the attractor.” Option 4 is really my favorite, though. I spent a lot of time working out the applyForce() method, and I think the examples are clearer continuing with the same technique of using this method to apply the forces.
In other words, where I once wrote
// Made-up force
let force = createVector(0, 0.1);
@@ -914,7 +913,7 @@
Example 2.7: Attraction with Man
attractor.show();
for (let i = 0; i < movers.length; i++) {
- //{!1} Calculate an attraction force for each Mover object.
+ //{!1} Calculate an attraction force for each Mover object.
let force = attractor.attract(movers[i]);
movers[i].applyForce(force);
@@ -937,8 +936,8 @@
The n-Body Problem
To begin, while having separate Mover and Attractor classes has been helpful so far, this distinction is a bit misleading. After all, according to Newton’s third law, all forces occur in pairs: if an attractor attracts a mover, then that mover should also attract the attractor. Instead of two classes here, what I really want is a single type of thing—called, for example, a Body—with every body attracting every other body.
The scenario I’m describing is commonly referred to as the n-body problem. It involves solving for the motion of a group of objects that interact via gravitational forces. The two-body problem is a famously solved problem, meaning the motions can be precisely computed with mathematical equations when only two bodies are involved. However, adding one more body turns the two-body problem into a three-body problem, and suddenly no formal solution exists (see Figure 2.10).
-
- Figure 2.10: Example paths of the two-body (predictable) vs. three-body (complex) problems
+
+ Figure 2.10: Example paths of the two-body (predictable) versus three-body (complex) problems
Although less accurate than using precise equations of motion, the examples built in this chapter can model both the two-body and three-body problems. To begin, I’ll move the attract() method from the Attractor class into the Mover class (which I will now call Body):
// The mover is now called a body.
@@ -961,7 +960,7 @@
The n-Body Problem
function setup() {
createCanvas(640, 240);
- // Create two Body objects, A and B.
+ // Create two Body objects, A and B.
bodyA = new Body(320, 40);
bodyB = new Body(320, 200);
}
@@ -969,7 +968,7 @@
The n-Body Problem
function draw() {
background(255);
- //{!2} A attracts B, and B attracts A.
+ //{!2} A attracts B, and B attracts A.
bodyA.attract(bodyB);
bodyB.attract(bodyA);
@@ -998,7 +997,7 @@
Example 2.8: Two-Body Attraction
Example 2.8 could be improved by refactoring the code to include constructor arguments that assign the body velocities. For now, however, this approach serves as a quick way to experiment with patterns based on various initial positions and velocities.
Exercise 2.14
-
The paper “Classification of Symmetry Groups for Planar n-Body Choreographies” by James Montaldi and Katrina Steckles (2013) explores choreographic solutions to the n-body problem (defined as periodic motions of bodies following one another at regular intervals). Educator and artist Dan Gries created an interactive demonstration of these choreographies. Try adding a third (or more!) body to Example 2.8 and experiment with setting initial positions and velocities. What choreographies can you achieve?
+
The paper “Classification of Symmetry Groups for Planar n-Body Choreographies” by James Montaldi and Katrina Steckles explores choreographic solutions to the n-body problem (defined as periodic motions of bodies following one another at regular intervals). Educator and artist Dan Gries created an interactive demonstration of these choreographies. Try adding a third (or more!) body to Example 2.8 and experiment with setting initial positions and velocities. What choreographies can you achieve?
I’m now ready to move on to an example with n bodies by incorporating an array:
// Start with an empty array.
@@ -1006,7 +1005,7 @@
Exercise 2.14
function setup() {
createCanvas(640, 240);
- // Fill the array with Body objects.
+ // Fill the array with Body objects.
for (let i = 0; i < 10; i++) {
bodies[i] = new Body(random(width), random(height));
}
@@ -1071,7 +1070,7 @@
Can you arrange the bodies of the n-body simulation to orbit the center of the canvas in a pattern that resembles a spiral galaxy? You may need to include an additional large body in the center to hold everything together. A solution is offered in my “Mutual Attraction” video in the Nature of Code series on the Coding Train website.
Bridget Riley, a celebrated British artist, was a driving force behind the Op Art movement of the 1960s. Her work features geometric patterns that challenge the viewer’s perceptions and evoke feelings of movement or vibration. Her 1974 piece Gala showcases a series of curvilinear forms that ripple across the canvas, evoking the natural rhythm of the sine wave.
@@ -44,15 +44,15 @@
Angles
What Is Pi?
The mathematical constant pi (or the Greek letter \pi) is a real number defined as the ratio of a circle’s circumference (the distance around the outside of the circle) to its diameter (a straight line that passes through the circle’s center). It’s equal to approximately 3.14159 and can be accessed in p5.js with the built-in PI variable.
-
The formula to convert from degrees to radians is as follow:
+
The formula to convert from degrees to radians is as follows:
Thankfully, if you prefer to think of angles in degrees, you can call angleMode(DEGREES), or you can use the convenience function radians() to convert values from degrees to radians. The constantsPI, TWO_PI, and HALF_PI are also available (equivalent to 180, 360, and 90 degrees, respectively). For example, here are two ways in p5.js to rotate a shape by 60 degrees:
+
Thankfully, if you prefer to think of angles in degrees, you can call angleMode(DEGREES), or you can use the convenience function radians() to convert values from degrees to radians. The constants PI, TWO_PI, and HALF_PI are also available (equivalent to 180, 360, and 90 degrees, respectively). For example, here are two ways in p5.js to rotate a shape by 60 degrees:
let angle = 60;
rotate(radians(angle));
angleMode(DEGREES);
rotate(angle);
-
While degrees can be useful, for the purposes of this book, I’ll be working with radians because they’re the standard unit of measurement across many programming languages and graphics environments. If they‘re new to you, this is a good opportunity to practice! Additionally, if you aren’t familiar with the way rotation is implemented in p5.js, I recommend watching my Coding Train video series on transformations in p5.js.
+
While degrees can be useful, for the purposes of this book, I’ll be working with radians because they’re the standard unit of measurement across many programming languages and graphics environments. If they’re new to you, this is a good opportunity to practice! Additionally, if you aren’t familiar with the way rotation is implemented in p5.js, I recommend watching my Coding Train video series on transformations in p5.js.
Exercise 3.1
Rotate a baton-like object around its center by using translate() and rotate().
@@ -62,7 +62,7 @@
Exercise 3.1
Angular Motion
-
Another term for rotation is angular motion—that is, motion about an angle. Just as linear motion can be described in terms of velocity—the rate at which an object’s position changes over time—angular motion can be described in terms of angular velocity—that rate at which an object’s angle changes over time. By extension, angular acceleration describes changes in an object’s angular velocity.
+
Another term for rotation is angular motion—that is, motion about an angle. Just as linear motion can be described in terms of velocity—the rate at which an object’s position changes over time—angular motion can be described in terms of angular velocity—the rate at which an object’s angle changes over time. By extension, angular acceleration describes changes in an object’s angular velocity.
Luckily, you already have all the math you need to understand angular motion. Remember the stuff I dedicated almost all of Chapters 1 and 2 to explaining?
Example 3.1: Angular Motion Usin
circle(60, 0, 16);
circle(-60, 0, 16);
- // Angular equivalent of velocity.add(acceleration)
+ // Angular equivalent of velocity.add(acceleration)
angleVelocity += angleAcceleration;
- // Angular equivalent of position.add(velocity)
+ // Angular equivalent of position.add(velocity)
angle += angleVelocity;
}
Instead of incrementing angle by a fixed amount to steadily rotate the baton, for every frame I add angleAcceleration to angleVelocity, then add angleVelocity to angle. As a result, the baton starts with no rotation and then spins faster and faster as the angular velocity accelerates.
@@ -164,11 +164,11 @@
Exercise 3.2
}
At this point, if you were to actually go ahead and create a Mover object, you wouldn’t see it behave any differently. This is because the angular acceleration is initialized to zero (this.angleAcceleration = 0;). For the object to rotate, it needs a nonzero acceleration! Certainly, one option is to hardcode a number in the constructor:
this.angleAcceleration = 0.01;
-
You can produce a more interesting result, however, by dynamically assigning an angular acceleration in the update() method according to forces in the environment. This could be my cue to start researching the physics of angular acceleration based on the concepts of torque and moment of inertia, but at this stage, that level of simulation would be a bit of a rabbit hole. (I’ll cover more about modeling angular acceleration with a pendulum later in “The Pendulum” on page 156, as well as look at how third-party physics libraries realistically model rotational motion in Chapter 6.)
+
You can produce a more interesting result, however, by dynamically assigning an angular acceleration in the update() method according to forces in the environment. This could be my cue to start researching the physics of angular acceleration based on the concepts of torque and moment of inertia, but at this stage, that level of simulation would be a bit of a rabbit hole. (I’ll cover modeling angular acceleration with a pendulum in more detail in “The Pendulum” on page 156, as well as look at how third-party physics libraries realistically model rotational motion in Chapter 6.)
Instead, a quick-and-dirty solution that yields creative results will suffice. A reasonable approach is to calculate angular acceleration as a function of the object’s linear acceleration, its rate of change of velocity along a path vector, as opposed to its rotation. Here’s an example:
// Use the x-component of the object’s linear acceleration to calculate angular acceleration.
this.angleAcceleration = this.acceleration.x;
-
Yes, this is arbitrary, but it does do something. If the object is accelerating to the right, its angular rotation accelerates in a clockwise direction; acceleration to the left results in a counterclockwise rotation. Of course, it’s important to think about scale in this case. The value of the acceleration vector’s x component might be too large, causing the object to spin in a way that looks ridiculous or unrealistic. You might even notice a visual illusion called the wagon wheel effect: an object appears to be rotating slower or even in the opposite direction because of the large changes between each frame of animation.
+
Yes, this is arbitrary, but it does do something. If the object is accelerating to the right, its angular rotation accelerates in a clockwise direction; acceleration to the left results in a counterclockwise rotation. Of course, it’s important to think about scale in this case. The value of the acceleration vector’s x component might be too large, causing the object to spin in a way that looks ridiculous or unrealistic. You might even notice a visual illusion called the wagon wheel effect: an object appears to be rotating more slowly or even in the opposite direction because of the large changes between each frame of animation.
Dividing the x component by a value, or perhaps constraining the angular velocity to a reasonable range, could really help. Here’s the entire update() function with these tweaks added.
Example 3.2: Forces with (Arbitrary) Angular Motion
@@ -197,22 +197,22 @@
Exercise 3.3
Step 2: Add rotation to the object to model its spin as it’s shot from the cannon. How realistic can you make it look?
Trigonometry Functions
-
I think I’m ready to reveal the secret of trigonometry. I’ve discussed angles, I’ve spun a baton. Now it’s time for . . . wait for it . . . sohcahtoa. Yes, sohcahtoa! This seemingly nonsensical word is actually the foundation for much of computer graphics work. A basic understanding of trigonometry is essential if you want to calculate angles, figure out distances between points, and work with circles, arcs, or lines. And sohcahtoa is a mnemonic device (albeit a somewhat absurd one) for remembering the meanings of the trigonometric functions sine, cosine, and tangent. It references the sides of a right triangle, as shown in Figure 3.4.
+
I think I’m ready to reveal the secret of trigonometry. I’ve discussed angles, I’ve spun a baton. Now it’s time for . . . wait for it . . . sohcahtoa. Yes, sohcahtoa! This seemingly nonsensical word is actually the foundation for much of computer graphics work. A basic understanding of trigonometry is essential if you want to calculate angles, figure out distances between points, and work with circles, arcs, or lines. And sohcahtoa is a mnemonic device (albeit a somewhat absurd one) for remembering the meanings of the trigonometric functions sine, cosine, and tangent. It references the sides of a right triangle, as shown in Figure 3.4.
Figure 3.4: A right triangle showing the sides as adjacent, opposite, and hypotenuse
Take one of the non-right angles in the triangle. The adjacent side is the one touching that angle, the opposite side is the one not touching that angle, and the hypotenuse is the side opposite the right angle. Sohcahtoa tells you how to calculate the angle’s trigonometric functions in terms of the lengths of these sides:
Take a look at Figure 3.4 again. You don’t need to memorize it, but see if you feel comfortable with it. Try redrawing it yourself. Next, let’s look at it in a slightly different way (see Figure 3.5).
-
- Figure 3.5: A vector \vec{v} with components x, y, and angle.
+
+ Figure 3.5: A vector \vec{v} with components x, y, and angle
See how a right triangle is created from the vector \vec{v}? The vector arrow is the hypotenuse, and the components of the vector (x and y) are the sides of the triangle. The angle is an additional means for specifying the vector’s direction (or heading). Viewed in this way, the trigonometric functions establish a relationship between the components of a vector and its direction + magnitude. As such, trigonometry will prove very useful throughout this book. To illustrate this, let’s look at an example that requires the tangent function.
@@ -280,7 +280,7 @@
Pointing in the Direction of Move
This code is pretty darn close and almost works. There’s a big problem, though. Consider the two velocity vectors depicted in Figure 3.8.
Though superficially similar, the two vectors point in quite different directions—opposite directions, in fact! In spite of this, look at what happens if I apply the inverse tangent formula to solve for the angle of each vector:
I get the same angle! That can’t be right, though, since the vectors are pointing in opposite directions. It turns out this is a pretty common problem in computer graphics. I could use atan() along with conditional statements to account for positive/negative scenarios, but p5.js (along with most programming environments) has a helpful function called atan2() that resolves the issue for me.
@@ -326,7 +326,7 @@
Polar vs. Cartesian Coordinates
The functions for sine and cosine in p5.js are sin() and cos(), respectively. Each takes one argument, a number representing an angle in radians. These formulas can thus be coded as follows:
let r = 75;
let theta = PI / 4;
-// Convert from polar (r, theta) to Cartesian (x, y).
+// Convert from polar (r, theta) to Cartesian (x, y).
let x = r * cos(theta);
let y = r * sin(theta);
This type of conversion can be useful in certain applications. For instance, moving a shape along a circular path using Cartesian coordinates isn’t so easy. However, with polar coordinates, it’s simple: just increment the angle! Here’s how it’s done with global r and theta variables.
@@ -354,7 +354,7 @@
Example 3.4: Polar to Cartesian
// Translate the origin point to the center of the screen.
translate(width / 2, height / 2);
- // Polar coordinates (r, theta) are converted to Cartesian (x, y) for use in the circle() function.
+ // Polar coordinates (r, theta) are converted to Cartesian (x, y) for use in the circle() function.
let x = r * cos(theta);
let y = r * sin(theta);
@@ -405,23 +405,23 @@
Properties of Oscillation
This pattern of oscillating back and forth around a central point is known as simple harmonic motion (or, to be fancier, the periodic sinusoidal oscillation of an object). The code to achieve it is remarkably simple, but before I get into it, I’d like to introduce some of the key terminology related to oscillation (and waves).
When a moving object exhibits simple harmonic motion, its position (in this case, the x-position) can be expressed as a function of time, with the following two elements:
-
Amplitude: The distance from the center of motion to either extreme
-
Period: The duration (time) for one complete cycle of motion
+
Amplitude: The distance from the center of motion to either extreme
+
Period: The duration (time) for one complete cycle of motion
To understand these terms, look again at the graph of the sine function in Figure 3.10. The curve never rises above 1 or below –1 along the y-axis, so the sine function has an amplitude of 1. Meanwhile, the wave pattern of the curve repeats every 2\pi units along the x-axis, so the sine function’s period is 2\pi. (By convention, the units here are radians, since the input value to the sine function is customarily an angle measured in radians.)
So much for the amplitude and period of an abstract sine function, but what are amplitude and period in the p5.js world of an oscillating circle? Well, amplitude can be measured rather easily in pixels. For example, if the canvas is 200 pixels wide, I might choose to oscillate around the center of the canvas, going between 100 pixels right of center and 100 pixels left of center. In other words, the amplitude is 100 pixels:
// The amplitude is measured in pixels.
let amplitude = 100;
-
The period is the amount of time for one complete cycle of an oscillation. However, in a p5.js sketch, what does time really mean? In theory, I could say I want the circle to oscillate every three seconds, then come up with an elaborate algorithm for moving the object according to real-world time, using millis() to track the passage of milliseconds. For what I’m trying to accomplish here, however, real-world time isn’t necessary. The more useful measure of time in p5.js is the number of frames that have elapsed, available through the built-in frameCount variable. Do I want the oscillating motion to repeat every 30 frames? Every 50 frames? For now, how about a period of 120 frames:
+
The period is the amount of time for one complete cycle of an oscillation. However, in a p5.js sketch, what does time really mean? In theory, I could say I want the circle to oscillate every three seconds, then come up with an elaborate algorithm for moving the object according to real-world time, using millis() to track the passage of milliseconds. For what I’m trying to accomplish here, however, real-world time isn’t necessary. The more useful measure of time in p5.js is the number of frames that have elapsed, available through the built-in frameCount variable. Do I want the oscillating motion to repeat every 30 frames? Every 50 frames? For now, how about a period of 120 frames:
// The period is measured in frames (the unit of time for animation).
let period = 120;
Once I have the amplitude and period, it’s time to write a formula to calculate the circle’s x-position as a function of time (the current frame count):
-
// amplitude and period are my own variables; frameCount is built into p5.js.
+
// amplitude and period are my own variables; frameCount is built into p5.js.
let x = amplitude * sin(TWO_PI * frameCount / period);
Think about what’s going here. First, whatever value the sin() function returns is multiplied by amplitude. As you saw in Figure 3.10, the output of the sine function oscillates between –1 and 1. Multiplying that value by my chosen amplitude—call it a—gives me the desired result: a value that oscillates between –a and a. (This is also a place where you could use p5.js’s map() function to map the output of sin() to a custom range.)
Now, think about what’s inside the sin() function:
TWO_PI * frameCount / period
-
What’s going on here? Start with what you know. I‘ve explained that sine has a period of 2\pi, meaning it will start at 0 and repeat at 2\pi, 4\pi, 6\pi, and so on. If my desired period of oscillation is 120 frames, I want the circle to be in the same position when frameCount is at 120 frames, 240 frames, 360 frames, and so on. Here, frameCount is the only value changing over time; it starts at 0 and counts upward. Let’s take a look at what the formula yields as frameCount increases:
+
What’s going on here? Start with what you know. I’ve explained that sine has a period of 2\pi, meaning it will start at 0 and repeat at 2\pi, 4\pi, 6\pi, and so on. If my desired period of oscillation is 120 frames, I want the circle to be in the same position when frameCount is at 120 frames, 240 frames, 360 frames, and so on. Here, frameCount is the only value changing over time; it starts at 0 and counts upward. Let’s take a look at what the formula yields as frameCount increases:
@@ -461,7 +461,7 @@
Properties of Oscillation
Dividing frameCount by period tells me the number of cycles that have been completed. (Is the wave halfway through the first cycle? Have two cycles completed?) Multiplying that number by TWO_PI, I get the desired result, an appropriate input to the sin() function, since TWO_PI is the value required for sine (or cosine) to complete one full cycle.
Putting it together, here’s an example that oscillates the x position of a circle with an amplitude of 100 pixels and a period of 120 frames.
-
Example 3.5: Simple Harmonic Motion
+
Example 3.5: Simple Harmonic Motion I
@@ -498,7 +498,7 @@
Oscillation with Angular Velocity
Now I’ll rewrite it in a slightly different way:
let x = amplitude * sin( some value that increments slowly );
If you care about precisely defining the period of oscillation in terms of frames of animation, you might need the formula as I first wrote it. If you don’t care about the exact period, however—for example, if you’ll be choosing it randomly—all you really need inside the sin() function is a value that increments slowly enough for the object’s motion to appear smooth from one frame to the next. Every time this value ticks past a multiple of 2\pi, the object will have completed one cycle of oscillation.
-
This technique mirrors what I did with Perlin noise in Chapter 0. In that case, I incremented an offset variable (which I called t or xoff) to sample various outputs from the noise() function, creating a smooth transition of values. Now, I’m going to increment a value (I’ll call it angle) that's fed into the sin() function. The difference is that the output from sin() is a smoothly repeating sine wave, without any randomness.
+
This technique mirrors what I did with Perlin noise in Chapter 0. In that case, I incremented an offset variable (which I called t or xoff) to sample various outputs from the noise() function, creating a smooth transition of values. Now, I’m going to increment a value (I’ll call it angle) that’s fed into the sin() function. The difference is that the output from sin() is a smoothly repeating sine wave, without any randomness.
You might be wondering why I refer to the incrementing value as angle, given that the object has no visible rotation. The term angle is used because the value is passed into the sin() function, and angles are the traditional inputs to trigonometric functions. With this in mind, I can reintroduce the concept of angular velocity (and acceleration) to rewrite the example to calculate the x position in terms of a changing angle. I’ll assume these global variables:
let angle = 0;
let angleVelocity = 0.05;
@@ -550,7 +550,7 @@
Example 3.7: Oscillator Objects
class Oscillator {
constructor() {
- // Use a p5.Vector to track two angles!
+ // Use a p5.Vector to track two angles!
this.angle = createVector();
this.angleVelocity = createVector(random(-0.05, 0.05), random(-0.05, 0.05));
// Random velocities and amplitudes
@@ -577,7 +577,7 @@
Example 3.7: Oscillator Objects
pop();
}
}
-
To better understand the Oscillator class, it might be helpful to focus on the movement of a single oscillator in the animation. First, observe its horizontal movement. You’ll notice that it oscillates regularly back and forth along the x-axis. Switching your focus to its vertical movement, you'll see it oscillating up and down along the y-axis. Each oscillator has its own distinct rhythm, given the random initialization of its angle, angular velocity, and amplitude.
+
To better understand the Oscillator class, it might be helpful to focus on the movement of a single oscillator in the animation. First, observe its horizontal movement. You’ll notice that it oscillates regularly back and forth along the x-axis. Switching your focus to its vertical movement, you’ll see it oscillating up and down along the y-axis. Each oscillator has its own distinct rhythm, given the random initialization of its angle, angular velocity, and amplitude.
The key is to recognize that the x and y properties of the p5.Vector objects this.angle, this.angleVelocity, and this.amplitude aren’t tied to spatial vectors anymore. Instead, they’re used to store the respective properties for two separate oscillations (one along the x-axis, one along the y-axis). Ultimately, these oscillations are manifested spatially when x and y are calculated in the show() method, mapping the oscillations onto the positions of the object.
Exercise 3.8
@@ -588,7 +588,7 @@
Exercise 3.9
Incorporate angular acceleration into the Oscillator object.
Waves
-
Imagine a single circle oscillating up and down according to the sine function. This is the equivalent to simulating a single point along the x-axis of a wave pattern. With a little panache and a for loop, you can animate the entire wave by placing a whole bunch of these oscillating circles next to one another (Figure 3.12).
+
Imagine a single circle oscillating up and down according to the sine function. This is the equivalent of simulating a single point along the x-axis of a wave pattern. With a little panache and a for loop, you can animate the entire wave by placing a whole bunch of these oscillating circles next to one another (Figure 3.12).
Figure 3.12: Animating the sine wave with oscillating circles
@@ -601,7 +601,7 @@
Waves
let amplitude = 100;
Then I’m going to loop through all the x values for each point on the wave. For now, I’ll put 24 pixels between adjacent x values. For each x, I’ll follow these three steps:
-
Calculate the y-position position according to amplitude and the sine of the angle.
+
Calculate the y-position according to amplitude and the sine of the angle.
Draw a circle at the (x, y) position.
Increment the angle by the delta angle.
@@ -625,7 +625,7 @@
Example 3.8: Static Wave
fill(127, 127);
for (let x = 0; x <= width; x += 24) {
- // Step 1: Calculate the y-position according to amplitude and sine of the angle.
+ // Step 1: Calculate the y-position according to the amplitude and sine of the angle.
let y = amplitude * sin(angle);
// Step 2: Draw a circle at the (x, y) position.
circle(x, y + height / 2, 48);
@@ -646,7 +646,7 @@
Example 3.8: Static Wave
- Figure 3.13: Three sine waves with varying deltaAngle values (0.05, 0.2, 0.6 from left to right)
+ Figure 3.13: Three sine waves with varying deltaAngle values (0.05, 0.2, and 0.6 from left to right)
Although I’m not precisely calculating the wavelength, you can see that the greater the change in angle, the shorter the wavelength. It’s also worth noting that as the wavelength decreases, it becomes more difficult to make out the wave since the vertical distance between the individual points increases.
Notice that everything in Example 3.8 happens inside setup(), so the result is static. The wave never changes or undulates. Adding motion is a bit tricky. Your first instinct might be to say, “Hey, no problem, I’ll just put the for loop inside the draw() function and let angle continue incrementing from one cycle to the next.”
@@ -669,7 +669,7 @@
Example 3.9: The Wave
function draw() {
background(255);
- // Each time through draw(), the angle that increments is set to startAngle.
+ // Each time through draw(), the angle that increments is set to startAngle.
let angle = startAngle;
for (let x = 0; x <= width; x += 24) {
@@ -724,7 +724,7 @@
Spring Forces
The extension is a measure of how much the spring has been stretched or compressed: as shown in Figure 3.15, it’s the difference between the current length of the spring and the spring’s resting length (its equilibrium state). Hooke’s law therefore says that if you pull on the bob a lot, the spring’s force will be strong, whereas if you pull on the bob a little, the force will be weak. Mathematically, the law is stated as follows:
F_{spring} = -kx
Here k is the spring constant. Its value scales the force, setting how elastic or rigid the spring is. Meanwhile, x is the extension, the current length minus the rest length.
-
Now remember, force is a vector, so you need to calculate both magnitude and direction. For the code, I‘ll start with the following three variables—two vectors for the anchor and bob positions, and one rest length:
+
Now remember, force is a vector, so you need to calculate both magnitude and direction. For the code, I’ll start with the following three variables—two vectors for the anchor and bob positions, and one rest length:
// Pick arbitrary values for the positions and rest length.
let anchor = createVector(0, 0);
let bob = createVector(0, 120);
@@ -736,7 +736,7 @@
Spring Forces
let dir = p5.Vector.sub(bob, anchor);
let currentLength = dir.mag();
let x = currentLength - restLength;
-
Now that I’ve sorted out the elements necessary for the magnitude of the force (–kx), I need to figure out the direction, a unit vector pointing in the direction of the force. The good news is that I already have this vector. Right? Just a moment ago I asked the question, “How I can calculate that distance?” and I answered, “How about the magnitude of a vector that points from the anchor to the bob?” Well, that vector describes the direction of the force!
+
Now that I’ve sorted out the elements necessary for the magnitude of the force (–kx), I need to figure out the direction, a unit vector pointing in the direction of the force. The good news is that I already have this vector. Right? Just a moment ago I asked the question, “How can I calculate that distance?” and I answered, “How about the magnitude of a vector that points from the anchor to the bob?” Well, that vector describes the direction of the force!
Figure 3.16 shows that if you stretch the spring beyond its rest length, a force should pull it back toward the anchor. And if the spring shrinks below its rest length, the force should push it away from the anchor. The Hooke’s law formula accounts for this reversal of direction with the –1.
@@ -751,7 +751,7 @@
Spring Forces
// Put it together: direction and magnitude!
force.setMag(-1 * k * x);
-
Now that I have the algorithm for computing the spring force, the question remains: what OOP structure should I use? This is one of those situations that has no one correct answer. Several possibilities exist, and the one I choose depends on my goals and personal coding style.
+
Now that I have the algorithm for computing the spring force, the question remains: What OOP structure should I use? This is one of those situations that has no one correct answer. Several possibilities exist, and the one I choose depends on my goals and personal coding style.
Since I’ve been working all along with a Mover class, I’ll stick with this same framework. I’ll think of the Mover class as the spring’s bob. The bob needs position, velocity, and acceleration vectors to move about the canvas. Perfect—I have those already! And perhaps the bob experiences a gravity force via the applyForce() method. This leaves just one more step, applying the spring force:
let bob;
@@ -778,7 +778,7 @@
Spring Forces
This allows me to write a lovely sketch as follows:
let bob;
-// Add a Spring object.
+// Add a Spring object.
let spring;
function setup() {
@@ -790,7 +790,7 @@
Spring Forces
let gravity = createVector(0, 1);
bob.applyForce(gravity);
- // This new method in the Spring class will take care of computing the force of the spring on the bob.
+ // This new method in the Spring class will take care of computing the force of the spring on the bob.
spring.connect(bob);
bob.update();
@@ -864,7 +864,7 @@
Example 3.10: A Spring Connection
Exercise 3.13
Before running to see the example online, take a look at this constrainLength method and see if you can fill in the blanks:
constrainLength(bob, minlen, maxlen) {
- //{!1} A vector pointing from Bob to Anchor
+ //{!1} A vector pointing from a bob to the anchor
let direction = p5.Vector.sub(bob.position, this.anchor);
let length = direction.mag();
@@ -894,17 +894,17 @@
The Pendulum
Figure 3.18: A pendulum with a pivot, arm, and bob
-
You might have noticed that in Example 3.10’s spring code, I never once used sine or cosine. Before you write off all this trigonometry stuff as a tangent, however, allow me to show an example of how it all fits together. Imagine a bob hanging from an anchor connected by a spring with a fully rigid connection that can neither be compressed nor extended. This idealized scenario describes a pendulum and provides an excellent opportunity to practice combining all that you’ve learned about forces and trigonometry.
+
You might have noticed that in Example 3.10’s spring code, I never once used sine or cosine. Before you write off all this trigonometry stuff as a tangent, however, allow me to show an example of how it all fits together. Imagine a bob hanging from an anchor connected by a spring with a fully rigid connection that can be neither compressed nor extended. This idealized scenario describes a pendulum and provides an excellent opportunity to practice combining all that you’ve learned about forces and trigonometry.
A pendulum is a bob suspended by an arm from a pivot (previously called the anchor in the spring). When the pendulum is at rest, it hangs straight down, as in Figure 3.18. If you lift up the pendulum at an angle from its resting state and then release it, however, it starts to swing back and forth, tracing the shape of an arc. A real-world pendulum would live in a 3D space, but I’m going to look at a simpler scenario: a pendulum in the 2D space of a p5.js canvas. Figure 3.19 shows the pendulum in a nonresting position and adds the forces at play: gravity and tension.
When the pendulum swings, its arm and bob are essentially rotating around the fixed point of the pivot. If no arm connected the bob and the pivot, the bob would simply fall to the ground under the influence of gravity. Obviously, that isn’t what happens. Instead, the fixed length of the arm creates the second force—tension. However, I’m not going to work with this scenario according to these forces, at least not in the way I approached the spring scenario.
Instead of using linear acceleration and velocity, I’m going to describe the motion of the pendulum in terms of angular acceleration and angular velocity, which refer to the change of the arm’s angle \theta relative to the pendulum’s resting position. I should first warn you, especially if you’re a seasoned physicist, that I’m going to conveniently ignore several important concepts here: conservation of energy, momentum, centripetal force, and more. This isn’t intended to be a comprehensive description of pendulum physics. My goal is to offer you an opportunity to practice your new skills in trigonometry and further explore the relationship between forces and angles through a concrete example.
-
- Figure 3.19: A pendulum showing \theta as angle relative to its resting position
+
+ Figure 3.19: A pendulum showing \theta as the angle relative to its resting position
-
To calculate the pendulum’s angular acceleration, I’m going to use Newton’s second law of motion but with a little trigonometric twist. Take a look back at Figure 3.19 and tilt your head so that the pendulum’s arm becomes the vertical axis. The force of gravity suddenly points askew, a little to the left—it’s at an angle with respect to your tilted head. If this is starting to hurt your neck, don’t worry. I’ll redraw the tilted figure and relabel the forces F_g for gravity and T for tension (Figure 3.20, left).
+
To calculate the pendulum’s angular acceleration, I’m going to use Newton’s second law of motion but with a little trigonometric twist. Take a look at Figure 3.19 and tilt your head so that the pendulum’s arm becomes the vertical axis. The force of gravity suddenly points askew, a little to the left—it’s at an angle with respect to your tilted head. If this is starting to hurt your neck, don’t worry. I’ll redraw the tilted figure and relabel the forces F_g for gravity and T for tension (Figure 3.20, left).
Figure 3.20: On the left, the pendulum is drawn rotated so that the arm is the y-axis. The right shows F_g zoomed in and divided into components F_{gx} and F_{gy}.
@@ -931,7 +931,7 @@
The Pendulum
\text{pendulum angular acceleration} = \text{acceleration due to gravity} \times \sin(\theta)
This is a good time for a reminder that the context here is creative coding and not pure physics. Yes, the acceleration due to gravity on Earth is 9.8 meters per second squared. But this number isn’t relevant in our world of pixels. Instead, I’ll use an arbitrary constant (called gravity) as a variable that scales the acceleration (incidentally, angular acceleration is usually written as \alpha so as to distinguish it from linear acceleration A):
\alpha = \text{gravity} \times \sin(\theta)
-
Before I put everything together, there’s another detail I neglected to mention. Or really, lots of little details. Think about the pendulum arm for a moment. Is it a metal rod? A string? A rubber band? How is it attached to the pivot point? How long is it? What’s its mass? Is it a windy day? I could continue to ask a lof of questions that would affect the simulation. I choose to live, however, in a fantasy world, one where the pendulum’s arm is an idealized rod that never bends and where the mass of the bob is concentrated in a single, infinitesimally small point.
+
Before I put everything together, there’s another detail I neglected to mention. Or really, lots of little details. Think about the pendulum arm for a moment. Is it a metal rod? A string? A rubber band? How is it attached to the pivot point? How long is it? What’s its mass? Is it a windy day? I could continue to ask a lot of questions that would affect the simulation. I choose to live, however, in a fantasy world, one where the pendulum’s arm is an idealized rod that never bends and where the mass of the bob is concentrated in a single, infinitesimally small point.
Even though I prefer not to worry myself with all these questions, a critical piece is still missing, related to the calculation of angular acceleration. To keep the derivation of the pendulum’s angular acceleration simple, I assumed that the length of the pendulum’s arm is 1. In reality, however, the length of the pendulum’s arm affects the acceleration of the pendulum because of the concepts of torque and moment of inertia.
Torque (or \tau) is a measure of the rotational force acting on an object. In the case of a pendulum, torque is proportional to both the mass of the bob and the length of the arm (M \times r). The moment of inertia (orI) of a pendulum is a measure of the amount of difficulty in rotating the pendulum around the pivot point. It’s proportional to the mass of the bob and the square of the length of the arm (Mr^2).
Remember Newton’s second law, F=M \times A? Well, it has a rotational counterpart, \tau = I \times \alpha. By rearranging the equation to solve for the angular acceleration \alpha, I get \alpha = \tau/I. Simplifying further, this becomes Mr/Mr^2 or 1/r. The angular acceleration doesn’t depend on the pendulum’s mass!
@@ -972,12 +972,12 @@
The Pendulum
}
-
- Figure 3.19: The bob position relative to the pivot in polar and Cartesian coordinates
+
+ Figure 3.22: The bob position relative to the pivot in polar and Cartesian coordinates
Note that the acceleration calculation now includes a multiplication by –1. When the pendulum is to the right of its resting position, the angle is positive, and so the sine of the angle is also positive. However, gravity should pull the bob back toward the resting position. Conversely, when the pendulum is to the left of its resting position, the angle is negative, and so its sine is negative too. In this case, the pulling force should be positive. Multiplying by –1 is necessary in both scenarios.
-
Next, I need a show() method to draw the pendulum on the canvas. But where exactly should I draw it? How do I calculate the x- and y-coordinates (Cartesian!) for both the pendulum’s pivot point (let’s call it pivot) and bob position (let’s call it bob)? This may be getting a little tiresome, but the answer, yet again, is trigonometry, as shown in Figure 3.19.
+
Next, I need a show() method to draw the pendulum on the canvas. But where exactly should I draw it? How do I calculate the x- and y-coordinates (Cartesian!) for both the pendulum’s pivot point (let’s call it pivot) and bob position (let’s call it bob)? This may be getting a little tiresome, but the answer, yet again, is trigonometry, as shown in Figure 3.22.
First, I’ll need to add a this.pivot property to the constructor to specify where to draw the pendulum on the canvas:
this.pivot = createVector(100, 10);
I know the bob should be a set distance away from the pivot, as determined by the arm length. That’s my variable r, which I’ll set now:
@@ -1006,7 +1006,7 @@
Example 3.11: Swinging Pendulum
function setup() {
createCanvas(640, 240);
- // Make a new Pendulum object with an origin position and arm length.
+ // Make a new Pendulum object with an origin position and arm length.
pendulum = new Pendulum(width / 2, 0, 175);
}
@@ -1019,7 +1019,7 @@
Example 3.11: Swinging Pendulum
class Pendulum {
constructor(x, y, r) {
- //{!8} Many, many variables keep track of the Pendulum’s various properties.
+ //{!8} Many, many variables keep track of the pendulum’s various properties.
this.pivot = createVector(x, y); // Position of pivot
this.bob = createVector(); // Position of bob
this.r = r; // Length of arm
@@ -1080,7 +1080,7 @@
Exercise 3.17
The Ecosystem Project
Take one of your creatures and incorporate oscillation into its motion. You can use the Oscillator class from Example 3.7 as a model. The Oscillator object, however, oscillates around a single point (the middle of the window). Try oscillating around a moving point.
-
In other words, design a creature that moves around the screen according to position, velocity, and acceleration. But that creature isn’t just a static shape; it’s an oscillating body. Consider tying the speed of oscillation to the speed of motion. Think of a butterfly’s flapping wings or the legs of an insect. Can you make it appear that the creature’s internal mechanics (oscillation) drive its locomotion? See the book’s website for an additional example combining attraction from Chapter 2 with oscillation.
+
In other words, design a creature that moves around the screen according to position, velocity, and acceleration. But that creature isn’t just a static shape; it’s an oscillating body. Consider tying the speed of oscillation to the speed of motion. Think of a butterfly’s flapping wings or the legs of an insect. Can you make it appear as though the creature’s internal mechanics (oscillation) drive its locomotion? See the book’s website for an additional example combining attraction from Chapter 2 with oscillation.
-
- Photo by Carl D. Anderson, public domain.
+
+ Photo by Carl D. Anderson
Positron
This early 20th-century photograph from a cloud chamber offers a glimpse into the world of subatomic particles, capturing the first ever observed positron. Cloud chambers are devices that make visible the paths of charged particles as they move through a supersaturated vapor.
@@ -25,7 +25,7 @@
Positron
In other words, the focus of this chapter is on how to keep track of a system of many elements. What those elements do and how they look is entirely up to you.
Why Particle Systems Matter
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 those situations.
-
In fact, just about every chapter from this one on include sketches incorporating 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).
+
In fact, just about every chapter from this one on includes sketches incorporating 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 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 want to take a more sophisticated, object-oriented approach. In addition to writing a class to describe a single particle, I 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:
// Ah, isn’t this main program so simple and lovely?
let system;
@@ -40,7 +40,7 @@
Why Particle Systems Matter
system.run();
}
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 ParticleSystem class, which 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 useful as you get to later chapters in this book.
-
Finally, working with particle systems is also an opportunity to tackle two other OOP 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 be only a system of one kind of particle.
+
Finally, working with particle systems is also an opportunity to tackle two other OOP 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 be a system of only one kind of particle.
A Single Particle
Before I can get rolling on coding the particle system, I need to write a class to describe a single particle. The good news: I’ve done this already! The 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 show() itself and update() its position:
class Particle {
@@ -63,7 +63,7 @@
A Single Particle
circle(this.position.x, this.position.y, 8);
}
}
-
This is about as simple as a particle can get. From here, I could take the particle in several directions. I could add the 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.
+
This is about as simple as a particle can get. From here, I could take the particle in several directions. I could add the 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: life span.
Some particle systems involve 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, 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 particles add up 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 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 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:
This example creates only one particle at a time for the sake of simplicity and testing. Each time the particle reaches the end of its lifespan, the 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.
+
This example creates only one particle at a time for the sake of simplicity and testing. Each time the particle reaches the end of its life span, the 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
Create a run() method in the Particle class that handles update(), show(), and applyForce(). What are the pros and cons of this approach?
@@ -179,7 +179,7 @@
Exercise 4.2
Add angular velocity (rotation) to the particle, and design a particle that isn’t a circle so its rotation is visible.
An Array of Particles
-
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 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 website.
+
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 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 website.
As I bring arrays into the picture, I’ll use a solution to Exercise 4.1 and assume a Particle.run() method that manages all of an individual particle’s functionality. While this approach also has some cons, 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;
// Start with an empty array.
@@ -199,7 +199,7 @@
An Array of Particles
}
}
The 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 a few other ways to do the same thing. This is something that I both love and hate about coding in JavaScript—it has so many 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 choices can be overwhelming and lead to a lot of confusion when learning.
-
Let’s take a ride on the loop-de-loop rollercoaster of choices for iterating over an array:
+
Let’s take a ride on the loop-de-loop roller coaster of choices for iterating over an array:
The traditional 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.
The 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.
@@ -237,7 +237,7 @@
An Array of Particles
let particle = particles[i];
particle.run();
if (particle.isDead()) {
- // Remove one particle at index i.
+ // Remove particle at index i.
particles.splice(i, 1);
}
}
@@ -245,7 +245,7 @@
An Array of Particles
for (let i = 0; i < particles.length; i++) {
let particle = particles[i];
particle.run();
- // Add a new particle to the list while iterating?
+ // Add a new particle to the list while iterating.
particles.push(new Particle(width / 2, 20));
}
This is a somewhat extreme scenario (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 length of the array increases. This will result in an infinite loop, as I can never increment past the size of the array!
@@ -332,7 +332,7 @@
Example 4.2: An Array of Particles
-
You might be wondering why, instead of checking each particle individually, I don’t just remove the oldest particle after a certain period of time (determined by checking the frameCount or array length). In this example, where the particles die in the same order in which they’re born, that approach would actually work. I could even use a different array method called shift(), which automatically removes the first element of an array. However, in many particle systems, other conditions or interactions may cause “younger” particles to die sooner than “older” particles. Checking isDead() in combination withsplice() is a nice, comprehensive solution that offers flexibility in managing particles across a variety of scenarios.
+
You might be wondering why, instead of checking each particle individually, I don’t just remove the oldest particle after a certain period of time (determined by checking the frameCount or array length). In this example, where the particles die in the same order in which they’re born, that approach would actually work. I could even use a different array method called shift(), which automatically removes the first element of an array. However, in many particle systems, other conditions or interactions may cause “younger” particles to die sooner than “older” particles. Checking isDead() in combination with splice() is a nice, comprehensive solution that offers flexibility in managing particles across a variety of scenarios.
A Particle Emitter
I’ve conquered the array and used it to manage a list of Particle objects, with the ability to add and delete particles at will. I could stop here and rest on my laurels, but I can and should take an additional step: writing a class describing the list of Particle objects itself. At the start of this chapter, I used a speculative class name ParticleSystem to represent the overall collection of particles. However, a more fitting term for the functionality of emitting particles is Emitter, which I’ll use from now on.
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.
@@ -451,7 +451,7 @@
A System of Emitters
You click the mouse and generate a particle system at the mouse’s position (Figure 4.3).
- Figure 4.3 Adding one particle system
+ Figure 4.3: Adding a particle system
You keep clicking the mouse. Each time, another particle system springs up where you clicked (Figure 4.4).
@@ -492,7 +492,7 @@
Example 4.4: A System of Systems
Then, in 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);
- // No emitters are removed, so a for . . . of loop can work here!
+ // No emitters are removed, so a for...of loop can work here!
for (let emitter of emitters) {
emitter.run();
emitter.addParticle();
@@ -510,7 +510,7 @@
Exercise 4.6
Inheritance and Polymorphism
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 OOP principles, 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 (Chapter 22) dedicated to them. Still, perhaps you’ve learned about inheritance and polymorphism only 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 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, exploding onto the screen all at once.
+
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 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, exploding onto the screen all at once.
What you have is clearly a particle system: a collection of individual pieces (particles) of confetti. You might be able to cleverly redesign the 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? It could become very messy to have 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 {
@@ -540,7 +540,7 @@
Inheritance and Polymorphism
}
}
}
-
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 copying and pasting a lot of code between the confetti classes?
+
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 copying and pasting a lot of code between the confetti classes?
Yes, you probably will. Even though the kinds of particles are different enough to merit breaking them into separate classes, they’ll likely share a ton of code. For example, they’ll all have vectors to keep track of position, velocity, and acceleration; an update() function that implements the motion algorithm; and more.
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. You might also be wondering whether adding all those types of confetti to a single particles array actually works. After all, I don’t typically mix different kinds of objects in one array, as it could get confusing. How will the code in the Emitter class know which particle is which kind of confetti? Wouldn’t separate arrays be easier to manage?
constructor() {
@@ -602,15 +602,15 @@
Inheritance Basics
Figure 4.2: An inheritance tree
Here’s how the syntax of inheritance works:
-
//{!1} The Animal class is the parent (or superclass).
+
//{!1} The Animal class is the parent (or superclass).
class Animal {
constructor() {
- // Dog and Cat will inherit the variable age.
+ // Dog and Cat inherit the variable age.
this.age = 0;
}
- //{!7} Dog and Cat inherit the functions eat() and sleep().
+ //{!7} Dog and Cat inherit the functions eat() and sleep().
eat() {
print("Yum!");
}
@@ -620,7 +620,7 @@
Inheritance Basics
}
}
-//{!1} The Dog class is the child (or subclass), indicated by the code `extends Animal`.
+//{!1} The Dog class is the child (or subclass), indicated by the code extends Animal.
class Dog extends Animal {
constructor() {
// super() executes code found in the parent class.
@@ -746,7 +746,7 @@
Polymorphism Basics
for (let animal of kingdom) {
animal.eat();
}
-
This is polymorphism (from the Greek polymorphos meaning “many forms”) in action. Although all the animals are grouped together in an array and processed in a single for loop, JavaScript can identify their true types and invoke the appropriate eat() method for each one. It’s that simple!
+
This is polymorphism (from the Greek polymorphos,meaning “many forms”) in action. Although all the animals are grouped together in an array and processed in a single for loop, JavaScript can identify their true types and invoke the appropriate eat() method for each one. It’s that simple!
Particles with Inheritance and Polymorphism
Now that I’ve covered the theory and syntax behind inheritance and polymorphism, I’m ready to write a working example of them in p5.js, based on my Particle class. First, take another look at a basic Particle implementation, adapted from Example 4.1:
class Particle {
@@ -786,10 +786,10 @@
Particles with Inheritance
class Confetti extends Particle {
constructor(x, y) {
super(x, y);
- // I could add variables for only Confetti here.
+ // I could add variables for only Confetti here.
}
- /* There’s no code here because methods like update() are inherited from the parent. */
+ /* There's no code here because methods like update() are inherited from the parent. */
// Override the show() method.
show() {
@@ -816,7 +816,7 @@
Particles with Inheritance
square(0, 0, 12);
pop();
}
-
The choice of 4\pi might seem arbitrary, but it’s intentional—two full rotations adds a significant degree of spin to the particle compared to just one.
+
The choice of 4\pi might seem arbitrary, but it’s intentional—two full rotations add a significant degree of spin to the particle compared to just one.
Exercise 4.7
Instead of using map() to calculate angle, try modeling angular velocity and acceleration.
@@ -862,7 +862,7 @@
Exercise 4.8
Create a particle system with more than two kinds of particles. Try varying the behavior of the particles in addition to the design.
Particle Systems with Forces
-
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 theapplyForce() function in my Particle class, I took a couple of 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):
+
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 applyForce() function in my Particle class, I took a couple of 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) {
@@ -882,7 +882,7 @@
Particle Systems with Forces
this.acceleration.add(f);
}
-
Now that the Particle class is complete, I have an 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 create and apply a gravity force in the run() method of each particle:
+
Now that the Particle class is complete, I have an 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 create and apply a gravity force in the run() method of each particle:
run() {
//{!2} Create a hardcoded vector and apply it as a force.
let gravity = createVector(0, 0.05);
@@ -954,14 +954,14 @@
Example 4.6: A Particle System
}
applyForce(force) {
- // Use a for...of loop to apply the force to all particles.
+ // Use a for...of loop to apply the force to all particles.
for (let particle of this.particles) {
particle.applyForce(force);
}
}
run() {
- // Can’t use the enhanced loop because you’re checking for particles to delete.
+ // You can’t use the enhanced loop because you’re checking for particles to delete.
for (let i = this.particles.length - 1; i >= 0; i--) {
let particle = this.particles[i];
particle.run();
@@ -971,26 +971,26 @@
Example 4.6: A Particle System
}
}
}
-
While this example demonstrates a hardcoded 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 kicked in only under certain conditions or at specific moments? A lot of room here remains for creativity and interactivity in the way you design your particle systems!
+
While this example demonstrates a hardcoded 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? A lot of room remains here for creativity and interactivity in the way you design your particle systems!
Particle Systems with Repellers
-
What if I want to take my code one step further and add a 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).
+
What if I want to take my code one step further and add a Repeller object—the inverse of the Attractor object covered in Chapter 2—that pushes away any particles 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).
Figure 4.3: A gravity force where vectors are all identical (left) and a repeller force where all vectors point in different directions (right)
-
To incorporate a new Repeller object into a particle system sketch. I’m going to need two major additions to the code:
+
To incorporate a new Repeller object into a particle system sketch, I’m going to need two major additions to the code:
A Repeller object (declared, initialized, and displayed)
A method that passes the Repeller object into the particle emitter so that the repeller can apply a force to each particle object
let emitter;
-// New thing: we declare a Repeller object.
+// New thing: we declare a Repeller object.
let repeller;
function setup() {
createCanvas(640, 240);
emitter = new Emitter(width / 2, 50);
- // New thing: we initialize a Repeller object.
+ // New thing: we initialize a Repeller object.
repeller = new Repeller(width / 2 - 20, height / 2);
}
@@ -1005,14 +1005,14 @@
Particle Systems with Repellers
emitter.applyRepeller(repeller);
emitter.run();
- // New thing: display the Repeller object.
+ // New thing: display the Repeller object.
repeller.show();
}
-
Creating a Repeller object is easy; it’s a duplicate of the Attractor class from Chapter 2, Example 2.6. Since this chapter doesn’t involve the concept of mass, I’ll add a property called power to the Repeller. This property can be used to adjust the strength of the repellent force:
+
Creating a Repeller object is easy; it’s a duplicate of the Attractor class from Example 2.6. Since this chapter doesn’t involve the concept of mass, I’ll add a property called power to the Repeller. This property can be used to adjust the strength of the repellent force:
class Repeller {
constructor(x, y) {
- // A Repeller doesn’t move, so you just need position.
+ // A Repeller doesn’t move, so you just need position.
this.position = createVector(x, y);
// Instead of mass, use the concept of power to scale the repellent force.
this.power = 150;
@@ -1050,8 +1050,8 @@
Particle Systems with Repellers
-
These nearly identical methods have only two differences. I mentioned one of them before: the argument to 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.
+
These nearly identical methods have only two differences. I mentioned one of them before: the argument to 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(), it is calculated as 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) {
// Step 1: Get the force direction.
let force = p5.Vector.sub(this.position, particle.position);
@@ -1110,7 +1110,7 @@
Example 4.7: A Particle Sy
}
applyForce(force) {
- // Apply a force as a p5.Vector.
+ // Apply a force as a p5.Vector.
for (let particle of this.particles) {
particle.applyForce(force);
}
@@ -1208,7 +1208,7 @@
Example 4.8: An Image-Textu
tint(255, this.lifespan);
image(img, this.position.x, this.position.y);
}
-
This smoke example is also a nice excuse to revisit the Gaussian distributions from “A Normal Distribution of Random Numbers” on page 13. 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 randomGaussian() function, the particle velocities can be initialized as follows:
+
This smoke example is also a nice excuse to revisit the Gaussian distributions from “A Normal Distribution of Random Numbers” on page 13. Instead of launching the particles in a purely random direction, which produces a fountain-like effect, the result will appear more smokelike if the initial velocity vectors cluster mostly around a mean value, with a lower probability of outlying velocities. Using the randomGaussian() function, the particle velocities can be initialized as follows:
let vx = randomGaussian(0, 0.3);
let vy = randomGaussian(-1, 0.3);
this.velocity = createVector(vx, vy);
@@ -1262,7 +1262,7 @@
Example 4.9: Additive Blending
emitter.addParticle();
}
}
-
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, an additional rendering option called WEBGL is avaialable. 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().
+
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, an additional rendering option called WEBGL is available. 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() {
// Enable the WebGL renderer.
createCanvas(640, 240, WEBGL);
@@ -1284,7 +1284,7 @@
Exercise 4.14
The Ecosystem Project
-
Take your creature from step 3 and build a system of creatures. How do they interact with one another? Can you use inheritance and polymorphism to create a variety of creatures, derived from the same codebase? Develop a methodology for the way they compete for resources (for example, food). Can you track a creature’s health much like a particle’s lifespan, removing creatures when appropriate? What rules can you incorporate to control the way creatures are born into the system?
+
Take your creature from Chapter 3 and build a system of creatures. How do they interact with one another? Can you use inheritance and polymorphism to create a variety of creatures, derived from the same codebase? Develop a methodology for the way they compete for resources (for example, food). Can you track a creature’s health much like a particle’s life span, removing creatures when appropriate? What rules can you incorporate to control the way creatures are born into the system?
Also, you might consider using a particle system in the design of a creature. What happens if an emitter is tied to the creature’s position?
-
- Photo courtesy of NOAA photo library, public domain.
+
+ Photo courtesy of the US National Oceanic and Atmospheric Administration
The Mo’i Fish
Six-finger threadfins (Polydactylus sexfilis), also known as fish of kings, or mo’i, in Hawaiian, are shown swimming in a shoal. The mo’i fish held a special status for Hawaiian royalty and were raised in dedicated ponds to ensure their population growth and prevent their extinction. The fish display a delicate and coordinated dance in their collective movement, with each individual mo’i subtly influencing and being influenced by its neighboring fish.
@@ -32,7 +32,7 @@
Forces from Within
I could start my exploration of autonomous agents in many places. Artificial simulations of ant and termite colonies are fantastic demonstrations of systems of agents, for example. For more on this topic, I encourage you to read Turtles, Termites, and Traffic Jams by Mitchel Resnick (Bradford Books, 1997). 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 I’ll return to the book’s ever-changing hero class—once Walker, then Mover, then Particle—and give it yet another incarnation.
Vehicles and Steering
-
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. Later, in his 1999 paper “Steering Behaviors for Autonomous Characters,” Reynolds uses the word vehicle to describe his autonomous agents. I’ll follow suit, calling my autonomous agent class Vehicle.
+
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. Later, in his 1999 paper “Steering Behaviors for Autonomous Characters,” Reynolds uses the word vehicle to describe his autonomous agents. I’ll follow suit, calling my autonomous agent class Vehicle:
class Vehicle {
@@ -47,18 +47,18 @@
Vehicles and Steering
Like the Mover and Particle classes before it, the Vehicle class’s motion is controlled through its position, velocity, and acceleration vectors. This will make the steering behaviors of a single autonomous agent straightforward to implement. Yet by building a system of multiple vehicles 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 or swarming behavior, which I’ll demonstrate in Example 5.9.
Why Vehicles?
-
In his 1986 book Vehicles: Experiments in Synthetic Psychology (Bradford Books), Italian neuroscientist and cyberneticist Valentino Braitenberg describes a series of hypothetical vehicles with simple internal structures, writing, “This is an exercise in fictional science, or science fiction, if you like that better.” Braitenberg argues that his extraordinarily simple mechanical vehicles manifest behaviors such as fear, aggression, love, foresight, and optimism. Reynolds took his inspiration from Braitenberg, and I’ll take mine from Reynolds.
+
In his book Vehicles: Experiments in Synthetic Psychology (Bradford Books, 1986), Italian neuroscientist and cyberneticist Valentino Braitenberg describes a series of hypothetical vehicles with simple internal structures, writing, “This is an exercise in fictional science, or science fiction, if you like that better.” Braitenberg argues that his extraordinarily simple mechanical vehicles manifest behaviors such as fear, aggression, love, foresight, and optimism. Reynolds took his inspiration from Braitenberg, and I’ll take mine from Reynolds.
Reynolds describes the motion of idealized vehicles—idealized because he wasn’t concerned with their actual engineering, but rather started with the assumption that they work and respond to the rules defined. These vehicles have three layers:
-
Action selection: A vehicle has a goal (or goals) and can choose an action (or a combination of actions) based on that goal. This is essentially where I left off the discussion of autonomous agents. The vehicle takes a look at its environment and selects an action based on a desire: “I see a zombie marching toward me. Since I don’t want my brains to be eaten, I’m going to flee from the zombie.” The goal is to keep one’s brains, and the action is to flee. Reynolds’s paper describes many goals and associated actions, such as seeking a target, avoiding an obstacle, and following a path. In a moment, I’ll start building out these examples with p5.js code.
+
Action selection: A vehicle has a goal (or goals) and can choose an action (or a combination of actions) based on that goal. This is essentially where I left off in the discussion of autonomous agents. The vehicle takes a look at its environment and selects an action based on a desire: “I see a zombie marching toward me. Since I don’t want my brains to be eaten, I’m going to flee from the zombie.” The goal is to keep one’s brains, and the action is to flee. Reynolds’s paper describes many goals and associated actions, such as seeking a target, avoiding an obstacle, and following a path. In a moment, I’ll start building out these examples with p5.js code.
Steering: Once an action has been selected, the vehicle has to calculate its next move. That next move will be a force—more specifically, a steering force. Luckily, Reynolds has developed a simple steering force formula that I’ll use throughout the examples in this chapter: steering force = desired velocity – current velocity. I’ll get into the details of this formula and why it works so effectively in the next section.
Locomotion: For the most part, I’m going to ignore this third layer. In the case of fleeing from zombies, the locomotion could be described as “left foot, right foot, left foot, right foot, as fast as you can.” In a canvas, however, a rectangle, circle, or triangle’s actual movement across a window is irrelevant, given that the motion is all an illusion in the first place. This isn’t to say that you should ignore locomotion entirely, however. You’ll find great value in thinking about the locomotive design of your vehicle and how you choose to animate it. The examples in this chapter will remain visually bare; a good exercise would be to elaborate on the animation style. For example, could you add spinning wheels, oscillating paddles, or shuffling legs?
Ultimately, the most important layer for you to consider is the first one, action selection. What are the elements of your system, and what are their goals? In this chapter, I’m going to cover a series of steering behaviors (that is, actions): seeking, fleeing, following a path, following a flow field, flocking with your neighbors, and so on. As I’ve said in other chapters, however, the point isn’t that you should use these exact behaviors in all your projects. Rather, the point is to show you how to model a steering behavior—any steering behavior—in code, and to provide a foundation for designing and developing your own vehicles with new and exciting goals and behaviors.
-
What’s more, even though the examples in this chapter are highly literal (follow that pixel!), you should allow yourself to think more abstractly (like Braitenberg). What would it mean for your vehicle to have “love” as its goal or “fear” as its driving force? Finally (and I’ll address this in “Combining Behaviors” on page XX), you won’t get very far by developing simulations with only one action. Yes, the first example’s action will be to seek a target. But being creative—by making these steering behaviors your own—it will all come down to mixing and matching multiple actions within the same vehicle. View the coming examples not as singular behaviors to be emulated, but as pieces of a larger puzzle that you’ll eventually assemble.
+
What’s more, even though the examples in this chapter are highly literal (follow that pixel!), you should allow yourself to think more abstractly (like Braitenberg). What would it mean for your vehicle to have “love” as its goal or “fear” as its driving force? Finally (and I’ll address this in “Combining Behaviors” on page XX), you won’t get very far by developing simulations with only one action. Yes, the first example’s action will be to seek a target. But by being creative—by making these steering behaviors your own—it will all come down to mixing and matching multiple actions within the same vehicle. View the coming examples not as singular behaviors to be emulated, but as pieces of a larger puzzle that you’ll eventually assemble.
The Steering Force
-
What exactly is a steering force? To answer, consider the following scenario: a vehicle with a current velocity is seeking a target. For fun, let’s think of the vehicle as a bug-like creature who desires to savor a delicious strawberry, as in Figure 5.1.
+
What exactly is a steering force? To answer, consider the following scenario: a vehicle with a current velocity is seeking a target. For fun, let’s think of the vehicle as a bug-like creature that desires to savor a delicious strawberry, as in Figure 5.1.
Figure 5.1: A vehicle with a velocity and a target
@@ -75,7 +75,7 @@
The Steering Force
Assuming a p5.Vector called target defining the target’s position, I then have this:
let desired = p5.Vector.sub(target, position);
-
There’s more to the story, however. What if the canvas is high-resolution and the target is thousands of pixels away? Sure, the vehicle might desire to teleport itself instantly to the target position with a massive velocity, but this won’t make for an effective animation. I’ll restate the desire as follows:
+
There’s more to the story, however. What if it is a high-resolution canvas and the target is thousands of pixels away? Sure, the vehicle might desire to teleport itself instantly to the target position with a massive velocity, but this won’t make for an effective animation. I’ll restate the desire as follows:
The vehicle desires to move toward the target at the maximum possible speed.
In other words, the desired vector should point from the vehicle’s current position to the target position, with a magnitude equal to the maximum speed of the vehicle, as shown in Figure 5.3.
@@ -118,7 +118,7 @@
The Steering Force
Figure 5.4: The vehicle applies a steering force equal to its desired velocity minus its current velocity.
This force looks quite different from gravitational attraction. Remember one of the principles of autonomous agents: an autonomous agent has a limited ability to perceive its environment, including its own state. Here’s that ability, subtly but powerfully embedded into Reynolds’s steering formula. In the case of gravitational attraction, the force pulling an object toward another is the same regardless of how that object is moving. But here, the vehicle is actively aware of its own velocity, and its steering force compensates accordingly. This adds a lifelike quality to the simulation, as the way in which the vehicle moves toward the target depends on its own understanding of its current motion.
-
In all this excitement, I’ve missed one last step. What sort of vehicle is this? Is it a super sleek race car with amazing handling? Or a large city bus that needs a lot of advance notice to turn? A graceful panda or a lumbering elephant? The example code, as it stands, has no feature to account for this variation in steering ability. For that, I need to limit the magnitude of the steering force. I’ll call this limit the maximum force (or maxforce for short):
+
In all this excitement, I’ve missed one last step. What sort of vehicle is this? Is it a super-sleek race car with amazing handling? Or a large city bus that needs a lot of advance notice to turn? A graceful panda or a lumbering elephant? The example code, as it stands, has no feature to account for this variation in steering ability. For that, I need to limit the magnitude of the steering force. I’ll call this limit the maximum force (or maxforce for short):
class Vehicle {
@@ -165,7 +165,7 @@
Example 5.1: Seeking a Target
this.acceleration = createVector(0, 0);
//{!1} Additional variable for size
this.r = 6.0;
- //{!2} Arbitrary values for maxspeed and force; try varying these!
+ //{!2} Arbitrary values for max speed and force; try varying these!
this.maxforce = 8;
this.maxspeed = 0.2;
}
@@ -244,7 +244,6 @@
The Arrive Behavior
The vehicle is so gosh darn excited about getting to the target that it doesn’t bother to make any intelligent decisions about its speed. No matter the distance to the target, it always wants to go as fast as possible. When the vehicle is very close, it will therefore end up overshooting the target (see Figure 5.6, top).
In some cases, this is the desired behavior. (Consider a puppy going after its favorite toy: it’s not slowing down, no matter how close it gets!) However, in many other cases (a car pulling into a parking spot, a bee landing on a flower), the vehicle’s thought process needs to consider its speed relative to the distance from its target (see Figure 5.6, bottom). For example:
-
I’m very far away. I want to go as fast as possible toward the target.
I’m very far away. I want to go as fast as possible toward the target.
I’m somewhat far away. I still want to go as fast as possible toward the target.
I’m getting close. I want to go more slowly toward the target.
@@ -306,7 +305,7 @@
Example 5.2: Arriving at a Target
this.applyForce(steer);
}
The arrive behavior is a great demonstration of an autonomous agent’s perception of the environment—including its own state. This model differs from the inanimate forces of Chapter 2: a celestial body attracted to another body doesn’t know it is experiencing gravity, whereas a cheetah chasing its prey knows it’s chasing.
-
The key is in the way the forces are calculated. For instance, in the gravitational attraction sketch (Example 2.6), the force always points directly from the object to the target—the exact direction of the desired velocity. Here, by contrast, the vehicle perceives its distance to the target and adjusts its desired speed accordingly, slowing as it gets closer. The force on the vehicle itself is therefore based not just on the desired velocity, but on the desired velocity relative to its current velocity. The vehicle accounts for its own state as part of its assessment of the environment.
+
The key is in the way the forces are calculated. For instance, in the gravitational attraction sketch (Example 2.6), the force always points directly from the object to the target—the exact direction of the desired velocity. Here, by contrast, the vehicle perceives its distance to the target and adjusts its desired speed accordingly, slowing as it gets closer. The force on the vehicle itself is therefore based not just on the desired velocity but also on the desired velocity relative to its current velocity. The vehicle accounts for its own state as part of its assessment of the environment.
Put another way, the magic of Reynolds’s desired minus velocity equation is that it essentially makes the steering force a manifestation of the current velocity’s error: “I’m supposed to be going this fast in this direction, but I’m actually going this fast in another direction. My error is the difference between where I want to go and where I’m currently going.” Sometimes this can lead to seemingly unexpected results, as in Figure 5.10.
@@ -324,8 +323,8 @@
Your Own Behaviors
Figure 5.11: The wandering steering behavior is calculated as seeking a target that moves randomly along the perimeter of a circle projected in front of the vehicle.
-
First, the vehicle predicts its future position as a fixed distance in front of it (in the direction of its current velocity). Then it draws a circle with radius r centered around that position and picks a random point along the circumference of the circle. That point, which moves randomly around the circle each frame of animation, is the vehicle’s target, so its desired velocity points in that direction.
-
Sounds absurd, right? Or, at the very least, a bit arbitrary. In fact, this is a clever and thoughtful solution—it uses randomness to drive a vehicle’s steering, but constrains that randomness along the path of a circle to keep the vehicle’s movement from appearing jittery, and, well, totally random.
+
First, the vehicle predicts its future position as a fixed distance in front of it (in the direction of its current velocity). Then it draws a circle with radius r centered on that position and picks a random point along the circumference of the circle. That point, which moves randomly around the circle for each frame of animation, is the vehicle’s target, so its desired velocity points in that direction.
+
Sounds absurd, right? Or, at the very least, a bit arbitrary. In fact, this is a clever and thoughtful solution—it uses randomness to drive a vehicle’s steering, but constrains that randomness along the path of a circle to keep the vehicle’s movement from appearing jittery and, well, totally random.
The seemingly random and arbitrary nature of this solution should drive home the point I’m trying to make: these are made-up behaviors, even if they’re inspired by real-life motion. You can just as easily concoct another elaborate scenario to compute a desired velocity. And you should!
Exercise 5.4
@@ -402,7 +401,7 @@
Flow Fields
this.field[i] = new Array(this.rows);
}
}
-
How should I fill in the missing values? Let’s say I have a canvas that’s 200 pixels wide by 200 pixels high. In theory, I could make a flow field that has a vector for every single pixel, meaning 40,000 vectors total (200 \times 200). This isn’t a terribly unreasonable number, but in this context, a vector per pixel is overkill. I can easily get by with, say, one vector every 10 pixels (20 \times 20 = 400). My resolution variable sets the size of each cell in pixels. Then I can calculate the number of columns and rows based on the size of the canvas divided by the resolution:
+
How should I fill in the missing values? Let’s say I have a canvas that’s 200 pixels wide by 200 pixels high. In theory, I could make a flow field that has a vector for every single pixel, meaning 40,000 vectors total (200 \times 200). This isn’t a terribly unreasonable number, but in this context, one vector per pixel is overkill. I can easily get by with, say, one vector every 10 pixels (20 \times 20 = 400). My resolution variable sets the size of each cell in pixels. Then I can calculate the number of columns and rows based on the size of the canvas divided by the resolution:
constructor() {
this.resolution = 10;
// The total number of columns equals the width divided by the resolution.
@@ -421,8 +420,8 @@
Flow Fields
Figure 5.14: A flow field with all vectors pointing to the right
For that, I can just set each vector to (1, 0).
-
//{!2} Use a nested loop to hit every column.
-// and every row of the flow field
+
//{!2} Use a nested loop to hit every column
+// and every row of the flow field.
for (let i = 0; i < this.cols; i++) {
for (let j = 0; j < this.rows; j++) {
//{!1} Arbitrary decision to make each vector point to the right
@@ -446,7 +445,7 @@
Flow Fields
Figure 5.16: A flow field calculated with Perlin noise
-
Just map each noise value to an angle from 0 to 2\pi, and create a vector from that angle:
+
Just map each noise value to an angle from 0 to 2\pi and create a vector from that angle:
let xoff = 0;
for (let i = 0; i < this.cols; i++) {
let yoff = 0;
@@ -546,23 +545,23 @@
Example 5.4: Flow-Field Following
Notice that lookup() is a method of the FlowField class, rather than of Vehicle. While you certainly could place lookup() within the Vehicle class instead, from my perspective, placing it in FlowField aligns best with the OOP principle of encapsulation. The lookup task, which retrieves a vector based on a position from the flow field, is inherently tied to the data of the FlowField object.
-
You may also notice some familiar elements from Chapter 4, such as the use of an array of vehicles. Although the vehicles here operate independently, this is a great first step toward thinking about the group behaviors that I’ll introduce later this chapter.
+
You may also notice some familiar elements from Chapter 4, such as the use of an array of vehicles. Although the vehicles here operate independently, this is a great first step toward thinking about the group behaviors that I’ll introduce later in this chapter.
Exercise 5.7
-
Adapt the flow-field example so the vectors change over time. (Hint: Try using the third dimension of Perlin noise!).
+
Adapt the flow-field example so the vectors change over time. (Hint: Try using the third dimension of Perlin noise!)
Exercise 5.8
Can you create a flow field from an image? For example, try having the vectors point from dark to light colors (or vice versa).
Path Following
-
The next steering behavior formulated by Reynolds I’d like to explore is path following. But let me quickly clarify something first: the behavior here is path following, not pathfinding. Pathfinding refers to an algorithm that solves for the shortest distance between two points, often in a maze. With path following, a predefined route, or path, already exists, and the vehicle simply tries to follow it.
+
The next steering behavior formulated by Reynolds that I’d like to explore is path following. But let me quickly clarify something first: the behavior here is path following, not path finding. Pathfinding refers to an algorithm that solves for the shortest distance between two points, often in a maze. With path following, a predefined route, or path, already exists, and the vehicle simply tries to follow it.
In this section, I will work through the algorithm, including the corresponding mathematics and code. However, before doing so, it’s important to cover a key concept in vector math that I skipped over in Chapter 1: the dot product. I haven’t needed it yet, but it’s necessary here and likely will prove quite useful for you beyond just this example.
The Dot Product
Remember all the vector math covered in Chapter 1? Add, subtract, multiply, and divide? Figure 5.17 has a recap of some of these operations.
-
- Figure 5.17: Adding vectors, and multiplying a vector by a scalar
+
+ Figure 5.17: Adding vectors and multiplying a vector by a scalar
Notice that multiplication involves multiplying a vector by a scalar value. This makes sense; when you want a vector to be twice as large (but facing the same direction), multiply it by 2. When you want it to be half the size, multiply it by 0.5. However, several other multiplication-like operations involve a pair of vectors that are useful in certain scenarios—the dot product, the cross product, and something called the Hadamard product. For now, I’m going to focus on the dot product.
Assume vectors \vec{A} and \vec{B}:
@@ -579,7 +578,7 @@
The Dot Product
let a = createVector(-3, 5);
let b = createVector(10, 1);
-// The p5.Vector class includes a function to calculate the dot product.
+// The p5.Vector class includes a function to calculate the dot product.
let n = a.dot(b);
If you look in the guts of the p5.Vector source code, you’ll find a pretty simple implementation of this dot() method:
Sure I could have told you about this angleBetween()method to begin with, but understanding the dot product in detail will better prepare you for the upcoming path-following examples and help you see how the dot product fits into a concept called scalar projection.
+
Sure, I could have told you about this angleBetween() method to begin with, but understanding the dot product in detail will better prepare you for the upcoming path-following examples and help you see how the dot product fits into a concept called scalar projection.
Exercise 5.9
Create a sketch that shows the angle between two vectors.
@@ -629,7 +628,7 @@
Exercise 5.9
-
There are a couple things to note about dot products:
+
There are a couple of things to note about dot products:
If two vectors (\vec{A} and \vec{B}) are orthogonal (that is, perpendicular), their dot product (\vec{A}\cdot\vec{B}) is equal to 0.
If two vectors are unit vectors, their dot product is equal to the cosine of the angle between them. In other words, \vec{A}\cdot\vec{B}=\cos(\theta) if \vec{A} and \vec{B} are of length 1.
@@ -684,7 +683,7 @@
Example 5.5: Creating a Path Object
Figure 5.22: Adding a vehicle moving off and away from the path
-
The first step is to is predict (assuming a constant velocity) where that vehicle will be in the future:
+
The first step is to predict (assuming a constant velocity) where that vehicle will be in the future:
// Start by making a copy of the velocity.
let future = vel.copy();
@@ -739,22 +738,22 @@
Example 5.5: Creating a Path Object
b.normalize();
b.setMag(a.dot(b));
let normalPoint = p5.Vector.add(path.start, b);
-
This process of scaling \vec{B} according to the normal point is commonly known as scalar projection. We say that ||\vec{A}||\times\cos(\theta)is the scalar projection of \vec{A} onto \vec{B}, as in Figure 5.25.
+
This process of scaling \vec{B} according to the normal point is commonly known as scalar projection. We say that ||\vec{A}||\times\cos(\theta)is the scalar projection of \vec{A} onto \vec{B}, as in Figure 5.25.
Figure 5.25: The scalar projection of \vec{A} onto \vec{B} is equal to ||\vec{A}||\times\cos(\theta).
Once I have the normal point along the path, the next step is to decide whether and how the vehicle should steer toward the path. Reynolds’s algorithm states that the vehicle should steer toward the path only if it’s in danger of straying beyond the path—that is, if the distance between the normal point and the predicted future position is greater than the path’s radius. This is illustrated in Figure 5.26.
-
- Figure 5.26: A vehicle with a future position on the path (top) and one that’s outside the path (bottom).
+
+ Figure 5.26: A vehicle with a future position on the path (top) and one that’s outside the path (bottom)
-
I can encode that logic with a simple if statement, and use my earlier seek() method to steer the vehicle when necessary:
+
I can encode that logic with a simple if statement and use my earlier seek() method to steer the vehicle when necessary:
let distance = p5.Vector.dist(future, normalPoint);
// If the vehicle is outside the path, seek the target.
if (distance > path.radius) {
- //{!1} The desired velocity and steering force can use the seek method created in Example 5.1.
+ //{!1} The desired velocity and steering force can use the seek() method created in Example 5.1.
this.seek(target);
}
But what’s the target that the path follower is seeking? Reynolds’s algorithm involves picking a point ahead of the normal on the path. Since I know the vector that defines the path (\vec{B}), I can implement this point ahead by adding a vector that points in \vec{B}’s direction to the vector representing the normal point, as in Figure 5.27.
@@ -790,7 +789,7 @@
Example 5.6: Simple Path Following
if statement to find the target only if it’s off the path.)
let b = p5.Vector.sub(path.end, path.start);
b.setMag(25);
let target = p5.Vector.add(normalPoint, b);
@@ -809,7 +808,7 @@
Example 5.6: Simple Path Following
-
Notice that instead of using all that dot-product and scalar projection code to find the normal point, I call a function: getNormalPoint(). In cases like this, it’s useful to break out the code that performs a specific task (finding a normal point) into a function that can be called when required. The function takes three vector arguments (see Figure 5.28): the first defines a point p in Cartesian space (the vehicle’s future position), and the second and third define a line segment between two points a and b (the path).
+
Notice that instead of using all that dot-product and scalar projection code to find the normal point, I call the getNormalPoint() function. In cases like this, it’s useful to break out the code that performs a specific task (finding a normal point) into a function that can be called when required. The function takes three vector arguments (see Figure 5.28): the first defines a point p in Cartesian space (the vehicle’s future position), and the second and third define a line segment between two points a and b (the path).
getNormalPoint(position, a, b) {
// Vector that points from a to position
let vectorA = p5.Vector.sub(position, a);
@@ -855,7 +854,7 @@
Example 5.7: Path Made o
class Path {
constructor() {
this.radius = 20;
- //{!1} A path is now an array of points (p5.Vector objects).
+ //{!1} A path is now an array of points (p5.Vector objects).
this.points = [];
}
@@ -960,7 +959,7 @@
Complex Systems
Nonlinearity: This aspect of complex systems is often casually referred to as the butterfly effect, coined by mathematician and meteorologist Edward Norton Lorenz, a pioneer in the study of chaos theory. In 1961, Lorenz was running a computer weather simulation for the second time and, perhaps to save a little time, typed in a starting value of 0.506 instead of 0.506127. The end result was completely different from the first result of the simulation.
- Stated more evocatively, the theory is that a single butterfly flapping its wings on the other side of the world could cause a massive weather shift and ruin your weekend at the beach. It‘s called nonlinear because there isn’t a linear relationship between a change in initial conditions and a change in outcome. A small change in initial conditions can have a massive effect on the outcome. Nonlinear systems are a superset of chaotic systems. In Chapter 7, you’ll see how even in a system of many 0s and 1s, if you change just one bit, the result will be completely different.
+ Stated more evocatively, the theory is that a single butterfly flapping its wings on the other side of the world could cause a massive weather shift and ruin your weekend at the beach. It’s called nonlinear because there isn’t a linear relationship between a change in initial conditions and a change in outcome. A small change in initial conditions can have a massive effect on the outcome. Nonlinear systems are a superset of chaotic systems. In Chapter 7, you’ll see how even in a system of many 0s and 1s, if you change just one bit, the result will be completely different.
Competition and cooperation: One ingredient that often makes a complex system tick is the presence of both competition and cooperation among the elements. The upcoming flocking system will have three rules: alignment, cohesion, and separation. Alignment and cohesion will ask the elements to “cooperate” by trying to stay together and move together. Separation, however, will ask the elements to “compete” for space. When the time comes, try taking out just the cooperation or just the competition, and you’ll see how the system loses its complexity. Competition and cooperation are found together in living complex systems, but not in nonliving complex systems like the weather.
@@ -969,7 +968,7 @@
Complex Systems
In this way, the cost and efficiency of public transportation are both the input of the system (determining whether you choose to use it or not) and the output (the degree of traffic congestion and subsequent cost and efficiency). Economic models are just one example of a human complex system. Others include fads and trends, elections, crowds, and traffic flow.
-
Complexity will serve as a key theme for much of the remainder of the book. In this section, I’ll begin by introducing an additional feature to the Vehicle class: the ability to perceive neighboring vehicles. This enhancement will pave the way for a culminating example of a complex system, in which the interplay of simple individual behaviors results in an emergent behavior: flocking.
+
Complexity will serve as a key theme for much of the remainder of the book. In this section, I’ll begin by introducing an additional feature to the Vehicle class: the ability to perceive neighboring vehicles. This enhancement will pave the way for a culminating example of a complex system in which the interplay of simple individual behaviors results in an emergent behavior: flocking.
Implementing Group Behaviors (or: Let’s Not Run Into Each Other)
Managing a group of objects is certainly not a new concept. You’ve seen this before—in Chapter 4, where I developed the Emitter class to represent an overall particle system. There, I used an array to store a list of individual particles. I’ll start with the same technique here and store Vehicle objects in an array:
// Declare an array of Vehicle objects.
@@ -992,7 +991,7 @@
Implementi
}
Maybe I want to add a behavior, a force to be applied to all the vehicles. This could be seeking the mouse:
vehicle.seek(mouseX, mouseY);
-
But that’s an individual behavior. and I’ve already spent the bulk of this chapter worrying about individual behaviors. You’re here because you want to apply a group behavior. I’ll begin with separation, a behavior that commands, “Avoid colliding with your neighbors!”
+
But that’s an individual behavior, and I’ve already spent the bulk of this chapter worrying about individual behaviors. You’re here because you want to apply a group behavior. I’ll begin with separation, a behavior that commands, “Avoid colliding with your neighbors!”
vehicle.separate();
That looks good but is not quite right. What’s missing? In the case of seek(), I said, “Seek mouseX and mouseY.” In the case of separate(), I’m saying, “Separate from everyone else.” Who is everyone else? It’s the list of all the other vehicles:
vehicle.separate(vehicles);
@@ -1026,7 +1025,7 @@
Implementi
Figure 5.32: The desired velocity for separation (equivalent to fleeing) is a vector that points in the opposite direction of a target.
-
Of course, this is just the beginning. The real work happens inside the separate() method. Reynolds defines the separation behavior as, “Steer to avoid crowding.” In other words, if a given vehicle is too close to you, steer away from that vehicle. Sound familiar? Remember the seek behavior, steering a vehicle toward a target? Reverse that force and you have the flee behavior, which is what should be applied here to achieve separation (see Figure 5.32).
+
Of course, this is just the beginning. The real work happens inside the separate() method. Reynolds defines the separation behavior as “steer to avoid crowding.” In other words, if a given vehicle is too close to you, steer away from that vehicle. Sound familiar? Remember the seek behavior, steering a vehicle toward a target? Reverse that force and you have the flee behavior, which is what should be applied here to achieve separation (see Figure 5.32).
@@ -1052,7 +1051,7 @@
Implementi
}
}
-
Notice that I’m checking not only if the distance is less than a desired separation but also if this is not equal to other. This is a key element. Remember, all the vehicles are in the array; without this extra check, the vehicle will attempt to flee from itself!
+
Notice that I’m checking not only whether the distance is less than a desired separation but also whether this is not equal to other. This is a key element. Remember, all the vehicles are in the array; without this extra check, the vehicle will attempt to flee from itself!
If the vehicles are too close, I compute a vector that points away from the offending vehicle:
if (this !== other && d < desiredseparation) {
//{!2 .offset} A vector pointing away from the other’s position
@@ -1078,15 +1077,15 @@
Implementi
//{!1 .bold} Make sure that there is at least one close
// vehicle. You don’t want to bother doing anything
- // if nothing is too close (not to mention you can’t
+ // if nothing is too close (not to mention, you can’t
// divide by zero!).
if (count > 0) {
//{.bold}
sum.div(count);
}
-
Once I have the average vector (stored in the variable sum), that vector can be scaled to the maximum speed and become the desired velocity—the vehicle desires to move in that direction at maximum speed! (In fact, I really don’t have to divide by count anymore since the magnitude is set manually.) And once I have the desired velocity, it’s the same old Reynolds story: steering equals desired minus velocity:
+
Once I have the average vector (stored in the variable sum), that vector can be scaled to the maximum speed and become the desired velocity—the vehicle desires to move in that direction at maximum speed! (In fact, I really don’t have to divide by count anymore since the magnitude is set manually.) And once I have the desired velocity, it’s the same old Reynolds story—steering equals desired minus velocity:
if (count > 0) {
- //{!1} Scale average to maxspeed
+ //{!1} Scale average to max speed
// (this becomes desired).
sum.setMag(this.maxspeed);
@@ -1115,7 +1114,7 @@
Example 5.9: Separation
let d = p5.Vector.dist(this.position, other.position);
if (this !== other && d < desiredSeparation) {
let diff = p5.Vector.sub(this.position, other.position);
- //{!1 .bold} What is the magnitude of the p5.Vector
+ //{!1 .bold} What is the magnitude of the p5.Vector
// pointing away from the other vehicle?
// The closer it is, the more the vehicle should flee.
// The farther, the less. So the magnitude is set
@@ -1183,7 +1182,7 @@
Combining Behaviors
//{!1} Instead of applying the force, return the vector.
return steer;
}
-
This change is subtle, but incredibly important: it allows the strength of these forces to be weighted all in one place.
+
This change is subtle but incredibly important: it allows the strength of these forces to be weighted all in one place.
Example 5.10: Combining Steering Behaviors (Seek and Separate)
@@ -1202,10 +1201,10 @@
Example 5.10
this.applyForce(separate);
this.applyForce(seek);
}
-
In this code, I use mult() to adjust the forces. By multiplying each force vector by a factor, its magnitude is scaled accordingly. These factors (in this case, 1.5 for separate and 0.5 for seek), represent the weight assigned to each force. However, the weights don’t have to be constants. Think about how they might vary dynamically based on conditions within the environment or properties of the vehicle. For example, what if the seek weight increases when the vehicle detects food nearby (imagine the vehicle as a creature with a hunger property) or the separate weight becomes larger if the vehicle enters a crowded area. This flexibility in adjusting the weights allows for more sophisticated and nuanced behaviors to emerge.
+
In this code, I use mult() to adjust the forces. By multiplying each force vector by a factor, its magnitude is scaled accordingly. These factors (in this case, 1.5 for separate and 0.5 for seek) represent the weight assigned to each force. However, the weights don’t have to be constants. Think about how they might vary dynamically based on conditions within the environment or properties of the vehicle. For example, what if the seek weight increases when the vehicle detects food nearby (imagine the vehicle as a creature with a hunger property) or the separate weight becomes larger if the vehicle enters a crowded area? This flexibility in adjusting the weights allows for more sophisticated and nuanced behaviors to emerge.
Exercise 5.14
-
Modify Example 5.10 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 what if some vehicles are more concerned with seeking and others more concerned with separating? Can you introduce other steering behaviors as well?
+
Modify Example 5.10 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 what if some vehicles are more concerned with seeking and others are more concerned with separating? Can you introduce other steering behaviors as well?
Flocking
Flocking is a group animal behavior found in many living creatures, such as birds, fish, and insects. In 1986, Reynolds created a computer simulation of flocking behavior and documented the algorithm in his paper “Flocks, Herds, and Schools: A Distributed Behavioral Model.” Re-creating this simulation in p5.js will bring together all the concepts in this chapter:
@@ -1216,7 +1215,7 @@
Flocking
The result will be a complex system—intelligent group behavior will emerge from the simple rules of flocking without the presence of a centralized system or leader.
The good news is, I’ve already demonstrated items 1 through 3 in this chapter, so this section can just be about putting it all together and seeing the result.
-
Before I begin, I should mention that I’m going to change the name of the Vehicle class (yet again). Reynolds uses the term boid (a made-up word that refers to a bird-like object) to describe the elements of a flocking system. I’ll do the same.
+
Before I begin, I should mention that I’m going to change the name of the Vehicle class (yet again). Reynolds uses the term boid (a made-up word that refers to a birdlike object) to describe the elements of a flocking system. I’ll do the same.
Three rules govern flocking:
Separation (aka avoidance): Steer to avoid colliding with your neighbors.
@@ -1228,7 +1227,7 @@
Flocking
Figure 5.34: The three rules of flocking: separation, alignment, and cohesion. The example vehicle and desired velocity are bold.
-
Just as with Example 5.10, which I combined separation and seeking, I want the Boid objects to have a single method that manages all three behaviors. I’ll call it flock():
+
Just as with Example 5.10, in which I combined separation and seeking, I want the Boid objects to have a single method that manages all three behaviors. I’ll call it flock():
flock(boids) {
//{!3} The three flocking rules
let separation = this.separate(boids);
@@ -1269,7 +1268,7 @@
Flocking
Figure 5.35: The example vehicle (bold) interacts with only the vehicles within its neighborhood (the circle).
-
I already applied similar logic when I implemented separation, calculating a force based on only other vehicles within a certain distance. Now I want to do the same for alignment (and eventually, cohesion):
+
I already applied similar logic when I implemented separation, calculating a force based only on other vehicles within a certain distance. Now I want to do the same for alignment (and eventually, cohesion):
align(boids) {
//{!1} This is an arbitrary value that could vary from boid to boid.
let neighborDistance = 50;
@@ -1294,7 +1293,7 @@
Flocking
return createVector(0, 0);
}
}
-
As with the separate() method, I’ve included the condition this !== other condition to ensure that a boid doesn’t consider itself when calculating the average velocity. It would probably work regardless, but having each boid constantly be influenced by its own velocity could lead to a feedback loop that would disrupt the overall behavior.
+
As with the separate() method, I’ve included the condition this !== other to ensure that a boid doesn’t consider itself when calculating the average velocity. It would probably work regardless, but having each boid constantly be influenced by its own velocity could lead to a feedback loop that would disrupt the overall behavior.
Exercise 5.15
@@ -1386,7 +1385,7 @@
Exercise 5.17
-
In his book The Computational Beauty of Nature (Bradford Books, 2000), Gary Flake describes a fourth rule for flocking, view: “Move laterally away from any boid that blocks the view.” Have your boids follow this rule.
+
In his book The Computational Beauty of Nature (Bradford Books, 2000), Gary Flake describes a fourth rule for flocking, view: “Move laterally away from any boid that blocks the view.” Have your boids follow this rule.
Now, things are getting really slow. Really, really, really slow.
+
Now things are getting really slow. Really, really, really slow.
Notice a pattern? As the number of elements increases by a factor of 10, the number of required cycles increases by a factor of 100. More broadly, as the number of elements increases by a factor of N, the cycles increase by a factor of N \times N, or N^2. In big O notation, this is known as O(N^2).
Perhaps you’re thinking, “No problem. With flocking, I need to consider only the boids that are close to the current boid. So even if I have 1,000 boids, I can just look at, say, the 5 closest boids to each one, and then I only have 5,000 cycles.” You pause for a moment and then start thinking, “So for each boid, I just need to check all the boids and find the 5 closest ones and I’m good!” See the catch-22? Even if you want to look at only the close ones, the only way to know what the close ones are would be to check all of them.
Or is there another way?
@@ -1416,7 +1415,7 @@
Spatial Subdivisions
Figure 5.36: A square canvas full of vehicles, subdivided into a grid of square cells
-
Now say that in order to apply the flocking rules to a given boid, you need to look at only the other boids that are in that boid’s cell. With an average of 20 boids per cell, each cell would require 400 cycles (20 \times 20 = 400), and with 100 cells, that’s 40,000 cycles total (400 \times 100 = \text{40,000}). That’s a massive savings of over 4,000,000!
+
Now say that in order to apply the flocking rules to a given boid, you need to look at only the other boids that are in that boid’s cell. With an average of 20 boids per cell, each cell would require 400 cycles (20 \times 20 = 400), and with 100 cells, that’s 40,000 cycles total (400 \times 100 = \text{40,000}). That’s a massive savings of over 4,000,000 cycles!
To implement the bin-lattice spatial subdivision algorithm in p5.js, I’ll need multiple arrays. The first array keeps track of all the boids, just as in the original flocking example:
let boids = [];
The second is a 2D array (repurposing the code from Example 5.4) representing the cells in the grid:
@@ -1479,7 +1478,7 @@
Example 5.12: Bin-Lattice S
}
I’m covering only the basics of the bin-lattice algorithm here. In practice, each boid should also check the boids in the neighboring cells (above, below, left, right, and diagonals), as well as the boids in its own cell. (To find out how that’s done, see the full code on the book’s website.) Even with that extra checking, however, the algorithm is still much more efficient than checking every single boid.
This approach still has flaws, however. For example, what if all the boids congregate in the corner and live in the same cell? Doesn’t that take me right back to checking all 2,000 against all 2,000? In fact, bin-lattice spatial subdivision is most effective 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 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 when and where it's needed.
+
The quadtree expands 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 when and where it’s needed.
Example 5.13: Quadtree
@@ -1497,7 +1496,7 @@
More Optimization Tricks
Use the magnitude squared (or sometimes the distance squared).
Calculate the sine and cosine lookup tables.
-
Don’t make gazillions of unnecessary p5.Vector objects.
+
Don’t make gazillions of unnecessary p5.Vector objects.
Each of these tips is detailed next.
Use the Magnitude Squared
@@ -1549,7 +1548,7 @@
Example 5.14: Sin/Cos Lookup Table
The code accompanying Example 5.14 enhances the initial snippets by incorporating variables for the lookup table’s precision, allowing it to store values at increments of less than 1 degree.
-
Don’t Make Gazillions of Unnecessary p5.Vector Objects
+
Don’t Make Gazillions of Unnecessary p5.Vector Objects
In any sketch, every object you create occupies space in the computer’s memory. This might not be a concern with just a few objects, but when sketches generate many objects, especially in loops or over time, it can slow performance. Sometimes it turns out that not all the objects are really necessary.
I have to admit, I’m perhaps the biggest culprit when it comes to creating excessive objects. In the interest of writing clear and understandable examples, I often choose to make extra p5.Vector objects when I absolutely don’t need to. For the most part, this isn’t a problem at all. But sometimes it can be. Take a look at this example:
function draw() {
@@ -1566,7 +1565,7 @@
Don’t Make Gazillions of
v.seek(mouse);
}
}
-
Now I’ve made just 1 vector instead of 1,000. Even better, I could turn the vector into a global variable, and then just assign the x and y value within draw() with set():
+
Now I’ve made just 1 vector instead of 1,000. Even better, I could turn the vector into a global variable and then just assign the x and y values within draw() with set():
let mouse;
function setup() {
diff --git a/content/06_libraries.html b/content/06_libraries.html
index 34e2e025..fc6e4919 100644
--- a/content/06_libraries.html
+++ b/content/06_libraries.html
@@ -5,7 +5,7 @@
Chapter 6. Physics Libraries
A library implies an act of faith
Which generations still in darkness hid
- Sign in their night in witness of the dawn.
+ Sign in their night, in witness of the dawn.
—Victor Hugo
@@ -14,24 +14,24 @@
Chapter 6. Physics Libraries
-
- Photo by Arshiya Urveeja Bose, CC BY-SA 4.0.
+
+ Photo by Arshiya Urveeja Bose
Living Root Bridges
-
In the Indian state of Meghalaya, the Khasi and Jaiñtia peoples live in areas that experience some of the highest rainfall in the world. During the monsoon season, floods often make traveling between villages impossible. As a result, the ancient tradition of constructing living root bridges emerged. These bridges, like the double living root bridge in East Khasi shown here, are created by guiding and growing tree roots through bamboo, palm trunks, or steel scaffolding. They grow and become stronger as the roots interact with the environment, forming adaptive, spring-like connections.
+
In the Indian state of Meghalaya, the Khasi and Jaiñtia peoples live in areas that experience some of the highest rainfall in the world. During the monsoon season, floods often make traveling between villages impossible. As a result, the ancient tradition of constructing living root bridges emerged. These bridges, like the double living root bridge in East Khasi shown here, are created by guiding and growing tree roots through bamboo, palm trunks, or steel scaffolding. They grow and become stronger as the roots interact with the environment, forming adaptive, springlike connections.
Think about what you’ve accomplished so far in this book. You’ve done the following:
-
Learned about concepts from the world of physics. (What is a vector? What is a force? What is a wave?)
+
Learned about concepts from the world of physics (What is a vector? What is a force? What is a wave?)
Understood the math and algorithms behind those concepts
Implemented those algorithms in p5.js with an object-oriented approach, culminating in building simulations of autonomous steering agents
These activities have yielded a set of motion simulations, allowing you to creatively define the physics of the worlds you build (whether realistic or fantastical). But, of course, you and I aren’t the first or only people to do this. The world of computer graphics and programming is full of prewritten code libraries dedicated to physics simulations.
-
Just try searching open-source physics engine and you could spend the rest of your day poring over a host of rich and complex codebases. This begs the question: If an existing code library takes care of physics simulation, why should you bother learning how to write any of the algorithms yourself? Here’s where the philosophy behind this book comes into play. While many libraries provide out-of-the-box physics to experiment with (super awesome, sophisticated, and robust physics at that), there are several good reasons for learning the fundamentals from scratch before diving into such libraries.
+
Just try searching open source physics engine and you could spend the rest of your day poring over a host of rich and complex codebases. This begs the question: If an existing code library takes care of physics simulation, why should you bother learning how to write any of the algorithms yourself? Here’s where the philosophy behind this book comes into play. While many libraries provide out-of-the-box physics to experiment with (super-awesome, sophisticated, and robust physics at that), there are several good reasons for learning the fundamentals from scratch before diving into such libraries.
First, without an understanding of vectors, forces, and trigonometry, it’s easy to get lost just reading the documentation of a library, let alone using it. Second, even though a library may take care of the math behind the scenes, it won’t necessarily simplify your code. A great deal of overhead may be required in understanding how a library works and what it expects from you code-wise. Finally, as wonderful as a physics engine might be, if you look deep down into your heart, you’ll likely see that you seek to create worlds and visualizations that stretch the limits of the imagination. A library may be great, but it provides only a limited set of features. It’s important to know when to live within those limitations in the pursuit of a creative coding project and when those limits will prove to be confining.
-
This chapter is dedicated to examining two open source physics libraries for JavaScript: Matter.js and Toxiclibs.js. I don’t mean to imply that these are the only libraries you should use for any and all creative coding projects that could benefit from a physics engine (see “Other Physics Libraries” box on page XX for alternatives, and check the book’s website for ports of the chapter’s examples to other libraries). However, both libraries integrate nicely with p5.js and will allow me to demonstrate the fundamental concepts behind physics engines and how they relate to and build upon the material I’ve covered so far.
-
Ultimately, the aim of this chapter isn't to teach you the details of a specific physics library, but to provide you with a foundation for working with any physics library. The skills you acquire here will enable you to navigate and understand documentation, opening the door for you to expand your abilities with any library you choose.
+
This chapter is dedicated to examining two open source physics libraries for JavaScript: Matter.js and Toxiclibs.js. I don’t mean to imply that these are the only libraries you should use for any and all creative coding projects that could benefit from a physics engine (see “Other Physics Libraries” on page XX for alternatives, and check the book’s website for ports of the chapter’s examples to other libraries). However, both libraries integrate nicely with p5.js and will allow me to demonstrate the fundamental concepts behind physics engines and how they relate to and build upon the material I’ve covered so far.
+
Ultimately, the aim of this chapter isn’t to teach you the details of a specific physics library, but to provide you with a foundation for working with any physics library. The skills you acquire here will enable you to navigate and understand documentation, opening the door for you to expand your abilities with any library you choose.
Why Use a Physics Library?
I’ve made the case for writing your own physics simulations (as you’ve learned to do in the previous chapters), but why use a physics library? After all, adding any external framework or library to a project introduces complexity and extra code. Is that additional overhead worth it? If you just want to simulate a circle falling down because of gravity, for example, do you really need to import an entire physics engine and learn its API? As the early chapters of this book hopefully demonstrated, probably not. Lots of scenarios like this are simple enough for you to get by writing the code yourself.
But consider another scenario. What if you want to have 100 circles falling? And what if they aren’t circles at all, but rather irregularly shaped polygons? And what if you want these polygons to bounce off one another in a realistic manner when they collide?
@@ -53,12 +53,12 @@
Other Physics Libraries
A multitude of other physics libraries are worth exploring alongside this chapter’s two case studies, each with unique strengths that may offer advantages in certain kinds of projects. In fact, when I first began writing this book, Matter.js didn’t exist, so the physics engine I initially used to demonstrate the examples was Box2D. It was (and likely still is) the most well-known physics engine of them all.
Box2D began as a set of physics tutorials written in C++ by Erin Catto for the Game Developers Conference in 2006. Since then, Box2D has evolved into a rich and elaborate open source physics engine. It’s been used for countless projects, most notably highly successful games such as the award-winning Crayon Physics and the runaway hit Angry Birds.
One important feature of Box2D is that it’s a true physics engine: it knows nothing about computer graphics and the world of pixels, and instead does all its measurements and calculations in real-world units like meters, kilograms, and seconds. It’s just that its “world” (a key term in Box2D) is a 2D plane with top, bottom, left, and right edges. You tell it things like “The gravity of the world is 9.81 newtons per kilogram, and a circle with a radius of 4 meters and a mass of 50 kilograms is located 10 meters above the world’s bottom.” Box2D will then tell you things like “One second later, the rectangle is at 5 meters from the bottom; two seconds later, it’s 10 meters below,” and so on.
-
While this provides for an amazingly accurate and robust physics engine (one that’s highly optimized and fast for C++ projects), it also necessitates lots of complicated code to translate back and forth between Box2D’s physics world and the world you want to draw —the pixel world of the graphics canvas. This creates a tremendous burden for the coder. I will, as best I can, continue to maintain a set of Box2D-compatible examples for this book (there are several JavaScript ports), but I believe the relative simplicity of working with a library like Matter.js that’s native to JavaScript and uses pixels as the unit of measurement will make for a more intuitive and friendly bridge from my p5.js examples.
+
While this provides for an amazingly accurate and robust physics engine (one that’s highly optimized and fast for C++ projects), it also necessitates lots of complicated code to translate back and forth between Box2D’s physics world and the world you want to draw—the pixel world of the graphics canvas. This creates a tremendous burden for the coder. I will, as best I can, continue to maintain a set of Box2D-compatible examples for this book (there are several JavaScript ports), but I believe the relative simplicity of working with a library like Matter.js that is native to JavaScript and uses pixels as the unit of measurement will make for a more intuitive and friendly bridge from my p5.js examples.
Another notable library is p5play, a project initiated by Paolo Pedercini and currently led by Quinton Ashley that was specifically designed for game development. It simplifies the creation of visual objects—known as sprites—and manages their interactions (namely, collisions and overlaps). As you may have guessed from the name, p5play is tailored to work seamlessly with p5.js. It uses Box2D under the hood for physics simulation.
Importing the Matter.js Library
In a moment, I’ll turn to working with Matter.js, created by Liam Brummitt in 2014. But before you can use an external JavaScript library in a p5.js project, you need to import it into your sketch. As you’re already quite aware, I’m using the official p5.js web editor for developing and sharing this book’s code examples. The easiest way to add a library is to edit the index.html file that’s part of every new p5.js sketch created in the editor.
-
To do that, first expand the file navigation bar on the lefthand side of the editor and select index.html, as shown in Figure 6.2.
+
To do that, first expand the file navigation bar on the left-hand side of the editor and select index.html, as shown in Figure 6.2.
In the coming sections, I’ll walk through each of these elements in detail, building several examples along the way. But first, there’s one other important element to briefly discuss:
-
[number: 5;]Vector: Describes an entity with magnitude and direction using x- and y-components, defining positions, velocities, and forces in a Matter.js world.
+
+
Vector: Describes an entity with magnitude and direction using x- and y-components, defining positions, velocities, and forces in a Matter.js world.
+
This brings us to an important crossroads. Any physics library is fundamentally built around vectors, and depending on how you spin it, that’s either a good thing or a bad thing. The good part is that you’ve just spent several chapters familiarizing yourself with what it means to describe motion and forces with vectors, so there’s nothing conceptually new for you to learn. The bad part—the part that makes a single tear fall from my eye—is that once you cross this threshold into the brave new world of physics libraries, you don’t get to use p5.Vector anymore.
It’s been great that p5.js has a built-in vector representation, but anytime you use a physics library, you’ll likely discover that it includes its own separate vector implementation, designed to be especially compatible with the rest of the library’s code. This makes sense. After all, why should Matter.js be expected to know about p5.Vectorobjects?
The upshot of all this is that while you won’t have to learn any new concepts, you do have to get used to new naming conventions and syntax. To illustrate, I’ll show you some now-familiar p5.Vector operations alongside the equivalent Matter.Vector code. First, how do you create a vector?
@@ -214,20 +216,20 @@
Matter.js Overview
-
As you can see, the concepts are the same, but the specifics of the code are different. First, every method name is now preceded by Matter.Vector, which defines the namespace of the source code. This is common for JavaScript libraries; p5.js is unusual for not consistently using namespaces. For example, to draw a circle in p5.js, you call circle() rather than p5.circle(). The circle() function lives in the global namespace. This, in my view, is one of the features that makes p5.js special in terms of ease of use and beginner friendliness. However, it also means that for any code you write with p5, you can’t use circle as a variable name. Namespacing a library protects against these kinds of errors and naming conflicts, and it’s why you’ll see everything in Matter.js called with the Matter prefix.
-
In addition, unlike p5’s static and nonstatic versions of vector methods like add() and mult(), all vector methods in Matter.js are static. If you want to change a Matter.Vector while operating on it, you can add it as an optional argument: Matter.Vector.add(a, b, a): adds a and b and places the result in a (the third argument). You can also set an existing variable to the newly created vector object resulting from a calculation, as in v = Matter.Vector.mult(v, 2). However, this version still creates a new vector in memory rather than updating the old one.
+
As you can see, the concepts are the same, but the specifics of the code are different. First, every method name is now preceded by Matter.Vector, which defines the namespace of the source code. This is common for JavaScript libraries; p5.js is unusual for not consistently using namespaces. For example, to draw a circle in p5.js, you call circle() rather than p5.circle(). The circle() function lives in the global namespace. This, in my view, is one of the features that makes p5.js special in terms of ease of use and beginner friendliness. However, it also means that for any code you write with p5.js, you can’t use circle as a variable name. Namespacing a library protects against these kinds of errors and naming conflicts, and it’s why you’ll see everything in Matter.js called with the Matter prefix.
+
In addition, unlike p5.js’s static and nonstatic versions of vector methods like add() and mult(), all vector methods in Matter.js are static. If you want to change a Matter.Vector while operating on it, you can add it as an optional argument: Matter.Vector.add(a, b, a) adds a and b and places the result in a (the third argument). You can also set an existing variable to the newly created vector object resulting from a calculation, as in v = Matter.Vector.mult(v, 2). However, this version still creates a new vector in memory rather than updating the old one.
Many physics libraries include a world object to manage everything. The world is typically in charge of the coordinate space, keeping a list of all the bodies in the simulation, controlling time, and more. In Matter.js, the world is created inside an Engine object, the main controller of your physics world and simulation:
-
// An alias for the Matter.js Engine class
+
// An alias for the Matter.js Engine class
let Engine = Matter.Engine;
-//{!1} A reference to the Matter physics engine
+//{!1} A reference to the Matter.js physics engine
let engine;
function setup() {
createCanvas(640, 360);
- // Create the Matter engine.
+ // Create the Matter.js engine.
engine = Engine.create();
}
Notice that the very first line of code creates an Engine variable and sets it equal to Matter.Engine. Here, I’m deciding to point the single keyword Engine to the Engine class namespaced inside Matter.js in order to make my code less verbose. This works because I know I won’t be using the word Engine for any other variables, nor does it conflict with something in p5.js. I’ll be doing this with Vector, Bodies, Composite, and more as I continue to build the examples. (But while the linked source code will always include all the aliases, I won’t always show them in the book text.)
@@ -253,8 +255,8 @@
Bodies
All the factory methods for creating bodies can be found in the Matter.Bodiesdocumentation page. I’ll start with the rectangle() method:
// Create a Matter.js body with a rectangular shape.
let box = Bodies.rectangle(x, y, w, h);
-
What luck! The rectangle()method signature is exactly the same as p5.js’s rect() function. In this case, however, the method isn’t drawing a rectangle but rather building the geometry for a Body object to store. (Note that calling Bodies.rectangle() works only if you first establish Bodies as an alias to Matter.Bodies.)
-
A body has now been created with a position and a size, and a reference to it is stored in the variable box. Bodies have many more properties that affect its motion, however. For example, density ultimately determines that body’s mass. Friction and restitution (bounciness) affect how the body interacts when it comes into contact with other bodies. For most cases, the defaults are sufficient, but Matter.js does allow you to specify these properties by passing through an additional argument to the factory method in the form of a JavaScript object literal, a collection of key-value pairs separated by commas and enclosed in curly brackets.
+
What luck! The rectangle() method signature is exactly the same as p5.js’s rect() function. In this case, however, the method isn’t drawing a rectangle but rather building the geometry for a Body object to store. (Note that calling Bodies.rectangle() works only if you first establish Bodies as an alias to Matter.Bodies.)
+
A body has now been created with a position and a size, and a reference to it is stored in the variable box. Bodies have many more properties that affect their motion, however. For example, density ultimately determines that body’s mass. Friction and restitution (bounciness) affect how the body interacts when it comes into contact with other bodies. For most cases, the defaults are sufficient, but Matter.js does allow you to specify these properties by passing through an additional argument to the factory method in the form of a JavaScript object literal, a collection of key-value pairs separated by commas and enclosed in curly brackets:
//{!5} Specify the properties of this body in an object literal.
let options = {
friction: 0.5,
@@ -262,7 +264,7 @@
Bodies
density: 0.002
}
let box = Matter.Bodies.rectangle(x, y, w, h, options);
-
Each key in the object literal (for example, friction) serves as a unique identifier, and its value (0.5) is the data associated with that key. You can think of an object literal is as a simple dictionary or lookup table—in this case, holding the desired settings for a new Matter.js body. Note, however, that while the options argument is useful for configuring the body, other initial conditions, such as linear or angular velocity, can be set via static methods of the Matter.Body class:
+
Each key in the object literal (for example, friction) serves as a unique identifier, and its value (0.5) is the data associated with that key. You can think of an object literal as a simple dictionary or lookup table—in this case, holding the desired settings for a new Matter.js body. Note, however, that while the options argument is useful for configuring the body, other initial conditions, such as linear or angular velocity, can be set via static methods of the Matter.Body class:
// Set an arbitrary initial linear and angular velocity.
const v = Vector.create(2, 0);
Body.setVelocity(box, v);
@@ -304,7 +306,7 @@
Render
One more critical order of business remains: physics engines must be told to step forward in time. Since I’m using the built-in renderer, I can also use the built-in runner, which runs the engine at a default frame rate of 60 frames per second. The runner is also customizable, but the details aren’t terribly important since the goal here is to move toward using p5.js’s draw() loop instead (coming in the next section):
// Run the engine!
Runner.run(engine);
-
Here’s the Matter.js code all together, with an added ground object—another rectangular body. Note the use of the { isStatic: true } option in the creation of the ground body to ensure that it remains in a fixed position. I’ll cover more details about static bodies in "Static Matter.js Bodies" on page XX.
+
Here’s the Matter.js code all together, with an added ground object—another rectangular body. Note the use of the { isStatic: true } option in the creation of the ground body to ensure that it remains in a fixed position. I’ll cover more details about static bodies in “Static Matter.js Bodies” on page XX.
Example 6.1: Matter.js Default Render and Runner
@@ -355,7 +357,7 @@
Example 6.1: Matter.js De
}
There’s no draw() function here, and all the variables are local to setup(). In fact, I’m not using any p5.js capabilities (beyond injecting a canvas onto the page). This is exactly what I want to tackle next!
Matter.js with p5.js
-
Matter.js keeps a list of all bodies that exist in the world, and as you’ve just seen, it can handle drawing and animating them with the Render and Runner objects. (That list, incidentally, is stored in engine.world.bodies.) What I’d like to show you now, however, is a technique for keeping your own list(s) of Matter.js bodies, so you can draw them with p5.
+
Matter.js keeps a list of all bodies that exist in the world, and as you’ve just seen, it can handle drawing and animating them with the Render and Runner objects. (That list, incidentally, is stored in engine.world.bodies.) What I’d like to show you now, however, is a technique for keeping your own list(s) of Matter.js bodies, so you can draw them with p5.js.
Yes, this approach may add redundancy and sacrifice a small amount of efficiency, but it more than makes up for that with ease of use and customization. With this methodology, you’ll be able to code as you’re accustomed to in p5.js, keeping track of which bodies are which and drawing them appropriately. Consider the file structure of the sketch shown in Figure 6.3.
@@ -408,7 +410,7 @@
Step 1: Add Matter.js to the p5.js Sketch
As it stands, the sketch makes no reference to Matter.js. That clearly needs to change. Fortunately, this part isn’t too tough: I’ve already demonstrated all the elements needed to build a Matter.js world. (And don’t forget, in order for this to work, make sure the library is imported in index.html.)
@@ -429,7 +431,7 @@
Step 1: Add Matter.js to the p5.
Engine.update(engine);
}
The Engine.update() method advances the physics world one step forward in time. Calling it inside the p5.js draw() loop ensures that the physics will update at every frame of the animation. This mechanism takes the place of the built-in Matter.js Runner object I used in Example 6.1. The draw() loop is the runner now!
-
Internally, when Engine.update() is called, Matter.js sweeps through the world, looks at all the bodies in it, and figures out what to do with them. Just calling Engine.update() on its own moves the world forward with default settings. However, as with Render, these settings are customizable and documented in the Matter.js reference.
+
Internally, when Engine.update() is called, Matter.js sweeps through the world, looks at all the bodies in it, and figures out what to do with them. Just calling Engine.update() on its own moves the world forward with default settings. However, as with Render, these settings are customizable and documented in the Matter.js documentation.
Step 2: Link Every Box Object with a Matter.js Body
I’ve set up my Matter.js world; now I need to link each Box object in my p5.js sketch with a body in that world. The original Box class includes variables for position and width. What I now want to say is “I hereby relinquish command of this object’s position to Matter.js. I no longer need to keep track of anything related to position, velocity, or acceleration. Instead, I need to keep track of only the existence of a Matter.js body and have faith that the physics engine will do the rest.”
@@ -459,7 +461,7 @@
Step 3: Draw the Box Body
let angle = this.body.angle;
Once I have the position and angle, I can render the object by using the native p5.js translate(), rotate(), and square() functions:
show() {
- //{!2} I need the Body’s position and angle.
+ //{!2} I need the body’s position and angle.
let position = this.body.position;
let angle = this.body.angle;
@@ -474,7 +476,7 @@
Step 3: Draw the Box Body
square(0, 0, this.w);
pop();
}
-
It’s important to note here that if you delete a Box object from the boxes array—perhaps when it moves outside the boundaries of the canvas or reaches the end of its lifespan, as demonstrated in Chapter 4—you must also explicitly remove the body associated with that Box object from the Matter.js world. This can be done with a removeBody() method on the Box class:
+
It’s important to note here that if you delete a Box object from the boxes array—perhaps when it moves outside the boundaries of the canvas or reaches the end of its life span, as demonstrated in Chapter 4—you must also explicitly remove the body associated with that Box object from the Matter.js world. This can be done with a removeBody() method on the Box class:
// This function removes a body from the Matter.js world.
removeBody() {
Composite.remove(engine.world, this.body);
@@ -485,7 +487,7 @@
Exercise 6.2
Start with the code for Example 6.2 and, using the methodology outlined in this chapter, add the code to implement Matter.js physics. Delete bodies that have left the canvas. The result should appear as in this image. Feel free to be creative in the way you draw the boxes!
- Dragging the mouse adds new boxes.
+
Static Matter.js Bodies
@@ -508,7 +510,7 @@
Example 6.3: Falling Boxes
this.y = y;
this.w = w;
this.h = h;
- //{!1} Lock the body in place by setting isStatic to true!
+ //{!1} Lock the body in place by setting isStatic to true!
let options = { isStatic: true };
this.body = Bodies.rectangle(this.x, this.y, this.w, this.h, options);
Composite.add(engine.world, this.body);
@@ -577,7 +579,7 @@
Example 6.4: Polygon Shapes
Figure 6.6: A concave shape can be drawn with multiple convex shapes.
-
Since the shape is built out of custom vertices, you can use p5’s beginShape(), endShape(), and vertex() functions when it comes time to actually draw the body. The CustomShape class could include an array to store the vertices’ pixel positions, relative to (0, 0), for drawing purposes. However, it’s best to query Matter.js for the positions instead. This way, there’s no need to use translate() or rotate(), since the Matter.js body stores its vertices as absolute world positions:
+
Since the shape is built out of custom vertices, you can use p5.js’s beginShape(), endShape(), and vertex() functions when it comes time to actually draw the body. The CustomShape class could include an array to store the vertices’ pixel positions, relative to (0, 0), for drawing purposes. However, it’s best to query Matter.js for the positions instead. This way, there’s no need to use translate() or rotate(), since the Matter.js body stores its vertices as absolute world positions:
// Add the compound body to the world.
Composite.add(engine.world, body);
-
While this does create a compound body by combining two shapes, the code isn’t quite right. If you run it, you’ll see that both shapes are centered around the same (x, y) position, as in Figure 6.7.
+
While this does create a compound body by combining two shapes, the code isn’t quite right. If you run it, you’ll see that both shapes are centered on the same (x, y) position, as in Figure 6.7.
Figure 6.7: A rectangle and a circle with the same (x, y) reference point
@@ -621,7 +623,7 @@
Exercise 6.3
Figure 6.8: A circle placed relative to a rectangle with a horizontal offset
-
I’ll use half the width of the rectangle as the offset, so the circle is centered around the edge of the rectangle:
+
I’ll use half the width of the rectangle as the offset, so the circle is centered on the edge of the rectangle:
let part1 = Bodies.rectangle(x, y, w, h);
//{!2} Add an offset from the x-position of the lollipop’s stick.
@@ -647,7 +649,7 @@
Example 6.5: Multiple Shapes on
stroke(0);
strokeWeight(1);
- // Translate and rotate the rectangle (part1).
+ // Translate and rotate the rectangle (part1).
push();
translate(position1.x, position1.y);
rotate(angle);
@@ -655,14 +657,14 @@
Example 6.5: Multiple Shapes on
rect(0, 0, this.w, this.h);
pop();
- // Translate and rotate the circle (part2).
+ // Translate and rotate the circle (part2).
push();
translate(position2.x, position2.y);
rotate(angle);
circle(0, 0, this.r * 2);
pop();
}
-
Before moving on, I want to stress that what you draw in your canvas window doesn’t magically experience perfect physics just by the mere act of creating Matter.js bodies. The chapter’s examples have worked because I’ve been carefully matching the way I’ve drawn each p5.js with the way I’ve defined the geometry of each Matter.js body. If you accidentally draw a shape differently, you won’t get an error—not from p5.js or from Matter.js. However, your sketch will look odd, and the physics won’t work correctly because the world you’re seeing won’t be aligned with the world as Matter.js understands it.
+
Before moving on, I want to stress that what you draw in your canvas window doesn’t magically experience perfect physics just by the mere act of creating Matter.js bodies. The chapter’s examples have worked because I’ve been carefully matching the way I’ve drawn each p5.js body with the way I’ve defined the geometry of each Matter.js body. If you accidentally draw a shape differently, you won’t get an error—not from p5.js or from Matter.js. However, your sketch will look odd, and the physics won’t work correctly because the world you’re seeing won’t be aligned with the world as Matter.js understands it.
To illustrate, let me return to Example 6.5. A lollipop is a compound body consisting of two parts, a rectangle (this.part1) and a circle (this.part2). I’ve been drawing each lollipop by getting the positions for the two parts separately: this.part1.position and this.part2.position. However, the overall compound body also has a position, this.body.position. It would be tempting to use that as the position for drawing the rectangle, and to figure out the circle’s position manually using an offset. After all, that’s how I conceived of the compound shape to begin with (look back at Figure 6.8):
show() {
let position = this.body.position;
@@ -679,7 +681,7 @@
Example 6.5: Multiple Shapes on
Figure 6.9: What happens when the shapes are drawn differently from their Matter.js configurations
-
At first glance, this new version may look fine, but if you look closer, the collisions are off and the shapes overlap in odd ways. This isn’t because the physics is broken; it’s because I’m not communicating properly between p5.js and Matter.js. It turns out the overall body position isn’t the center of the rectangle, but rather the center of mass between the rectangle and the circle. Matter.js is calculating the physics and managing collisions as before, but I’m drawing each body in the wrong place! (In the online version, you can toggle the correct and incorrect rendering by clicking the mouse.)
+
At first glance, this new version may look fine, but if you look closer, the collisions are off and the shapes overlap in odd ways. This isn’t because the physics is broken; it’s because I’m not communicating properly between p5.js and Matter.js. It turns out the overall body position isn’t the center of the rectangle, but rather the center of mass between the rectangle and the circle. Matter.js is calculating the physics and managing collisions as before, but I’m drawing each body in the wrong place! (In the online version, you can toggle the correct and incorrect renderings by clicking the mouse.)
Exercise 6.4
Make your own little alien being by using multiple shapes attached to a single body. Remember, you aren’t limited to using the basic shape-drawing functions in p5.js; you can use images and colors, add hair with lines, and more. Think of the Matter.js shapes as skeletons for your original fantastical design!
@@ -716,7 +718,7 @@
Distance Constraints
stiffness: 0.5
}
Technically, the only required options are bodyA and bodyB, the two bodies connected by the constraint. If you don’t specify any additional options, Matter.js will choose defaults for the other properties. For example, it will use (0, 0) for each relative anchor point (the body’s center), set the length to the current distance between the bodies, and assign a default stiffness of 0.7. Two other notable options I didn’t include are damping and angularStiffness. The damping option affects the constraint’s resistance to motion, with higher values causing the constraint to lose energy more quickly. The angularStiffness option controls the rigidity of the constraint’s angular motion, with higher values resulting in less angular flexibility between the bodies.
-
Once the options are configured, the constraint can be created. As usual, this assumes another alias: Constraint is equal to Matter.Constraint:
+
Once the options are configured, the constraint can be created. As usual, this assumes another alias—Constraint is equal to Matter.Constraint:
let constraint = Constraint.create(options);
//{!1} Don’t forget to add the constraint to the world!
Composite.add(engine.world, constraint);
@@ -794,8 +796,8 @@
Revolute Constraints
Another kind of connection between bodies common to physics engines is a revolute joint. This type of constraint connects two bodies at a common anchor point, also known as a hinge (see Figure 6.11). While Matter.js doesn’t have a separate revolute constraint, you can make one with a regular Constraint of length 0. This way, the bodies can rotate around a common anchor point.
-
The first step is to create the connected bodies. For a first example, I’d like to create a spinning rectangle (akin to a propellor or windmill) in a fixed position. For this case, I need only one body connected to a point. This simplifies the code since I don’t have to worry about collisions between the two bodies connected at a hinge:
-
// Create a body at a given (x, y) with a width and height.
+
The first step is to create the connected bodies. For a first example, I’d like to create a spinning rectangle (akin to a propeller or windmill) in a fixed position. For this case, I need only one body connected to a point. This simplifies the code since I don’t have to worry about collisions between the two bodies connected at a hinge:
+
// Create a body at a given position with width and height.
let body = Bodies.rectangle(x, y, w, h);
Composite.add(engine.world, body);
Next, I can create the constraint. With a length of 0, it needs a stiffness of 1; otherwise, the constraint may not be stable enough to keep the body connected at the anchor point:
@@ -863,7 +865,7 @@
Exercise 6.6
Mouse Constraints
-
Before I introduce the MouseConstraint class, consider the following question: how do you set the position of a Matter.js body to the mouse position? More to the point, why would you need a constraint for this? After all, you have access to the body’s position, and you have access to the mouse’s position. What’s wrong with assigning one to the other?
+
Before I introduce the MouseConstraint class, consider the following question: How do you set the position of a Matter.js body to the mouse position? More to the point, why would you need a constraint for this? After all, you have access to the body’s position, and you have access to the mouse’s position. What’s wrong with assigning one to the other?
While this code will move the body, it will also have the unfortunate result of breaking the physics. Imagine you’ve built a teleportation machine that allows you to move instantly from your bedroom to your kitchen (good for late-night snacking). That’s easy enough to imagine, but now go ahead and rewrite Newton’s laws of motion to account for the possibility of teleportation. Not so easy anymore, is it?
@@ -874,7 +876,7 @@
Mouse Constraints
let { Mouse, MouseConstraint } = Matter;
// Need a reference to the p5.js canvas to listen for the mouse
let canvas = createCanvas(640, 240);
-// Create a Matter mouse attached to the native HTML5 canvas element.
+// Create a Matter mouse attached to the native HTML5 canvas element.
let mouse = Mouse.create(canvas.elt);
Next, use the mouse object to create a MouseConstraint:
In this example, you’ll see that the stiffness property of the constraint is set to 0.7, giving a bit of elasticity to the imaginary mouse string. Other properties such as angularStiffness and damping can also influence the mouse’s interaction. Play around with these values. What happens if you adjust the stiffness?
Adding More Forces
-
In Chapter 2, I covered how to build an environment with multiple forces at play. An object might respond to gravitational attraction, wind, air resistance, and so on. Clearly, forces are at work in Matter.js as rectangles and circles spin and fly around the screen! But so far, I’ve demonstrated how to manipulate only a single global force gravity:
+
In Chapter 2, I covered how to build an environment with multiple forces at play. An object might respond to gravitational attraction, wind, air resistance, and so on. Clearly, forces are at work in Matter.js as rectangles and circles spin and fly around the screen! But so far, I’ve demonstrated how to manipulate only a single global force: gravity.
let engine = Engine.create();
// Change the engine’s gravity to point horizontally.
engine.gravity.x = 1;
@@ -908,11 +910,11 @@
Here, the Box class’s applyForce() method receives a force vector and simply passes it along to Matter.js’s applyForce() method to apply it to the corresponding body. The key difference with this approach is that Matter.js is a more sophisticated engine than the examples from Chapter 2. The earlier examples assumed that the force was always applied at the mover’s center. Here, I’ve specified the exact position on the body where the force is applied. In this case, I’ve just applied it to the center as before by asking the body for its position, but this could be adjusted. For example, a force pushing at the edge of a box, causing it to spin across the canvas, much like dice tumbling when thrown.
+
Here, the Box class’s applyForce() method receives a force vector and simply passes it along to Matter.js’s applyForce() method to apply it to the corresponding body. The key difference with this approach is that Matter.js is a more sophisticated engine than the examples from Chapter 2. The earlier examples assumed that the force was always applied at the mover’s center. Here, I’ve specified the exact position on the body where the force is applied. In this case, I’ve just applied it to the center as before by asking the body for its position, but this could be adjusted—for example, a force pushing at the edge of a box, causing it to spin across the canvas, much like dice tumbling when thrown.
How can I bring forces into a Matter.js-driven sketch? Say I want to use a gravitational attraction force. Remember the code from Example 2.6 in the Attractor class?
attract(mover) {
let force = p5.Vector.sub(this.position, mover.position);
@@ -940,12 +942,12 @@
Example 6.9: Attraction with Matter
}
attract(mover) {
- //{!2} The attract method now uses Matter.js vector functions.
+ //{!2} The attract() method now uses Matter.js vector functions.
let force = Vector.sub(this.body.position, mover.body.position);
let distance = Vector.magnitude(force);
distance = constrain(distance, 5, 25);
- //{!1} Use a small value for G keep the system stable.
+ //{!1} Use a small value for G to keep the system stable.
let G = 0.02;
//{!1} The mover’s mass is included here, but the attractor’s mass is left out since, as a static body, it is equivalent to infinity.
let strength = (G * mover.body.mass) / (distance * distance);
@@ -969,7 +971,7 @@
Example 6.9: Attraction with Matter
this.body = Bodies.circle(x, y, this.radius, options);
}
-
This is also a good time to revisit to the concept of mass. Although I’m accessing the mass property of the body associated with the mover in the attract() method, I never explicitly set it. In Matter.js, the mass of a body is automatically calculated based on its size (area) and density. Larger bodies will therefore have a greater mass. To increase the mass relative to the size, you can try setting a density property in the options object (the default is 0.001). For static bodies, such as the attractor, the mass is considered infinite. This is how the attractor stays locked in position despite the movers continuously knocking into it.
+
This is also a good time to revisit the concept of mass. Although I’m accessing the mass property of the body associated with the mover in the attract() method, I never explicitly set it. In Matter.js, the mass of a body is automatically calculated based on its size (area) and density. Larger bodies will therefore have a greater mass. To increase the mass relative to the size, you can try setting a density property in the options object (the default is 0.001). For static bodies, such as the attractor, the mass is considered infinite. This is how the attractor stays locked in position despite the movers continuously knocking into it.
Exercise 6.7
Incorporate Body.applyForce() into a new spin() method for Example 6.7’s Windmill class to simulate a motor continuously rotating the windmill.
@@ -994,7 +996,7 @@
Collision Events
}
The global mousePressed() function in p5.js is executed whenever the mouse is clicked. This is known as a callback, a function that’s called back at a later time when an event occurs. Matter.js collision events operate in a similar fashion. Instead of p5.js just knowing to look for a function called mousePressed() when a mouse event occurs, however, you have to explicitly define the name for a Matter.js collision callback:
This code specifies that a function named handleCollisions should be executed whenever a collision between two bodies starts. Matter.js also has events for 'collisionActive' (executed over and over for the duration of an ongoing collision) and 'collisionEnd'(executed when two bodies stop colliding), but for a basic demonstration, knowing when the collision begins is more than adequate.
+
This code specifies that a function named handleCollisions should be executed whenever a collision between two bodies starts. Matter.js also has events for 'collisionActive' (executed over and over for the duration of an ongoing collision) and 'collisionEnd' (executed when two bodies stop colliding), but for a basic demonstration, knowing when the collision begins is more than adequate.
Just as mousePressed() is triggered when the mouse is clicked, handleCollisions() (or whatever you choose to name the callback function) is triggered when two shapes collide. It can be written as follows:
function handleCollisions(event) {
@@ -1006,15 +1008,15 @@
Collision Events
for (let pair of event.pairs) {
}
-
Step 2: Pair, could tell me which two bodies you include?
+
Step 2: Pair, could you tell me which two bodies you include?
Each pair in the pairs array is an object with references to the two bodies involved in the collision, bodyA and bodyB. I’ll extract those bodies:
for (let pair of event.pairs) {
let bodyA = pair.bodyA;
let bodyB = pair.bodyB;
}
Step 3: Bodies, could you tell me which particles you’re associated with?
-
Getting from the relevant Matter.js bodies to the Particle objects they’re associated with is a little harder. After all, Matter.js doesn’t know anything about my code. Sure, it’s doing all sorts of stuff to keep track of the relationships between bodies and constraints, but it’s up to me to manage my own objects and their associations with Matter.js elements. That said, every Matter.js body is instantiated with an empty object— { }— called plugin, ready to store any custom data about that body. I can link the body to a custom object (in this case, a Particle) by storing a reference to that object in the plugin property.
-
Take a look at the updated constructor in the Particle class where the body is made. Note that the body-making procedure has been expanded by one line of code to add a particle property inside plugin. It’s important to make sure you’re adding a new property to the existing plugin object (in this case, plugin.particle = this) rather than overwriting the pluginobject (for example, with plugin = this). The latter could interfere with other features or customizations.
+
Getting from the relevant Matter.js bodies to the Particle objects they’re associated with is a little harder. After all, Matter.js doesn’t know anything about my code. Sure, it’s doing all sorts of stuff to keep track of the relationships between bodies and constraints, but it’s up to me to manage my own objects and their associations with Matter.js elements. That said, every Matter.js body is instantiated with an empty object—{ }—called plugin, ready to store any custom data about that body. I can link the body to a custom object (in this case, a Particle) by storing a reference to that object in the plugin property.
+
Take a look at the updated constructor in the Particle class where the body is made. Note that the body-making procedure has been expanded by one line of code to add a particle property inside plugin. It’s important to make sure you’re adding a new property to the existing plugin object (in this case, plugin.particle = this) rather than overwriting the plugin object (for example, with plugin = this). The latter could interfere with other features or customizations.
class Particle {
@@ -1022,7 +1024,7 @@
Collision Events
this.radius = radius;
this.body = Bodies.circle(x, y, this.radius);
- //{!1 .bold} “this” refers to this Particle object, telling the Matter.js Body to store a
+ //{!1 .bold} this refers to this Particle object, telling the Matter.js Body to store a
// reference to this particle that can be accessed later.
this.body.plugin.particle = this;
@@ -1042,7 +1044,7 @@
Example 6.10: Collision Events
let bodyA = pair.bodyA;
let bodyB = pair.bodyB;
- //{!2} Retrieve the particles associated with the colliding bodies via the plugin.
+ //{!2} Retrieve the particles associated with the colliding bodies via the plugin.
let particleA = bodyA.plugin.particle;
let particleB = bodyB.plugin.particle;
@@ -1053,14 +1055,14 @@
Example 6.10: Collision Events
}
}
}
-
In most cases, you can’t assume that the objects that collided are all Particle objects. After all, the particle might have collided with a Boundary object (another other kind of thing, depending on what’s in your world). You can check an object’s type with JavaScript’s instanceof operator, as I’ve done in this example.
+
In most cases, you can’t assume that the objects that collided are all Particle objects. After all, the particle might have collided with a Boundary object (another kind of thing, depending on what’s in your world). You can check an object’s type with JavaScript’s instanceof operator, as I’ve done in this example.
Exercise 6.9
Create a simulation in which Particle objects disappear when they collide with one another. Where and how should you delete the particles? Can you have them shatter into smaller particles?
A Brief Interlude: Integration Methods
Has this ever happened to you? You’re at a fancy cocktail party, regaling your friends with tall tales of your incredible software physics simulations. Suddenly, out of the blue, someone pipes up: “Enchanting! But what integration method are you using?”
-
“What?!” you think to yourself. “Integration?”
+
What?! you think to yourself. Integration?
Maybe you’ve heard the term before. Along with differentiation, it’s one of the two main operations in calculus. Oh right, calculus.
I’ve managed to get most of the way through this material related to physics simulation without really needing to dive into calculus. As I wrap up the first half of this book, however, it’s worth taking a moment to examine the calculus behind what I’ve been demonstrating and how it relates to the methodology in certain physics libraries (like Box2D, Matter.js, and the upcoming Toxiclibs.js). This way, you’ll know what to say at the next cocktail party when someone asks you about integration.
I’ll begin with a question: “What does integration have to do with position, velocity, and acceleration?” To answer, I should first define differentiation, the process of finding a derivative. The derivative of a function is a measure of how a function changes over time. Consider position and its derivative. Position is a point in space, while velocity is the change in position over time. Therefore, velocity can be described as the derivative of position. And what’s acceleration? The change in velocity over time. Acceleration is the derivative of velocity.
@@ -1084,10 +1086,10 @@
Verlet Physics with Toxiclibs.js
Toxiclibs is an independent, open source library collection for computational design tasks with Java and Processing developed by Karsten “toxi” Schmidt. The classes are purposefully kept fairly generic in order to maximize reuse in different contexts ranging from generative design, animation, interaction/interface design, data visualization to architecture and digital fabrication, use as teaching tool and more.
-
Schmidt continues to contribute to the creative coding field through his recent project, thi.ng umbrella. This work can be considered an indirect successor to Toxiclibs, but with a much greater scope and detail. If you like this book, you might especially enjoy exploring thi.ng vectors, which provides more than 800 vector algebra functions using plain vanilla JavaScript arrays.
-
While thi.ng/umbrella may be a more modern and sophisticated approach, Toxiclibs remains a versatile tool, and I continue to use a version compatible with the latest version of Processing (4.1 as of the time of this writing). For this book, we should thank our lucky stars for Toxiclibs.js, a JavaScript adaptation of the library, created by Kyle Phillips (hapticdata). I’m going to cover only a few examples related to Verlet physics, but Toxiclibs.js includes a suite of other packages with functionality related to color, geometry, math, and more.
+
Schmidt continues to contribute to the creative coding field through his recent project, thi.ng umbrella. This work can be considered an indirect successor to Toxiclibs, but with a much greater scope and detail. If you like this book, you might especially enjoy exploring thi.ng vectors, which provides more than 800 vector algebra functions using plain-vanilla JavaScript arrays.
+
While thi.ng/umbrella may be a more modern and sophisticated approach, Toxiclibs remains a versatile tool, and I continue to use a version compatible with the latest version of Processing (4.3 as of the time of this writing). For this book, we should thank our lucky stars for Toxiclibs.js, a JavaScript adaptation of the library, created by Kyle Phillips (hapticdata). I’m going to cover only a few examples related to Verlet physics, but Toxiclibs.js includes a suite of other packages with functionality related to color, geometry, math, and more.
The examples I’m about to demonstrate could also be created using Matter.js, but I’ve decided to move to Toxiclibs.js for several reasons. The library holds a special place in my heart as a personal favorite, and it’s historically significant. I also believe that showing more than one physics library is important for providing a broader understanding of the tools and approaches available.
-
This switch from Matter.js to Toxiclibs.js raises an important question, though: how should you decide which library to use for a project? Matter.js, or Toxiclibs.js, or something else? If you fall into one of the following two categories, your decision is a bit easier:
+
This switch from Matter.js to Toxiclibs.js raises an important question, though: How should you decide which library to use for a project? Matter.js, or Toxiclibs.js, or something else? If you fall into one of the following two categories, your decision is a bit easier:
My project involves collisions. I have circles, squares, and other strangely shaped objects that knock each other around and bounce off each other. In this case, you’re going to want to use Matter.js, since Toxiclibs.js doesn’t handle rigid-body collisions.
My project involves lots of particles flying around the screen. Sometimes they attract each other. Sometimes they repel each other. And sometimes they’re connected with springs. In this case, Toxiclibs.js is likely your best choice. It’s simpler to use in some ways than Matter.js and particularly well suited to connected systems of particles. It’s also high performance, because it gets to ignore all of the collision geometry.
@@ -1118,7 +1120,7 @@
Verlet Physics with Toxiclibs.js
Yes
-
Spring connections (force-based)
+
Spring connections (force based)
Yes
Yes
@@ -1174,7 +1176,7 @@
Verlet Physics with Toxiclibs.js
-
I’ll discuss how some of these features translate to Toxclibs.js, before putting them together to create some interesting examples.
+
I’ll discuss how some of these features translate to Toxiclibs.js before putting them together to create some interesting examples.
Vectors
Here we go again. Remember all that time spent learning the ins and outs of the p5.Vector class? Then remember how you had to revisit all those concepts with Matter.js and the Matter.Vector class? Well, it’s time to do it again, because Toxiclibs.js also includes its own vector classes. It has one for two dimensions and one for three: Vec2D and Vec3D. Both are found in the toxi.geom package and can be aliased in the same manner as Vector with Matter.js:
Looking over this code, you might first notice that drawing the particle is as simple as grabbing the x and y properties and using them with circle(). Second, you might notice that this Particle class doesn’t do much beyond storing a reference to a VerletParticle2D object. This hints at something important. Think back to the discussion of inheritance in Chapter 4, and then ask yourself: what is a Particle object other than an augmented VerletParticle2D object? Why bother making two objects—a Particle and a VerletParticle2D—for every one particle in the world, when I could simply extend the VerletParticle2D class to include the extra code needed to draw the particle?
+
Looking over this code, you might first notice that drawing the particle is as simple as grabbing the x and y properties and using them with circle(). Second, you might notice that this Particle class doesn’t do much beyond storing a reference to a VerletParticle2D object. This hints at something important. Think back to the discussion of inheritance in Chapter 4, and then ask yourself: What is a Particle object other than an augmented VerletParticle2D object? Why bother making two objects—a Particle and a VerletParticle2D—for every one particle in the world, when I could simply extend the VerletParticle2D class to include the extra code needed to draw the particle?
class Particle extends VerletParticle2D {
constructor(x, y, r) {
//{!1} Call super() with (x, y) so the object is initialized properly.
@@ -1312,7 +1314,7 @@
Springs
VerletSpring2D: A springy connection between two particles. The spring’s properties can be configured in such a way as to create a stiff, stick-like connection or a highly elastic, stretchy connection. A particle can also be locked so that only one end of the spring can move.
VerletConstrainedSpring2D: A spring whose maximum distance can be limited. This can help the whole spring system achieve better stability.
-
VerletMinDistanceSpring2D: A spring that enforces its rest length only if the current distance is less than its rest length. This is handy if you want to ensure that objects are at least a certain distance from each other, but don’t care if the distance is bigger than the enforced minimum.
+
VerletMinDistanceSpring2D: A spring that enforces its rest length only if the current distance is less than its rest length. This is handy if you want to ensure that objects are at least a certain distance from each other, but you don’t care if the distance is bigger than the enforced minimum.
Inheritance and polymorphism once again prove to be useful when making springs. A spring expects two VerletParticle2D objects when it’s created, but as before, two Particle objects will do, since Particle extendsVerletParticle2D.
Here’s some example code to create a spring. This snippet assumes the existence of two particles, particle1 and particle2, and creates a connection between them with a given rest length and strength:
@@ -1325,7 +1327,7 @@
Springs
physics.addSpring(spring);
I have almost everything I need to build a simple first Toxiclibs.js example: two particles connected to form a springy pendulum. I want to add one more element, however: mouse interactivity.
With Matter.js, I explained that the physics simulation breaks down if you manually override a body’s position by setting it to the mouse. With Toxiclibs.js, this isn’t a problem. If I want to, I can set a particle’s (x, y) position manually. However, before doing so, it’s generally a good idea to call the particle’s lock() method, which fixes the particle in place. This is identical to setting the isStatic property to true in Matter.js.
-
The idea is to lock the particle temporarily so it stops responding to the world’s physics, alter its position, and then unlock it (with the unlock() method) so it can start moving again from its new location. For example, say I want to reposition a particle whenever the mouse is clicked.
+
The idea is to lock the particle temporarily so it stops responding to the world’s physics, alter its position, and then unlock it (with the unlock() method) so it can start moving again from its new location. For example, say I want to reposition a particle whenever the mouse is clicked:
if (mouseIsPressed) {
//{!4} First lock the particle, then set the x and y, then unlock() it.
particle1.lock();
@@ -1368,14 +1370,14 @@
Example 6.11: Simple Spring
// Create one spring.
let spring = new VerletSpring2D(particle1, particle2, length, 0.01);
- //{!3} Must add everything to the world.
+ //{!3} Must add everything to the world
physics.addParticle(particle1);
physics.addParticle(particle2);
physics.addSpring(spring);
}
function draw() {
- //{!1} Must update the physics.
+ //{!1} Must update the physics
physics.update();
background(255);
@@ -1427,7 +1429,7 @@
A String
Figure 6.14: Twenty particles all spaced 10 pixels apart
-
I can loop from i equals 0 all the way up to total, creating new particles and setting each one’s y position set to i * 10. The first particle is at (0, 10), the second at (0, 20, the third at (0, 30), and so on:
+
I can loop from i equals 0 all the way up to total, creating new particles and setting each one’s y position to i * 10. The first particle is at (0, 10), the second at (0, 20), the third at (0, 30), and so on:
for (let i = 0; i < total; i++) {
//{!1} Space them out along the x-axis.
let particle = new Particle(i * length, 10, 4);
@@ -1436,16 +1438,16 @@
A String
//{!1} Add the particle to the array.
particles.push(particle);
}
-
Even though it’s redundant, I’m adding the particles to both the Toxiclibs.js physics world and to the particles array. This will help me manage the sketch (especially when I might have more than one string of particles).
+
Even though it’s redundant, I’m adding the particles to both the Toxiclibs.js physics world and the particles array. This will help me manage the sketch (especially when I might have more than one string of particles).
Now for the fun part: it’s time to connect all the particles. Particle index 0 will be connected to particle 1, particle 1 to particle 2, 2 to 3, 3 to 4, and so on (see Figure 6.15).
Figure 6.15: Each particle is connected to the next particle in the array.
In other words, particle i needs to be connected to particle i+1 (except for when i represents the last element of the array):
-
// The loop stops before the last element (total – 1).
+
// The loop stops before the last element (total – 1).
for (let i = 0; i < total - 1; i++) {
- // The spring connects particle i to i + 1.
+ // The spring connects particle i to i + 1.
let spring = new VerletSpring2D(particles[i], particles[i + 1], spacing, 0.01);
//{!1} The spring must also be added to the world.
physics.addSpring(spring);
@@ -1492,7 +1494,7 @@
A Soft-Body Character
Figure 6.16: A skeleton for a soft-body character. The vertices are numbered according to their positions in an array.
-
Creating the particles is the easy part; it’s exactly the same as before! I’d like to make one change, though. Rather than having the setup() function add the particles and springs to the physics world, I’ll incorporate this responsibility into the Particle constructor.
+
Creating the particles is the easy part; it’s exactly the same as before! I’d like to make one change, though. Rather than having the setup() function add the particles and springs to the physics world, I’ll incorporate this responsibility into the Particle constructor:
class Particle extends VerletParticle2D {
constructor(x, y, r) {
super(x, y);
@@ -1507,7 +1509,7 @@
A Soft-Body Character
circle(this.x, this.y, this.r * 2);
}
}
-
While it’s not strictly necessary, I’d also like to make a Spring class that inherits its functionality from VerletSpring2D. For this example, I want the resting length of the spring to always be equal to the distance between the skeleton’s particles when they’re first created. Additionally, I’m keeping the implementation simple here by hardcoding a uniform strength value of 0.01 in the Spring constructor. You may want to enhance the example with a more sophisticated design that sets varying degrees of springiness to the different parts of the soft-body character..
+
While it’s not strictly necessary, I’d also like to make a Spring class that inherits its functionality from VerletSpring2D. For this example, I want the resting length of the spring to always be equal to the distance between the skeleton’s particles when they’re first created. Additionally, I’m keeping the implementation simple here by hardcoding a uniform strength value of 0.01 in the Spring constructor. You may want to enhance the example with a more sophisticated design that sets varying degrees of springiness to the different parts of the soft-body character.
class Spring extends VerletSpring2D {
// The constructor receives two particles as arguments.
constructor(a, b) {
@@ -1656,7 +1658,7 @@
A Force-Directed Graph
No particle is connected to itself. That is, 0 isn’t connected to 0, 1 isn’t connected to 1, and so on.
Connections aren’t repeated in reverse. For example, if 0 is connected to 1, I don’t need to explicitly say that 1 is also connected to 0. I already know this, based on the definition of how a spring works!
-
How do I write the code to make these connections for N particles? Look at the left column of the table of connections. It reads: 000112. This tells me that I need to access each particle in the list from 0 to N – 1:
+
How do I write the code to make these connections for N particles? Look at the four columns illustrated in Figure 6.19. They iterate all the connections starting from particles 0 up to 3. This tells me that I need to access each particle in the list from 0 to N – 1:
for (let i = 0; i < this.particles.length - 1; i++) {
// Use the variable particle_i to store the particle reference.
@@ -1670,7 +1672,7 @@
A Force-Directed Graph
For every pair of particles i and j, I can then create a spring. I’ll go back to using VerletSpring2D directly, but you could also incorporate a custom Spring class:
-
//{!1} The spring connects particles i and j.
+
//{!1} The spring connects particles i and j.
physics.addSpring(new VerletSpring2D(particle_i, particle_j, length, 0.01));
}
}
@@ -1716,7 +1718,7 @@
Exercise 6.13
Attraction and Repulsion Behaviors
When it came time to create an attraction example for Matter.js, I showed how the Matter.Body class includes an applyForce() method. All I then needed to do was calculate the attraction force F_g = (G \times m_1 \times m_2) \div d^2 as a vector and apply it to the body. Similarly, the Toxiclibs.js VerletParticle2D class also includes a method called addForce() that can apply any calculated force to a particle.
-
However, Toxiclibs.js takes this idea one step further by offering built-in functionality for common forces (called behaviors) such as attraction! For example, if you add anAttractionBehavior object to a particular VerletParticle2D object, all other particles in the physics world will experience an attraction force toward that particle.
+
However, Toxiclibs.js takes this idea one step further by offering built-in functionality for common forces (called behaviors) such as attraction! For example, if you add an AttractionBehavior object to a particular VerletParticle2D object, all other particles in the physics world will experience an attraction force toward that particle.
Say I create an instance of my Particle class (which extends the VerletParticle2D class):
let particle = new Particle(320, 120);
Now I can create an AttractionBehavior associated with that particle:
Just as discussed in "Spatial Subdivisions" on page XX, Toxiclibs.js projects with large numbers of particles interacting with one another can run very slow because of the N^2 nature of the algorithm (every particle checking every other particle). To speed up the simulation, you could use the manual addForce()method in conjunction with a binning algorithm. Keep in mind, this would also require you to manually calculate the attraction force, as the built-in AttractionBehavior would no longer apply.
+
Just as discussed in “Spatial Subdivisions” on page XX, Toxiclibs.js projects with large numbers of particles interacting with one another can run very slowly because of the N^2 nature of the algorithm (every particle checking every other particle). To speed up the simulation, you could use the manual addForce() method in conjunction with a binning algorithm. Keep in mind, this would also require you to manually calculate the attraction force, as the built-in AttractionBehavior would no longer apply.
Exercise 6.14
Use AttractionBehavior in conjunction with spring forces.
The Ecosystem Project
-
Take your system of creatures from step 5 and use a physics engine to drive their motion and behaviors. Here are some possibilities:
+
Take your system of creatures from Chapter 5 and use a physics engine to drive their motion and behaviors. Here are some possibilities:
Use Matter.js to allow collisions between creatures. Consider triggering an event when two creatures collide.
Use Matter.js to augment the design of your creatures. Build a skeleton with distance joints or make appendages with revolute joints.
Originating from the Akan people of Ghana, kente cloth is a woven fabric celebrated for its vibrant colors and intricate patterns. Woven in narrow strips, each design is unique, and when joined, the strips form a tapestry of complex and emergent patterns that tell a story or carry a message. The image shows three typical Ewe kente stripes, highlighting the diverse weaving traditions that reflect the rich cultural tapestry of Ghana.
-
In Chapter 5, I defined a complex system as a network of elements with short-range relationships, operating in parallel, that exhibit emergent behavior. I created a flocking simulation to demonstrate how a complex system adds up to more than the sum of its parts. In this chapter, I’m going to turn to developing other complex systems known as cellular automata.
+
In Chapter 5, I defined a complex system as a network of elements with short-range relationships, operating in parallel, that exhibit emergent behavior. I created a flocking simulation to demonstrate how a complex system adds up to more than the sum of its parts. In this chapter, I’m going to turn to developing other complex systems known as cellular automata.
In some respects, this shift may seem like a step backward. No longer will the individual elements of my systems be members of a physics world, driven by forces and vectors to move around the canvas. Instead, I’ll build systems out of the simplest digital element possible: a single bit. This bit is called a cell, and its value (0 or 1) is called its state. Working with such simple elements will help reveal how complex systems operate, and will offer an opportunity to elaborate on some programming techniques that apply to code-based projects. Building cellular automata will also set the stage for the rest of the book, where I’ll increasingly focus on systems and algorithms rather than vectors and motion—albeit systems and algorithms that I can and will apply to moving bodies.
What Is a Cellular Automaton?
A cellular automaton (cellular automata plural, or CA for short) is a model of a system of cell objects with the following characteristics:
@@ -43,23 +43,23 @@
Elementary Cellular Automata
States
Neighborhood
-
The simplest grid would be 1D: a line of cells (Figure 7.2).
+
The simplest grid would be 1D: a line of cells (Figure 7.2).
Figure 7.2: A 1D line of cells
-
The simplest set of states (beyond having only one state) would be two states: 0 or 1 (Figure 7.3). Perhaps the initial states are set randomly.
+
The simplest set of states (beyond having only one state) would be two states: 0 or 1 (Figure 7.3). Perhaps the initial states are set randomly.
-
- Figure 7.3: A 1D line of cells marked with states 0 or 1. What familiar programming data structure could represent this sequence?
+
+ Figure 7.3: A 1D line of cells marked with state 0 or 1. What familiar programming data structure could represent this sequence?
-
The simplest neighborhood in one dimension for any given cell would be the cell itself and its two adjacent neighbors: one to the left and one to the right (Figure 7.4). I’ll have to decide what I want to do with the cells on the left and right edges, since those have only one neighbor each, but I can sort out this detail later.
+
The simplest neighborhood in one dimension for any given cell would be the cell itself and its two adjacent neighbors: one to the left and one to the right (Figure 7.4). I’ll have to decide what I want to do with the cells on the left and right edges, since those have only one neighbor each, but I can sort out this detail later.
Figure 7.4: A neighborhood in one dimension is three cells.
I have a line of cells, each with an initial state, and each with two neighbors. The exciting thing is, even with this simplest CA imaginable, the properties of complex systems can emerge. But I haven’t yet discussed perhaps the most important detail of how CA work: change over time.
-
I’m not talking about real-world time here, but rather about the CA developing across a series of discrete time steps, which could also be called generations. In the case of a CA in p5.js, time will likely be tied to the frame count of the animation. The question, as depicted in Figure 7.5, is this: given the states of the cells at time equals 0 (or generation 0), how do I compute the states for all cells at generation 1? And then how do I get from generation 1 to generation 2? And so on and so forth.
+
I’m not talking about real-world time here, but rather about the CA developing across a series of discrete time steps, which could also be called generations. In the case of a CA in p5.js, time will likely be tied to the frame count of the animation. The question, as depicted in Figure 7.5, is this: Given the states of the cells at time equals 0 (or generation 0), how do I compute the states for all cells at generation 1? And then how do I get from generation 1 to generation 2? And so on and so forth.
Figure 7.5: The states for generation 1 are calculated using the states of the cells from generation 0.
@@ -77,12 +77,12 @@
Elementary Cellular Automata
Figure 7.7: Counting with 3 bits in binary, or the eight possible configurations of a three-cell neighborhood
-
Once all the possible neighborhood configurations are defined, an outcome (new state value: 0 or 1) is specified for each configuration. In Wolfram's original notation and other common references, these configurations are written in descending order. Figure 7.8 follows this convention, starting with 111 and counting down to 000.
+
Once all the possible neighborhood configurations are defined, an outcome (new state value: 0 or 1) is specified for each configuration. In Wolfram’s original notation and other common references, these configurations are written in descending order. Figure 7.8 follows this convention, starting with 111 and counting down to 000.
Figure 7.8: A ruleset shows the outcome for each possible configuration of three cells.
-
Keep in mind that unlike the sum or averaging methods, the rulesets in elementary CA don’t follow any arithmetic logic—they’re just arbitrary mappings of inputs to outputs. The input is the current configuration of the neighborhood (one of eight possibilities), and the output is the next state of the middle cell in the neighborhood (0 or 1—it’s up to you to define the rule).
+
Keep in mind that unlike the sum or averaging method, the rulesets in elementary CA don’t follow any arithmetic logic—they’re just arbitrary mappings of inputs to outputs. The input is the current configuration of the neighborhood (one of eight possibilities), and the output is the next state of the middle cell in the neighborhood (0 or 1—it’s up to you to define the rule).
Once you have a ruleset, you can set the CA in motion. The standard Wolfram model is to start generation 0 with all cells having a state of 0 except for the middle cell, which should have a state of 1. You can do this with any size (length) grid, but for clarity, I’ll use a 1D CA of nine cells so that the middle is easy to pick out (see Figure 7.9).
@@ -105,18 +105,18 @@
Elementary Cellular Automata
The low-resolution shape that emerges in Figure 7.12 is the Sierpiński triangle. Named after the Polish mathematician Wacław Sierpiński, it’s a famous example of a fractal. I’ll examine fractals more closely in Chapter 8, but briefly, they’re patterns in which the same shapes repeat themselves at different scales. To give you a better sense of this, Figure 7.13 shows the CA over several more generations and with a wider grid size.
-
- Figure 7.13: Wolfram elementary CA, rule 90
+
+ Figure 7.13: Wolfram elementary CA
And Figure 7.14 shows the CA again, this time with cells that are just a single pixel wide so the resolution is much higher.
-
- Figure 7.14: Wolfram elementary CA, rule 90, at higher resolution
+
+ Figure 7.14: Wolfram elementary CA at higher resolution
Take a moment to let the enormity of what you’ve just seen sink in. Using an incredibly simple system of 0s and 1s, with little neighborhoods of three cells, I was able to generate a shape as sophisticated and detailed as the Sierpiński triangle. This is the beauty of complex systems.
Of course, this particular result didn’t happen by accident. I picked the set of rules in Figure 7.8 because I knew the pattern it would generate. The mere act of defining a ruleset doesn’t guarantee visually exciting results. In fact, for a 1D CA in which each cell can have two possible states, there are exactly 256 possible rulesets to choose from, and only a handful are on par with the Sierpiński triangle. How do I know there are 256 possible rulesets? It comes down to a little more binary math.
Defining Rulesets
-
Take a look back at Figure 7.7 and notice again the eight possible neighborhood configurations, from 000 to 111. These are a ruleset’s inputs, and they remain constant from ruleset to ruleset. Only the outputs vary from one ruleset to another—the individual 0 or 1 paired with each neighborhood configuration. Figure 7.9 represented a ruleset entirely with 0s and 1s. Now Figure 7.15 shows the same ruleset visualized with white and black squares.
+
Take a look back at Figure 7.7 and notice again the eight possible neighborhood configurations, from 000 to 111. These are a ruleset’s inputs, and they remain constant from ruleset to ruleset. Only the outputs vary from one ruleset to another—the individual 0 or 1 paired with each neighborhood configuration. Figure 7.8 represented a ruleset entirely with 0s and 1s. Now Figure 7.15 shows the same ruleset visualized with white and black squares.
Figure 7.15: Representing the same ruleset (from Figure 7.8) with white and black squares
@@ -135,14 +135,14 @@
Defining Rulesets
-
- Figure 7.18: A textile cone snail (Conus textile), Cod Hole, Great Barrier Reef, Australia. (photograph by Richard Ling)
+
+ Figure 7.18: A textile cone snail (Conus textile), Cod Hole, Great Barrier Reef, Australia (photo by Richard Ling)
The result is a recognizable shape, though it certainly isn’t as exciting as the Sierpiński triangle. As I said earlier, most of the 256 elementary rulesets don’t produce compelling outcomes. However, it’s still quite incredible that even just a few of these rulesets—simple systems of cells with only two possible states—can produce fascinating patterns seen every day in nature. For example, Figure 7.18 shows a snail shell resembling Wolfram’s rule 30. This demonstrates how valuable CAs can be in simulation and pattern generation.
Before I go too far down the road of characterizing the results of different rulesets, though, let’s look at how to build a p5.js sketch that generates and visualizes a Wolfram elementary CA.
Programming an Elementary CA
-
You may be thinking: “Okay, I have this cell thing. And the cell thing has properties, like a state, what generation it’s on, who its neighbors are, and where it lives pixel-wise on the screen. And maybe it has functions, like to display itself and determine its new state.” This line of thinking is an excellent one and would likely lead you to write code like this:
+
You may be thinking, “Okay, I have this cell thing. And the cell thing has properties, like a state, what generation it’s on, who its neighbors are, and where it lives pixel-wise on the screen. And maybe it has functions, like to display itself and determine its new state.” This line of thinking is an excellent one and would likely lead you to write code like this:
class Cell {
}
@@ -193,12 +193,12 @@
Programming an Elementary CA
In fact, it’s not quite that easy. What have I done wrong? Think about how the code will execute. The first time through the loop, cell index i equals 0. The code wants to look at cell 0’s neighbors. Left is i - 1 or -1. Oops! An array by definition doesn’t have an element with an index of -1. It starts with 0.
I alluded to this problem of edge cases earlier in the chapter and said I could worry about it later. Well, later is now. How should I handle the cell on the edge that doesn’t have a neighbor to both its left and its right? Here are three possible solutions to this problem:
-
Edges remain constant. This is perhaps the simplest solution. Don’t bother to evaluate the edges and always leave their state value constant (0 or 1).
+
Edges remain constant. This is perhaps the simplest solution. Don’t bother to evaluate the edges, and always leave their state value constant (0 or 1).
Edges wrap around. Think of the CA as a strip of paper, and turn that strip of paper into a ring. The cell on the left edge is a neighbor of the cell on the right edge, and vice versa. This can create the appearance of an infinite grid and is probably the most commonly used solution.
Edges have different neighborhoods and rules. If I wanted to, I could treat the edge cells differently and create rules for cells that have a neighborhood of two instead of three. You may want to do this in some circumstances, but in this case, it’s going to be a lot of extra lines of code for little benefit.
To make the code easiest to read and understand right now, I’ll go with option 1 and skip the edge cases, leaving the values constant. This can be accomplished by starting the loop one cell later and ending it one cell earlier:
-
//{.bold} A loop that ignores the first and last cell
+
//{.bold} A loop that ignores the first and last cells
for (let i = 1; i < cells.length - 1; i++) {
let left = cells[i - 1];
let middle = cells[i];
@@ -233,7 +233,7 @@
Programming an Elementary CA
//{.bold} The new generation becomes the current generation.
cells = newcells;
I’m almost done, but I still need to define rules(), the function that computes the new state value based on the neighborhood (left, middle, and right cells). I know the function needs to return an integer (0 or 1), as well as receive three arguments (for the three neighbors):
-
//{!1} Function signature: receives 3 ints and returns 1
+
//{!1} Function signature: receives 3 values and returns 1
function rules(a, b, c) { return _______ }
I could write this function in many ways, but I’d like to start with a long-winded one that will hopefully provide a clear illustration of what’s happening. How shall I store the ruleset? Remember that a ruleset is a series of 8 bits (0 or 1) that define the outcome for every possible neighborhood configuration. If you need a refresher, Figure 7.20 shows the Wolfram notation for the Sierpiński triangle ruleset, along with the corresponding 0s and 1s listed in order. This should give you a hint as to the data structure I have in mind!
@@ -255,7 +255,7 @@
Programming an Elementary CA
else if (a === 0 && b === 0 && c === 1) return ruleset[6];
else if (a === 0 && b === 0 && c === 0) return ruleset[7];
}
-
I like writing the rules() function this way because it describes line by line exactly what’s happening for each neighborhood configuration. However, it’s not a great solution. After all, what if a CA has four possible states (0 through 3) instead of two? Suddenly there are 64 possible neighborhood configurations. And with 10 possible states, 1,000 configurations. And just imagine programming von Neumann’s 29 possible states. I’d be stuck typing out thousands upon thousands of else if statements!
+
I like writing the rules() function this way because it describes line by line exactly what’s happening for each neighborhood configuration. However, it’s not a great solution. After all, what if a CA has four possible states (0 through 3) instead of two? Suddenly there are 64 possible neighborhood configurations. And with 10 possible states, 1,000 configurations. And just imagine programming von Neumann’s 29 possible states. I’d be stuck typing out thousands upon thousands of else...if statements!
Another solution, though not quite as transparent, is to convert the neighborhood configuration (a 3-bit number) into a regular integer and use that value as the index into the ruleset array. This can be done as follows, using JavaScript’s built-in parseInt() function:
function rules(a, b, c) {
// A quick way to concatenate three numbers into a string
@@ -282,7 +282,7 @@
Programming an Elementary CA
let ruleset = [0, 1, 0, 1, 1, 0, 1, 0];
function setup() {
- //{!3} All cells start with state 0. . .
+ //{!3} All cells start with state 0 . . .
for (let i = 0; i < width; i++) {
cells[i] = 0;
}
@@ -315,7 +315,7 @@
Drawing an Elementary CA
Figure 7.21: Rule 90 visualized as a stack of generations
-
First, this visual interpretation of the data is completely literal. It’s useful for demonstrating the algorithms and results of Wolfram’s elementary CA, but it shouldn’t necessarily drive your own personal work. You’re not likely building a project that needs precisely this algorithm with this visual style. So while learning to draw a CA in this way will help you understand and implement CA systems, this skill should exist only as a foundation.
+
First, this visual interpretation of the data is completely literal. It’s useful for demonstrating the algorithms and results of Wolfram’s elementary CA, but it shouldn’t necessarily drive your own personal work. You’re not likely building a project that needs precisely this algorithm with this visual style. So, while learning to draw a CA in this way will help you understand and implement CA systems, this skill should exist only as a foundation.
Second, the fact that a 1D CA is visualized with a 2D image can be misleading. It’s very important to remember that this is not a 2D CA. I’m simply choosing to show a history of all the generations stacked vertically. This technique creates a 2D image out of many instances of 1D data, but the system itself is 1D. Later, I’ll show you an actual 2D CA (the Game of Life), and I’ll cover how to visualize such a system.
The good news is that drawing an elementary CA isn’t particularly difficult. I’ll begin by rendering a single generation. Let’s say each cell should be a 10\times10 square:
let w = 10;
@@ -409,7 +409,7 @@
Exercise 7.4
Create a visualization of the CA that scrolls upward as the generations increase so that you can view the generations to “infinity.” Hint: Instead of keeping track of one generation at a time, you’ll need to store a history of generations, always adding a new one and deleting the oldest one in each frame.
Wolfram Classification
-
Now that you have a sketch for visualizing an elementary CA, you can supply it whatever ruleset you want and see the results. What kind of outcomes can you expect? As I noted earlier, the vast majority of elementary CA rulesets produce visually uninspiring results, while some result in wondrously complex patterns like those found in nature. Wolfram has divided the range of outcomes into four classes.
+
Now that you have a sketch for visualizing an elementary CA, you can supply it whatever ruleset you want and see the results. What kinds of outcomes can you expect? As I noted earlier, the vast majority of elementary CA rulesets produce visually uninspiring results, while some result in wondrously complex patterns like those found in nature. Wolfram has divided the range of outcomes into four classes.
Class 1: Uniformity
Class 1 CAs end up, after a certain number of generations, with every cell constant. This isn’t terribly exciting to watch. Rule 222 is a class 1 CA; if you run it for enough generations, every cell will eventually become and remain black (see Figure 7.22).
@@ -429,12 +429,12 @@
Class 3: Random
Figure 7.24: Rule 30
Class 4: Complexity
-
Class 4 CAs can be thought of as a mix between class 2 and class 3. You can find repetitive, oscillating patterns inside the CA, but where and when these patterns appear is unpredictable and seemingly random. If a class 3 CA wowed you, then a class 4 like rule 110 should really blow your mind (Figure 7.25)!
+
Class 4 CAs can be thought of as a mix between class 2 and class 3. You can find repetitive, oscillating patterns inside the CA, but where and when these patterns appear is unpredictable and seemingly random. If a class 3 CA wowed you, then a class 4 like rule 110 (Figure 7.25) should really blow your mind!
Figure 7.25: Rule 110
-
In Chapter 5, I introduced the concept of a complex system and used flocking to demonstrate how simple rules can result in emergent behaviors. Class 4 CA remarkably exhibit the characteristics of complex systems and are the key to simulating phenomena such as forest fires, traffic patterns, and the spread of diseases. Research and applications of CA consistently emphasize the importance of class 4 as the bridge between CA and nature.
+
In Chapter 5, I introduced the concept of a complex system and used flocking to demonstrate how simple rules can result in emergent behaviors. Class 4 CAs remarkably exhibit the characteristics of complex systems and are the key to simulating phenomena such as forest fires, traffic patterns, and the spread of diseases. Research and applications of CA consistently emphasize the importance of class 4 as the bridge between CA and nature.
The Game of Life
The next step is to move from a 1D CA to a 2D one: the Game of Life. This will introduce additional complexity—each cell will have a bigger neighborhood—but with the complexity comes a wider range of possible applications. After all, most of what happens in computer graphics lives in two dimensions, and this chapter demonstrates how to apply CA thinking to a 2D p5.js canvas.
In 1970, Martin Gardner wrote a Scientific American article that documented mathematician John Conway’s new Game of Life, describing it as recreational mathematics: “To play life you must have a fairly large checkerboard and a plentiful supply of flat counters of two colors. It is possible to work with pencil and graph paper but it is much easier, particularly for beginners, to use counters and a board.”
@@ -458,7 +458,7 @@
The Rules of the Game
Figure 7.26: A 2D CA showing the neighborhood of nine cells
-
With three cells, a 3-bit number had eight possible configurations. With nine cells, there are 9 bits, or 512 possible neighborhoods. In most cases, defining an outcome for every single possibility would be impractical. The Game of Life gets around this problem by defining a set of rules according to general characteristics of the neighborhood: is the neighborhood overpopulated with life, surrounded by death, or just right? Here are the rules of life:
+
With three cells, a 3-bit number had eight possible configurations. With nine cells, there are 9 bits, or 512 possible neighborhoods. In most cases, defining an outcome for every single possibility would be impractical. The Game of Life gets around this problem by defining a set of rules according to general characteristics of the neighborhood: Is the neighborhood overpopulated with life, surrounded by death, or just right? Here are the rules of life:
Death: If a cell is alive (state = 1), it will die (state becomes 0) under the following circumstances:
@@ -550,7 +550,7 @@
The Implementation
if (board[i ][j - 1] === 1) sum++;
if (board[i + 1][j - 1] === 1) sum++;
-// Middle row of neighbors (note i, j is skipped)
+// Middle row of neighbors (note i, j is skipped)
if (board[i - 1][j ] === 1) sum++;
if (board[i + 1][j ] === 1) sum++;
@@ -561,7 +561,7 @@
The Implementation
Just as with the Wolfram CA, I find myself writing out a bunch of if statements. This is another situation where, for teaching purposes, it’s useful and clear to write the code this way, explicitly stating every step (each time a neighbor has a state of 1, the counter increases). Nevertheless, it’s a bit silly to say, “If the cell state equals 1, add 1 to a counter” when I could instead just say, “Add every cell state to a counter.” After all, if the state can be only 0 or 1, the sum of all the neighbors’ states will yield the total number of live cells. Since the neighbors are arranged in a mini 3\times3 grid, I can introduce another nested loop to compute the sum more efficiently:
let sum = 0;
-//{!2} Use k and l as the counters since i and j are already used!
+//{!2} Use k and l as the counters since i and j are already used!
for (let k = -1; k <= 1; k++) {
for (let l = -1; l <= 1; l++) {
//{!1} Add up all the neighbors’ states.
@@ -644,11 +644,9 @@
Exercise 7.7
Object-Oriented Cells
Over the course of this book, I’ve built examples of systems of objects that have properties and move about the canvas. In this chapter, although I’ve been talking about a cell as if it were an object, I haven’t used the principles of object orientation in the code. This has worked because a cell is such an enormously simple object; its only property is its state, a single 0 or 1. However, I could further develop CA systems in plenty of ways beyond the simple models discussed here, and often these may involve keeping track of multiple properties for each cell. For example, what if a cell needs to remember its history of states? Or what if you want to apply motion and physics to a CA and have the cells move about the canvas, dynamically changing their neighbors from frame to frame?
To accomplish any of these ideas (and more), it would be helpful to see how to treat each cell as an object, rather than as a single 0 or 1 in an array. In a Game of Life simulation, for example, I’ll no longer want to initialize each cell like this:
-
board[i][j] = floor(random(2));
-
+
board[i][j] = floor(random(2));
Instead, I want something like this:
-
board[i][j] = new Cell(floor(random(2)));
-
+
board[i][j] = new Cell(floor(random(2)));
Here, Cell is a new class that I’ll write. What are the properties of a Cell object? In the Game of Life example, I might choose to create a cell that stores its position and size along with its state:
class Cell {
@@ -661,7 +659,7 @@
Object-Oriented Cells
this.y = y;
this.w = w;
-
In the non-OOP version, I used separate 2D arrays to keep track of the states for the current and next generation. By making a cell an object, however, each cell could keep track of both states by introducing a variable for the previous state:
+
In the non-OOP version, I used separate 2D arrays to keep track of the states for the current and next generations. By making a cell an object, however, each cell could keep track of both states by introducing a variable for the previous state:
// What was its previous state?
this.previous = this.state;
@@ -759,7 +757,7 @@
Continuous
This chapter has focused on examples with a finite number of discrete cell states—either 0 or 1. What if the cell’s state could be any floating-point number from 0 to 1?
Exercise 7.10
-
Adapt the Wolfram elementary CA to have a float state. You could define rules such as, “If the state is greater than 0.5” or “. . . less than 0.2.”
+
Adapt the Wolfram elementary CA to have a float state. You could define rules such as “If the state is greater than 0.5” or “. . . less than 0.2.”
Image Processing
I briefly touched on this earlier, but many image-processing algorithms operate on CA-like rules. For example, blurring an image requires creating a new pixel out of the average of a neighborhood of pixels. Simulations of ink dispersing on paper or water rippling over an image can also be achieved with CA rules.
-
- Photo by Saad Akhtar, CC BY-SA 4.0.
+
+ Photo by Saad Akhtar
Chakri Maha Prasat Hall
The Chakri Maha Prasat Hall, located within the Grand Palace in the heart of Bangkok, Thailand, is an architectural feat known for its intricate details and grandeur. Each level of the multilayered roof echoes a smaller or larger version of itself and represents the different levels of Mount Meru, the center of the Buddhist universe.
@@ -53,7 +53,7 @@
What Is a Fractal?
Figure 8.5: Two coastlines, with scale
A coastline is an example of a stochastic fractal, meaning it’s built out of probabilities and randomness. Unlike the deterministic (or predictable) tree-branching structure, a stochastic fractal is statistically self-similar. This means that even if a pattern isn’t precisely the same at every size, the general quality of the shape and its overall feel stay the same no matter how much you zoom in or out. The examples in this chapter explore both deterministic and stochastic techniques for generating fractal patterns.
-
While self-similarity is a key trait of fractals, it’s important to realize that self-similarity alone doesn’t make a fractal. After all, a straight line is self-similar: it looks the same at any scale and can be thought of as comprising lots of little lines. But a straight line isn’t a fractal. Fractals are characterized by having a fine structure at small scales (keep zooming into the coastline and you’ll continue to find fluctuations) and can’t be described with Euclidean geometry. As a rule, if you can say “It’s a line!” then it’s not a fractal.
+
While self-similarity is a key trait of fractals, it’s important to realize that self-similarity alone doesn’t make a fractal. After all, a straight line is self-similar: it looks the same at any scale and can be thought of as comprising lots of little lines. But a straight line isn’t a fractal. Fractals are characterized by having a fine structure at small scales (keep zooming in on the coastline and you’ll continue to find fluctuations) and can’t be described with Euclidean geometry. As a rule, if you can say “It’s a line!” then it’s not a fractal.
The Mandelbrot Set
One of the most well-known and recognizable fractal patterns is named for Mandelbrot himself. Generating the Mandelbrot set involves testing the properties of complex numbers after they’re passed through an iterative function. Do they tend to infinity? Do they stay bounded?
@@ -78,7 +78,7 @@
Implementing Recursive Functions
//{!1} Call the function background() in the definition of someFunction().
background(0);
}
-
Here’s the key difference with recursion: what would happen if you called the function you’re defining within that function itself? Can someFunction() call someFunction()?
+
Here’s the key difference with recursion: What would happen if you called the function you’re defining within that function itself? Can someFunction() call someFunction()?
function someFunction() {
//{!1} Is this a paradox?
someFunction();
@@ -113,7 +113,7 @@
Implementing Recursive Functions
return n * factorial(n - 1);
}
}
-
The factorial() function calls itself within its own definition. It may look a bit odd at first, but it works, as long as a stopping condition exists (in this case, n <= 1) so the function doesn’t get stuck calling itself forever. (I’m using <= instead of === as a safeguard against infinite recursion, but I should probably include additional error checking to manage non-integer or negative inputs to be more mathematically accurate.)
+
The factorial() function calls itself within its own definition. It may look a bit odd at first, but it works, as long as a stopping condition exists (in this case, n <= 1) so the function doesn’t get stuck calling itself forever. (I’m using <= instead of === as a safeguard against infinite recursion, but I should probably include additional error checking to manage noninteger or negative inputs to be more mathematically accurate.)
Figure 8.7 illustrates the steps that unfold when factorial(4) is called.
@@ -139,7 +139,7 @@
Example 8.1: Recursive Circles Once
}
The drawCircles() function draws a circle based on a set of parameters that it receives as arguments. It then calls itself with those same parameters, adjusting them slightly. The result is a series of circles, each of which is drawn inside the previous circle.
Just as the factorial() function stops recursing when n equals 0, notice that drawCircles() recursively calls itself only if the radius is greater than 4. This is a crucial point. As with iteration, all recursive functions must have an exit condition! You’re likely already aware that all for and while loops must include a Boolean expression that eventually evaluates to false, thus exiting the loop. Without one, the sketch would get caught inside an infinite loop. The same can be said about recursion. If a recursive function calls itself forever and ever with no exit, you’d be treated to a chilly, frozen screen in most cases. The browser, however, has protections built in, and rather than freeze, it will quit the sketch with the error message Maximum call stack size exceeded. This is just a fancy way of saying, “Too many recursive calls to the same function; time to stop!”
-
Example 8.1 was rather trivial; it could easily be achieved through simple iteration with a for or while loop. The results become more interesting, however, when a function is defined to call itself more than once. In such scenarios, recursion becomes wonderfully elegant. To illustrate, I’ll make drawCircles() a bit more complex: for every circle displayed, draw two more circles inside it, half its size: one left of center and one right of center.
+
Example 8.1 was rather trivial; it could easily be achieved through simple iteration with a for or while loop. The results become more interesting, however, when a function is defined to call itself more than once. In such scenarios, recursion becomes wonderfully elegant. To illustrate, I’ll make drawCircles() a bit more complex: for every circle displayed, draw two more circles inside it, half its size—one left of center and one right of center.
Example 8.2: Recursive Circles Twice
@@ -196,8 +196,8 @@
Drawing the Cantor Set with Recur
cantor(10, 20, width - 20);
You’d see something like Figure 8.8.
-
- Figure 8.8: The result of a single call to cantor() is a single line
+
+ Figure 8.8: The result of a single call to cantor() is a single line.
@@ -220,7 +220,7 @@
Drawing the Cantor Set with Recur
Figure 8.10: Two generations of lines drawn with the Cantor set rules
This works over two generations, but continuing to manually call line() will quickly become unwieldy. For the succeeding generations, I’d need 4, then 8, then 16 calls to line(). A for loop is the usual way around such a problem, but give that a try and you’ll see that working out the math for each iteration quickly proves inordinately complicated. Don’t despair, however: here’s where recursion comes to the rescue!
-
Take a look at where I draw the first line of the second generation, from the start to the one-third mark:
+
Look at where I draw the first line of the second generation, from the start to the one-third mark:
line(x, y + 20, x + length / 3, y + 20);
Instead of calling the line() function directly, why not call the cantor() function? After all, what does the cantor() function do? It draws a line at an (x, y) position with a given length. The x value stays the same, y increments by 20, and the length is length / 3:
cantor(x, y + 20, length / 3);
@@ -229,11 +229,11 @@
Drawing the Cantor Set with Recur
Now the cantor() function looks like this:
function cantor(x, y, length) {
line(x, y, x + len, y);
- //{$1} Two recursive calls. Note that 20 pixels are added to y.
+ //{$1} Two recursive calls. Note that 20 pixels are added to y.
cantor(x, y + 20, length / 3);
cantor(x + (2 * length / 3), y + 20, length / 3);
}
-
Since the cantor() function is now recursive, the same rule will be applied to the next lines and to the next and to the next as cantor() calls itself again and again! But don’t go running this code quite yet. The sketch is missing that crucial element: an exit condition. It has to stop recursing at some point. Here, I’ll choose to stop if the line length is less than or equal to 1 pixel. In other words, keep going if length is greater than 1.
+
Since the cantor() function is now recursive, the same rule will be applied to the next line and to the next and to the next as cantor() calls itself again and again! But don’t go running this code quite yet. The sketch is missing that crucial element: an exit condition. It has to stop recursing at some point. Here, I’ll choose to stop if the line length is less than or equal to 1 pixel. In other words, keep going if length is greater than 1.
Example 8.4: The Cantor Set
@@ -280,7 +280,7 @@
The Monster Curve
//{!2} A line between two points: a and b
constructor(a, b) {
- // a and b are p5.Vector objects.
+ // a and b are p5.Vector objects.
this.start = a.copy();
this.end = b.copy();
}
@@ -312,7 +312,7 @@
The Monster Curve
}
}
This is my foundation for the sketch. I have a KochLine class that keeps track of a line from point start to point end, and I have an array that keeps track of all the KochLine objects. Given these elements, how and where should I apply the Koch rules and the principles of recursion?
-
Remember the Game of Life cellular automaton from Chapter 7? In that simulation, I always kept track of two generations: current and next. When I was finished calculating the next generation, next became current, and I moved on to computing the new next generation. I’m going to apply a similar technique here. I have a segments array listing the current set of line segments (at the start of the program, there’s only one). Now I need a second array (I’ll call it next), where I can place all the new KochLine objects generated from applying the Koch rules. For every singleKochLine in the current array, four new line segments will be added to next. When I’m done, the next array becomes the new segments (see Figure 8.13).
+
Remember the Game of Life cellular automaton from Chapter 7? In that simulation, I always kept track of two generations: current and next. When I was finished calculating the next generation, next became current, and I moved on to computing the new next generation. I’m going to apply a similar technique here. I have a segments array listing the current set of line segments (at the start of the program, there’s only one). Now I need a second array (I’ll call it next), where I can place all the new KochLine objects generated from applying the Koch rules. For every single KochLine in the current array, four new line segments will be added to next. When I’m done, the next array becomes the new segments array (see Figure 8.13).
Figure 8.13: The next generation of the fractal is calculated from the current generation. Then next becomes the new current in the transition from one generation to another.
@@ -333,8 +333,8 @@
The Monster Curve
// The next segments!
segments = next;
}
-
By calling generate() over and over, the Koch curve rules will be recursively applied to the existing set of KochLine segments. But, of course, I’ve skipped over the real work of the function: how do I actually break one line segment into four as described by the rules? I need a way to calculate the start and end points of each line.
-
Because the KochLine class uses p5.Vector objects to store the start and end points, this is a wonderful opportunity to practice all that vector math from Chapter 1, along with some trigonometry from Chapter 3. First, I should establish the scope of the problem: how many points do I need to compute for each KochLine object? Figure 8.14 shows the answer.
+
By calling generate() over and over, the Koch curve rules will be recursively applied to the existing set of KochLine segments. But, of course, I’ve skipped over the real work of the function: How do I actually break one line segment into four as described by the rules? I need a way to calculate the start and end points of each line.
+
Because the KochLine class uses p5.Vector objects to store the start and end points, this is a wonderful opportunity to practice all that vector math from Chapter 1, along with some trigonometry from Chapter 3. First, I should establish the scope of the problem: How many points do I need to compute for each KochLine object? Figure 8.14 shows the answer.
Figure 8.14: Two points become five points.
@@ -344,7 +344,7 @@
Where do I get these points? Why not ask theKochLine object to calculate them for me?
+
Where do I get these points? Why not ask the KochLine object to calculate them for me?
function generate() {
let next = [];
for (let segment of segments) {
@@ -359,10 +359,10 @@
The Monster Curve
segments = next;
}
Wait, let’s take a look at this one line of code a little bit more closely:
-
// This is object destructuring but for an array!
+
// This is object destructuring, but for an array!
let [a, b, c, d, e] = segment.kochPoints();
-
As you may recall, in Chapter 6 I explained object destructuring as a means of extracting properties from an object and assigning them to individual variables. Guess what? You can do the same with arrays! Here, as long as the kochPoints() method returns an array of five elements, I can conveniently unpack and assign them, each to its respective variables: a, b, c, d, and e. It's a lovely way to handle multiple return values. Just as with objects, array destructuring keeps the code neat and tidy.
-
Now I just need to write a new kochPoints() method in the KochLine class that returns an array of p5.Vector objects representing the points a through e in Figure 8.15. I’ll knock off a and e first, which are the easiest: they’re just copies of the start and end points of the original line:
+
As you may recall, in Chapter 6 I explained object destructuring as a means of extracting properties from an object and assigning them to individual variables. Guess what? You can do the same with arrays! Here, as long as the kochPoints() method returns an array of five elements, I can conveniently unpack and assign them, each to its respective variables: a, b, c, d, and e. It’s a lovely way to handle multiple return values. Just as with objects, array destructuring keeps the code neat and tidy.
+
Now I just need to write a new kochPoints() method in the KochLine class that returns an array of p5.Vector objects representing the points a through e in Figure 8.15. I’ll knock off a and e first, which are the easiest—they’re just copies of the start and end points of the original line:
kochPoints() {
//{!1} Note the use of copy(). As discussed in Chapter 5, it’s best to avoid making copies whenever
@@ -380,7 +380,7 @@
The Monster Curve
// Create a vector from start to end.
let v = p5.Vector.sub(this.end, this.start);
- // Shorten length to one-third.
+ // Shorten the length to one-third.
v.div(3);
//{!1} Add that vector to the beginning of the line to find the new point.
@@ -391,11 +391,11 @@
The Monster Curve
-
- Figure 8.16: The vector \vec{v} is rotated by 60° to find the third point.
+
+ Figure 8.16: The vector \vec{v} is rotated by 60 degreesto find the third point.
-
The last point, c, is the most difficult one to compute. However, if you consider that the angles of an equilateral triangle are all 60 degrees, this makes you work suddenly easier. If you know how to find the new b with a vector one-third the length of the line, what if you rotate that same vector 60 degrees (or \pi/3 radians) and add it to b, as in Figure 8.16? You’d arrive at c!
+
The last point, c, is the most difficult one to compute. However, if you consider that the angles of an equilateral triangle are all 60 degrees, this makes your work suddenly easier. If you know how to find the new b with a vector one-third the length of the line, what if you rotate that same vector 60 degrees (or \pi/3 radians) and add it to b, as in Figure 8.16? You’d arrive at c!
//{!1} Rotate by –PI/3 radians (negative angle so it rotates “up”).
v.rotate(-PI / 3);
@@ -464,7 +464,7 @@
The Deterministic Version
Figure 8.17: Each generation of a fractal tree, following the given production rules. The final tree is several generations later.
Once again, I have a nice fractal with a recursive definition: a branch is a line with two branches connected to it. What makes this fractal a bit more difficult than the previous ones is the use of the word rotate in the fractal’s rules. Each new branch must rotate relative to the previous branch, which is rotated relative to all its previous branches. Luckily, p5.js has a mechanism to keep track of rotations: transformations.
-
I touched on transformations in Chapter 3. They’re a set of functions, such as translate(), rotate(), scale(), push(), and pop(), that allow you to change the position, orientation, and scale of shapes in your sketch. The translate() function moves the coordinate system, rotate() rotates it, and push() and pop() help save and restore the current transformation state. If you aren’t familiar with these functions, I have a set of videos on transformations in p5.js available at the Coding Train.
+
I touched on transformations in Chapter 3. They’re a set of functions, such as translate(), rotate(), scale(), push(), and pop(), that allow you to change the position, orientation, and scale of shapes in your sketch. The translate() function moves the coordinate system, rotate() rotates it, and push() and pop() help save and restore the current transformation state. If you aren’t familiar with these functions, I have a set of videos on transformations in p5.js available at the Coding Train website.
I’ll begin by drawing a single branch, the trunk of the tree. Since I’m going to be using the rotate() function, I need to make sure I’m continuously translating along the branches while drawing. Remember, when you rotate in p5.js, you’re always rotating around the origin, or point (0, 0), so here the origin must always be translated to the start of the next branch being drawn (equivalent to the end of the previous branch). Since the trunk starts at the bottom of the window, I first have to translate to that spot:
translate(width / 2, height);
Then I can draw the trunk as a line upward:
@@ -529,7 +529,7 @@
Exercise 8.6
Follow the recursive algorithm of drawing branches, and number them in the diagram in the order that p5.js would actually draw each one.
-
You may have noticed that the recursive function as written has a major problem: it has no exit condition, so it would get stuck in infinite recursive calls to itself. Also, the branches of the tree should get shorter at each level, but so far I’ve hardcoded every branch to have a length of 100 pixels. The solutions to these two issues are intertwined: if the branches shrink from one generation to the next, I can make the function stop recursing when the branches have become too short:
+
You may have noticed that the recursive function as written has a major problem: it has no exit condition, so it would get stuck in infinite recursive calls to itself. Also, the branches of the tree should get shorter at each level, but so far I’ve hardcoded every branch to have a length of 100 pixels. The solutions to these two issues are intertwined—if the branches shrink from one generation to the next, I can make the function stop recursing when the branches have become too short:
//{!1} Each branch now receives its length as an argument.
function branch(len) {
line(0, 0, 0, -len);
@@ -568,7 +568,7 @@
Example 8.6: A Recursive Tree
function draw() {
background(255);
- // Map the angle to range from 0 to 90° (HALF_PI) according to mouseX.
+ // Map the angle to range from 0° to 90° (HALF_PI) according to mouseX.
angle = map(mouseX, 0, width, 0, HALF_PI);
// Start the tree from the bottom of the canvas.
@@ -637,7 +637,7 @@
Exercise 8.10
L-systems
In 1968, Hungarian botanist Aristid Lindenmayer developed a grammar-based system to model the growth patterns of plants. This system uses textual symbols and a specific set of rules to generate patterns, just as a language’s grammar defines rules for constructing sentences out of words. Known as an L-system (short for Lindenmayer system), this technique can be used to generate the recursive fractal patterns demonstrated so far in this chapter. L-systems are additionally valuable because they provide a mechanism for using simple symbols to keep track of fractal structures that require complex and multifaceted production rules.
-
Implementing an L-system in p5.js requires working with recursion, transformations, and strings of text. This chapter already covers recursion and transformations, but strings are new. Here’s a quick snippet of code demonstrating the three aspects of working with text important to L-systems: creating, concatenating, and iterating over strings. You can refer to the book’s website for additional string resources and tutorials:
+
Implementing an L-system in p5.js requires working with recursion, transformations, and strings of text. This chapter already covers recursion and transformations, but strings are new. Here’s a quick snippet of code demonstrating the three aspects of working with text important to L-systems: creating, concatenating, and iterating over strings. You can refer to the book’s website for additional string resources and tutorials.
// A string is created as text between quotes (single or double).
let message1 = "Hello!";
@@ -646,7 +646,7 @@
L-systems
// The length of a string is stored in its length property.
for (let i = 0; i < message.length; i++) {
- //{!1} Individual characters can be accessed by an index, just like an array! I'm using charAt(i) instead of `[i]`.
+ //{!1} Individual characters can be accessed by an index, just like an array! I’m using charAt(i) instead of [i].
let character = message.charAt(i);
}
An L-system has three main components:
@@ -668,10 +668,8 @@
L-systems
Rules
-
- A → AB
- B → A
-
+
A → AB
+B → A
@@ -828,7 +826,7 @@
Example 8.8: Simple L-sy
-
This type of drawing framework is often referred to as turtle graphics (from the old days of Logo programming). Imagine a turtle sitting on your p5.js canvas, able to accept a small set of commands: turn left, turn right, move forward, draw a line, and so on. While p5.js isn’t set up to operate this way by default, I can emulate a turtle graphics engine fairly easily with translate(), rotate(), and line(). Here’s how I would convert the FG+–[] L-system alphabet into p5.js code:
+
This type of drawing framework is often referred to as turtle graphics (from the old days of Logo programming). Imagine a turtle sitting on your p5.js canvas, able to accept a small set of commands: turn left, turn right, move forward, draw a line, and so on. While p5.js isn’t set up to operate this way by default, I can emulate a turtle graphics engine fairly easily with translate(), rotate(), and line(). Here’s how I would convert this L-system’s alphabet into p5.js code:
@@ -879,7 +877,7 @@
Example 8.8: Simple L-sy
//{!14} Perform the correct task for each character.
// This could also be written with a switch statement,
// which might be nicer to look at, but leaving it as an
- // if/else if structure helps readers unfamiliar with that syntax.
+ // if...else statement helps readers unfamiliar with that syntax.
if (c === 'F') {
line(0, 0, length, 0);
translate(length, 0);
@@ -945,7 +943,7 @@
Example 8.9: An L-system
lsystem.generate();
}
- //{!2 .offset} The Turtle object has a length and angle.
+ //{!2 .offset} The Turtle object has a length and angle.
turtle = new Turtle(4, radians(25));
}
@@ -963,17 +961,17 @@
Exercise 8.11
Exercise 8.12
-
The seminal work in L-systems and plant structures, The Algorithmic Beauty of Plants by Przemysław Prusinkiewicz and Aristid Lindenmayer (Springer), was published in 1990. Chapter 1 describes many sophisticated L-systems with additional drawing rules and available alphabet characters. It also describes several methods for generating stochastic L-systems. Expand the L-system code in Example 8.9 to include one or more of the extra features described by Prusinkiewicz and Lindenmayer.
+
The seminal work in L-systems and plant structures, The Algorithmic Beauty of Plants by Przemysław Prusinkiewicz and Aristid Lindenmayer (Springer), was published in 1990. Chapter 1 describes many sophisticated L-systems with additional drawing rules and available alphabet characters. It also describes several methods for generating stochastic L-systems. Expand the L-system code in Example 8.9 to include one or more of the extra features described by Prusinkiewicz and Lindenmayer.
Exercise 8.13
-
In this chapter, I emphasized using fractal algorithms for generating visual patterns. However, fractals can be found in other creative mediums. For example, they’re evident in Johann Sebastian Bach’s Cello Suite no. 3, and the structure of David Foster Wallace’s novel Infinite Jest (Little, Brown, 1996) was inspired by fractals. Consider using the examples in this chapter to generate audio or text.
+
In this chapter, I emphasized using fractal algorithms for generating visual patterns. However, fractals can be found in other creative mediums. For example, they’re evident in Johann Sebastian Bach’s Cello Suite No. 3, and the structure of David Foster Wallace’s novel Infinite Jest (Little, Brown, 1996) was inspired by fractals. Consider using the examples in this chapter to generate audio or text.
The Ecosystem Project
Incorporate fractals into your ecosystem. Here are some possibilities:
-
Add plant-like creatures to the ecosystem environment.
+
Add plantlike creatures to the ecosystem environment.
Say one of your plants is similar to a fractal tree. Can you add leaves or flowers to the ends of the branches? What if the leaves can fall off the tree (depending on a wind force)? What if you add fruit that can be picked and eaten by the creatures?
Design a creature with a fractal pattern.
Use an L-system to generate instructions for the way a creature should move or behave.
-
- Photo courtesy of the National Park Service, public domain.
+
+ Photo courtesy of the National Park Service
Pueblo Pottery
For centuries, pottery created by the Ancestral Puebloans and Mogollon cultures of the southwestern United States and northern Mexico has held great significance in both ceremonial and everyday contexts. Techniques and design elements like those used to create this Chaco Ancestral Pueblo bowl are passed down through generations, with each potter learning, preserving, and subtly adapting these designs. This ongoing process gives rise to a continually evolving tapestry of familial and cultural expression.
@@ -36,7 +36,7 @@
Why Use Genetic Algorithms?
Figure 9.1: Infinite cats typing at infinite keyboards
-
This is my meow-veloustwist on the infinite monkey theorem, whichis stated as follows: a monkey hitting keys randomly on a typewriter will eventually type the complete works of Shakespeare, given an infinite amount of time. It’s only a theory because in practice the number of possible combinations of letters and words makes the likelihood of the monkey actually typing Shakespeare minuscule. To put it in perspective, even if the monkey had started typing at the beginning of the universe, the probability that by now it would have produced just Hamlet, to say nothing of the entireworks of Shakespeare, is still absurdly unlikely.
+
This is my meow-veloustwist on the infinite monkey theorem,whichis stated as follows: a monkey hitting keys randomly on a typewriter will eventually type the complete works of Shakespeare, given an infinite amount of time. It’s only a theory because in practice the number of possible combinations of letters and words makes the likelihood of the monkey actually typing Shakespeare minuscule. To put it in perspective, even if the monkey had started typing at the beginning of the universe, the probability that by now it would have produced just Hamlet, to say nothing of the entireworks of Shakespeare, is still absurdly unlikely.
Consider a cat named Clawdius. Clawdius types on a reduced typewriter containing only 27 characters: the 26 English letters plus the spacebar. The probability of Clawdius hitting any given key is 1 in 27.
Next, consider the phrase “to be or not to be that is the question” (for simplicity, I’m ignoring capitalization and punctuation). The phrase is 39 characters long, including spaces. If Clawdius starts typing, the chance he’ll get the first character right is 1 in 27. Since the probability he’ll get the second character right is also 1 in 27, he has a 1 in 729 (27 \times 27) chance of landing the first two characters in correct order. (This follows directly from our discussion of probability in Chapter 0.) Therefore, the probability that Clawdius will type the full phrase is 1 in 27 multiplied by itself 39 times, or (1/27)^{39}. That equals a probability of . . .
1 \text{ in } \text{66,555,937,033,867,822,607,895,549,241,096,482,953,017,615,834,735,226,163}
@@ -57,12 +57,12 @@
How Genetic Algorithms Work
Variation: There must be a variety of traits present in the population of creatures or a means to introduce variation for evolution to take place. Imagine a population of beetles that were exactly the same: same color, same size, same wingspan, same everything. Without any variety in the population, the children would always be identical to the parents and to each other. New combinations of traits could never occur, and nothing could evolve.
Selection: There must be a mechanism by which some creatures have the opportunity to be parents and pass on their genetic information, while others don’t. This is commonly referred to as survival of the fittest. Take, for example, a population of gazelles that are chased by lions. The faster gazelles have a better chance of escaping the lions, increasing their chances of living longer, reproducing, and passing on their genetic information to offspring.
- The term fittest can be misleading, however. It’s often thought to mean biggest, fastest, or strongest, but while it can sometimes encompass physical attributes like size, speed, or strength, it doesn’t have to. The core of natural selection lies in whatever traits best suit an organism’s environment and increase its likelihood of survival and ultimately reproduction. Instead of asserting superiority, fittest can be better understood as “able to reproduce.” Take the Dolania americana (also known as the American sand-burrowing mayfly), which is believed to have the shortest lifespan of any insect. An adult female lives for only five minutes, but as long as it has managed to deposit its egg in the water, it will pass its genetic information to the next generation. For the typing cats, a more fit cat, one that I will assign as more likely to reproduce, is one that has typed more characters present in a given phrase of Shakespeare.
+ The term fittest can be misleading, however. It’s often thought to mean biggest, fastest, or strongest, but while it can sometimes encompass physical attributes like size, speed, or strength, it doesn’t have to. The core of natural selection lies in whatever traits best suit an organism’s environment and increase its likelihood of survival and ultimately reproduction. Instead of asserting superiority, fittest can be better understood as “able to reproduce.” Take the Dolania americana (aka the American sand-burrowing mayfly), which is believed to have the shortest life span of any insect. An adult female lives for only five minutes, but as long as it has managed to deposit its egg in the water, it will pass its genetic information to the next generation. For the typing cats, a more fit cat, one that I will assign as more likely to reproduce, is one that has typed more characters present in a given phrase of Shakespeare.
I want to emphasize the context in which I’m applying these Darwinian concepts: a simulated, artificial environment where specific goals can be quantified, all for the sake of creative exploration. Throughout history, the principles of genetics have been used to harm those who have been marginalized and oppressed by dominant societal structures. I believe it is essential to approach projects involving GAs with careful consideration of the language used, and to ensure that the documentation and descriptions of the work are framed inclusively.
With these concepts established, I’ll begin walking through the GA narrative. I’ll do this in the context of typing cats. The algorithm will be divided into several steps that unfold over two parts: a set of conditions for initialization, and the steps that are repeated over and over again until the correct phrase is found.
-
Step 1: Creating a Population
+
Step 1: Population Creation
For typing cats, the first step of the GA is to create a population of phrases. I’m using the term phrase rather loosely to mean any string of characters. These phrases are the creatures of this example, though of course they aren’t very creature-like.
In creating the population of phrases, the Darwinian principle of variation applies. Let’s say for the sake of simplicity that I’m trying to evolve the phrase cat and that I have a population of three phrases:
@@ -113,7 +113,7 @@
Step 1: Creating a Population
-
Think of the genotype as the digital information, the data that represents color—in the case of grayscale values, an integer from 0 to 255. The way you choose to express the data is arbitrary: a red value, a green value, and a blue value. It doesn’t even need to be color at all: in a different approach, you could use the same values to describe the length of a line, the weight of a force, and so on:
+
Think of the genotype as the digital information, the data that represents color—in the case of grayscale values, an integer from 0 to 255. The way you choose to express the data is arbitrary: a red value, a green value, and a blue value. It doesn’t even need to be color at all—in a different approach, you could use the same values to describe the length of a line, the weight of a force, and so on:
@@ -283,7 +283,7 @@
Step 2: Selection
As you can see, elements A and B are clearly the most fit and would have the highest score. But neither contains the correct characters for the end of the phrase. Element C, even though it would receive a very low score, happens to have the genetic data for the end of the phrase. While I might want A and B to be picked to generate the majority of the next generation, I still want C to have a small chance to participate in the reproductive process too.
Step 3: Reproduction
-
Now that I’ve demonstrated a strategy for picking parents, the last step is to use reproduction to create the population’s next generation, keeping in mind the Darwinian principle of heredity—that children inherit properties from their parents. Again, numerous techniques that could be employed here. For example, one reasonable (and easy-to-program) strategy is cloning, meaning just one parent is picked and an exact copy of that parent is created as a child element. As with the elitist approach to selection, however, this runs counter to the goal of variation. Instead, the standard approach with GAs is to pick two parents and create a child according to two steps:
+
Now that I’ve demonstrated a strategy for picking parents, the last step is to use reproduction to create the population’s next generation, keeping in mind the Darwinian principle of heredity—that children inherit properties from their parents. Again, numerous techniques could be employed here. For example, one reasonable (and easy-to-program) strategy is cloning, meaning just one parent is picked and an exact copy of that parent is created as a child element. As with the elitist approach to selection, however, this runs counter to the goal of variation. Instead, the standard approach with GAs is to pick two parents and create a child according to two steps:
Crossover
Mutation
@@ -327,7 +327,7 @@
Step 3: Reproduction
Mutation is described in terms of a rate. A given GA might have a mutation rate of 5 percent, or 1 percent, or 0.1 percent, for example. Say I’ve arrived through crossover at the child phrase catire. If the mutation rate is 1 percent, this means that each character in the phrase has a 1 percent chance of mutating before being “born” into the next generation. What does it mean for a character to mutate? In this case, mutation could be defined as picking a new random character. A 1 percent probability is fairly low, so most of the time mutation won’t occur at all in a six-character string (about 94 percent of the time, in fact). However, when it does, the mutated character is replaced with a randomly generated one (see Figure 9.6).
As you’ll see in the coming examples, the mutation rate can greatly affect the behavior of the system. A very high mutation rate (such as, say, 80 percent) would negate the entire evolutionary process and leave you with something more akin to a brute-force algorithm. If the majority of a child’s genes are generated randomly, you can’t guarantee that the more fit genes occur with greater frequency with each successive generation.
Overall, the process of selection (picking two parents) and reproduction (crossover and mutation) is repeated N times until you have a new population of N child elements.
-
Step 4: Repeat!
+
Step 4: Repetition!
At this point, the new population of children becomes the current population. Then the process returns to step 2 and starts all over again, evaluating the fitness of each element, selecting parents, and producing another generation of children. Hopefully, as the algorithm cycles through more and more generations, the system evolves closer and closer to the desired solution.
Coding the Genetic Algorithm
Now that I’ve described all the steps of the GA, it’s time to translate them into code. Before I dive into the details of the implementation, let’s think about how these steps fit into the overall standard structure of a p5.js sketch. What goes into setup(), and what goes into draw()?
@@ -348,7 +348,7 @@
Step 1: Initialization
If I’m going to create a population, I need a data structure to store a list of elements in the population:
// An array for the population of elements
let population = [];
-
Choosing an array to represent a list is straightforward, but the question remains: an array of what? An object is an excellent choice for storing the genetic information, as it can hold multiple properties and methods. These genetic objects will be structured according to a class that I’ll call DNA:
+
Choosing an array to represent a list is straightforward, but the question remains: An array of what? An object is an excellent choice for storing the genetic information, as it can hold multiple properties and methods. These genetic objects will be structured according to a class that I’ll call DNA:
class DNA {
}
@@ -377,7 +377,7 @@
Step 1: Initialization
function setup() {
for (let i = 0; i < population.length; i++) {
- //{!1} Initialize each element of the population; 18 is hardcoded for now as the length of the genes array.
+ //{!1} Initialize each element of the population; 18 is hardcoded for now as the length of the genes array.
population[i] = new DNA(18);
}
}
@@ -416,8 +416,8 @@
Step 2: Selection
phrase.calculateFitness(target);
}
}
-
Once the fitness scores have been computed, the next step is to build the mating pool for the reproduction process. The mating pool is a data structure from which two parents are repeatedly selected. Recalling the description of the selection process, the goal is to pick parents with probabilities calculated according to fitness. The members of the population with the highest fitness scores should be most likely to be selected; those with the lowest scores, the least likely.
-
In Chapter 0, I covered the basics of probability and generating a custom distribution of random numbers. I’m going to use the same techniques here to assign a probability to each member of the population, picking parents by spinning the wheel of fortune. Revisiting Figure 9.2 again, your mind might immediately go back to Chapter 3 and contemplate coding a simulation of an actual spinning wheel. As fun as this might be (and you should make one!), it’s quite unnecessary.
+
Once the fitness scores have been computed, the next step is to build the mating pool for the reproduction process. The mating pool is a data structure from which two parents are repeatedly selected. Recalling the description of the selection process, the goal is to pick parents with probabilities calculated according to fitness. The members of the population with the highest fitness scores should be the most likely to be selected; those with the lowest scores, the least likely.
+
In Chapter 0, I covered the basics of probability and generating a custom distribution of random numbers. I’m going to use the same techniques here to assign a probability to each member of the population, picking parents by spinning the wheel of fortune. Revisiting Figure 9.2, your mind might immediately go back to Chapter 3 and contemplate coding a simulation of an actual spinning wheel. As fun as this might be (and you should make one!), it’s quite unnecessary.
@@ -442,10 +442,10 @@
Step 2: Selection
I can select two random instances of DNA from the mating pool by using the p5.js random() function. When an array is passed as an argument to random(), the function returns a single random element from the array:
let parentA = random(matingPool);
let parentB = random(matingPool);
-
This method of building a mating pool and choosing parents from it works, but it isn’t the only way to perform selection. Other, more memory-efficient techniques don’t require an additional array full of multiple references to each element. For example, think back to the discussion of non-uniform distributions of random numbers in Chapter 0. There, I implemented the accept-reject method. If applied here, the approach would be to randomly pick an element from the original population array, and then pick a second, qualifying random number to check against the element’s fitness value. If the fitness is less than the qualifying number, start again and pick a new element. Keep going until two parents are deemed fit enough.
+
This method of building a mating pool and choosing parents from it works, but it isn’t the only way to perform selection. Other, more memory-efficient techniques don’t require an additional array full of multiple references to each element. For example, think back to the discussion of nonuniform distributions of random numbers in Chapter 0. There, I implemented the accept-reject method. If applied here, the approach would be to randomly pick an element from the original population array, and then pick a second, qualifying random number to check against the element’s fitness value. If the fitness is less than the qualifying number, start again and pick a new element. Keep going until two parents are deemed fit enough.
Yet another excellent alternative is worth exploring that similarly capitalizes on the principle of fitness-proportionate selection. To understand how it works, imagine a relay race in which each member of the population runs a given distance tied to its fitness. The higher the fitness, the farther they run. Let’s also assume that the fitness values have been normalized to all add up to 1 (just as with the wheel of fortune). The first step is to pick a starting line—a random distance from the finish. This distance is a random number from 0 to 1. (You’ll see in a moment that the finish lineis assumed to be at 0.)
let start = random(1);
-
Then the relay race begins at the starting line with first member of the population:
+
Then the relay race begins at the starting line with the first member of the population:
let index = 0;
The runner travels a distance defined by its normalized fitness score, then hands the baton to the next runner:
@@ -551,7 +551,7 @@
Step 3: Reproduction (Crosso
// but the crossover method will override the array.)
let child = new DNA(this.genes.length);
- //{!1} Pick a random midpoint in the genes array.
+ //{!1} Pick a random midpoint in the genes array.
let midpoint = floor(random(this.genes.length));
for (let i = 0; i < this.genes.length; i++) {
@@ -651,7 +651,7 @@
Example 9.1: Gene
population[i] = child;
}
- // Step 4: Repeat — go back to the beginning of `draw()`!
+ // Step 4: Repetition. Go back to the beginning of draw()!
}
The sketch.js file precisely mirrors the steps of the GA. However, most of the functionality called upon is encapsulated in the DNA class:
@@ -665,7 +665,7 @@
Example 9.1: Gene
}
}
- //{.code-wide} Converts the array to a string the phenotype.
+ //{.code-wide} Converts the array to a string of the phenotype.
getPhrase() {
return this.genes.join("");
}
@@ -721,7 +721,7 @@
Exercise 9.6
Exercise 9.7
-
Explore the idea of a dynamic mutation rate. For example, try calculating a mutation rate that inversely correlates with the average fitness of the parent phrases, so that higher fitness results in fewer mutations. Does this change affect the behavior of the overall system and how quickly the target phrase is found?
+
Explore the idea of a dynamic mutation rate. For example, try calculating a mutation rate that inversely correlates with the average fitness of the parent phrases so that higher fitness results in fewer mutations. Does this change affect the behavior of the overall system and how quickly the target phrase is found?
Customizing Genetic Algorithms
@@ -806,9 +806,9 @@
Key 1: The Global Variables
-
Without any mutation at all (0 percent), you just have to get lucky. If all the correct characters are present somewhere in an element of the initial population, you’ll evolve the phrase very quickly. If not, there’s no way for the sketch to ever reach the exact phrase. Run it a few times and you’ll see both instances. In addition, once the mutation rate gets high enough (10 percent, for example), so much randomness is involved (1 out of every 10 letters is random in each new child) that the simulation is pretty much back to a random typing cat. In theory, it will eventually solve the phrase, but you may be waiting much, much longer than is reasonable.
+
Without any mutation at all (0 percent), you just have to get lucky. If all the correct characters are present somewhere in an element of the initial population, you’ll evolve the phrase very quickly. If not, there’s no way for the sketch to ever reach the exact phrase. Run it a few times and you’ll see both instances. In addition, once the mutation rate gets high enough (10 percent, for example), so much randomness is involved (1 out of every 10 letters is random in each new child) that the simulation is pretty much back to a randomly typing cat. In theory, it will eventually solve the phrase, but you may be waiting much, much longer than is reasonable.
Key 2: The Fitness Function
-
Playing around with the mutation rate or population size is pretty easy and involves little more than typing numbers in your sketch. The real hard work of a developing a GA is in writing the fitness function. If you can’t define your problem’s goals and evaluate numerically how well those goals have been achieved, you won’t have successful evolution in your simulation.
+
Playing around with the mutation rate or population size is pretty easy and involves little more than typing numbers in your sketch. The real hard work of developing a GA is in writing the fitness function. If you can’t define your problem’s goals and evaluate numerically how well those goals have been achieved, you won’t have successful evolution in your simulation.
Before I move on to other scenarios exploring more sophisticated fitness functions, I want to look at flaws in my Shakespearean fitness function. Consider solving for a phrase that isn’t 18 characters long, but 1,000. And take two elements of the population, one with 800 characters correct and one with 801. Here are their fitness scores:
@@ -835,7 +835,7 @@
Key 2: The Fitness Function
To put it another way, Figure 9.8 shows graphs of two possible fitness functions.
- Figure 9.8: A fitness graph of y = x (left) and of y = x^2 (right)
+ Figure 9.8: A fitness graph of y = x (left) and of y = x^2 (right)
On the left is a linear graph; as the number of characters goes up, so does the fitness score. By contrast, in the graph on the right, as the number of characters goes up, the fitness score goes way up. That is, the fitness increases at an accelerating rate as the number of correct characters increases.
I can achieve this second type of result in various ways. For example, I could say this:
@@ -971,7 +971,7 @@
Key 3: The Genotype and Phenotype
}
}
What’s great about dividing the genotype and phenotype into separate classes (DNA and Rocket, for example) is that when it comes time to build all the code, you’ll notice that the DNA class I developed earlier remains intact. The only thing that changes is the kind of data stored in the array (numbers, vectors, and so on) and the expression of that data in the phenotype class.
-
In the next section, I'll follow this idea a bit further and walk through the necessary steps to implement an example that involves moving bodies and an array of vectors as DNA.
+
In the next section, I’ll follow this idea a bit further and walk through the necessary steps to implement an example that involves moving bodies and an array of vectors as DNA.
Evolving Forces: Smart Rockets
I mentioned rockets for a specific reason: in 2009, Jer Thorp released a GAs example on his blog titled “Smart Rockets.” Thorp pointed out that the National Aeronautics and Space Administration (NASA) uses evolutionary computing techniques to solve all sorts of problems, from satellite antenna design to rocket-firing patterns. This inspired him to create a Flash demonstration of evolving rockets.
Here’s the scenario: a population of rockets launches from the bottom of the screen with the goal of hitting a target at the top of the screen. Obstacles block a straight-line path to the target (see Figure 9.9).
@@ -1058,13 +1058,13 @@
Developing the Rockets
-
+
- Figure 9.11: Vectors created with random x and y values (left) and using p5.Vector.random2D()(right)
+ Figure 9.11: Vectors created with random x and y values (left) and using p5.Vector.random2D() (right)
As you may recall from Chapter 3, a better choice is to pick a random angle and create a vector of length 1 from that angle. This produces results that form a circle (see the right of Figure 9.11) and can be achieved with polar-to-Cartesian conversion or the trusty p5.Vector.random2D() method:
for (let i = 0; i < length; i++) {
@@ -1075,14 +1075,14 @@
Developing the Rockets
class DNA {
constructor() {
- // The genetic sequence is an array of vectors
+ // The genetic sequence is an array of vectors.
this.genes = [];
// How strong can the thrusters be?
this.maxForce = 0.1;
- // Notice that the length of genes is equal to a global lifeSpan variable.
+ // Notice that the length of genes is equal to a global lifeSpan variable.
for (let i = 0; i < lifeSpan; i++) {
this.genes[i] = p5.Vector.random2D();
- //{!1} Scale the vectors randomly, but no stronger than the maximum force.
+ //{!1} Scale the vectors randomly, but not stronger than the maximum force.
this.genes[i].mult(random(0, maxforce));
}
}
@@ -1109,7 +1109,7 @@
Developing the Rockets
this.dna = dna;
// A rocket has fitness.
this.fitness = 0;
- //{!1} A counter for the DNA genes array
+ //{!1} A counter for the DNA genes array
this.geneCounter = 0;
this.position = createVector(x, y);
this.velocity = createVector();
@@ -1117,9 +1117,9 @@
Developing the Rockets
}
run() {
- // Apply a force from the genes array.
+ // Apply a force from the genes array.
this.applyForce(this.dna.genes[this.geneCounter]);
- // Go to the next force in the genes array.
+ // Go to the next force in the genes array.
this.geneCounter++;
//{!1} Update the rocket’s physics.
this.update();
@@ -1130,7 +1130,7 @@
Managing the Population
To keep my sketch.js file tidier, I’ll put the code for managing the array of Rocket objects in a Population class. As with the DNA class, the happy news is that I barely have to change anything from the typing cats example. I’m just organizing the code in a more object-oriented way, with a selection() method and a reproduction() method. For the sake of demonstrating a different technique, I’ll also normalize the fitness values in selection() and use the weighted-selection (relay-race) algorithm in reproduction(). This eliminates the need for a separate mating-pool array. The weightedSelection() code is the same as that written earlier in the chapter:
class Population {
- // Population has variables to keep track of mutation rate, current
+ // Population has variables to keep track of the mutation rate, current
// population array, and number of generations.
constructor(mutation, length) {
// Mutation rate
@@ -1173,14 +1173,14 @@
Managing the Population
let parentB = this.weightedSelection();
let child = parentA.crossover(parentB);
child.mutate(this.mutationRate);
- // Rocket goes in the new population.
+ // Rocket goes in the new population.
newPopulation[i] = new Rocket(320, 240, child);
}
// Now the new population is the current one.
this.population = newPopulation;
}
-
I need to make one more fairly significant change, however. With typing cats, a random phrase was evaluated as soon as it was created. The string of characters had no lifespan; it existed purely for the purpose of calculating its fitness. The rockets, however, need to live for a period of time before they can be evaluated—that is, they need to be given a chance to make their attempt at reaching the target. Therefore, I need to add one more method to the Population class that runs the physics simulation. This is identical to what I did in the run() method of a particle system—update all the particle positions and draw them:
+
I need to make one more fairly significant change, however. With typing cats, a random phrase was evaluated as soon as it was created. The string of characters had no life span; it existed purely for the purpose of calculating its fitness. The rockets, however, need to live for a period of time before they can be evaluated—that is, they need to be given a chance to make their attempt at reaching the target. Therefore, I need to add one more method to the Population class that runs the physics simulation. This is identical to what I did in the run() method of a particle system—update all the particle positions and draw them:
live() {
for (let rocket of this.population) {
@@ -1218,7 +1218,7 @@
Example 9.2: Smart Rockets
// How many frames does a generation live for?
let lifeSpan = 500;
-// Keep track of the lifespan.
+// Keep track of the life span.
let lifeCounter = 0;
// The population
@@ -1317,9 +1317,9 @@
Making Improvements
this.recordDistance = distance;
}
-
Additionally, a rocket deserves a reward based on the speed with which it reaches its target. For that, I need a way of knowing when a rocket has hit the target. Actually, I already have one: the Obstacle class has a contains() method, and there’s no reason the target can’t also be implemented as an obstacle. It’s just an obstacle that the rocket wants to hit! I can use the contains() method to set a new hitTarget flag on each Rocket object. A rocket will stop if it hits the target, just as it stops if it hits an obstacle.
+
Additionally, a rocket deserves a reward based on the speed with which it reaches its target. For that, I need a way of knowing when a rocket has hit the target. Actually, I already have one: the Obstacle class has a contains() method, and there’s no reason the target can’t also be implemented as an obstacle. It’s just an obstacle that the rocket wants to hit! I can use the contains() method to set a new hitTarget flag on each Rocket object. A rocket will stop if it hits the target, just as it stops if it hits an obstacle:
-
// If the object reaches the target, set a Boolean flag to true.
+
// If the object reaches the target, set a Boolean flag to true.
if (target.contains(this.position)) {
this.hitTarget = true;
}
@@ -1379,8 +1379,8 @@
Interactive Selection
The innovation here isn’t the use of the GA, but rather the strategy behind the fitness function. In front of each monitor is a sensor on the floor that can detect the presence of a visitor viewing the screen. The fitness of an image is tied to the length of time that viewers look at the image. This is known as interactive selection, a GA with fitness values assigned by people.
Far from being confined to art installations, interactive selection is quite prevalent in the digital age of user-generated ratings and reviews. Could you imagine evolving the perfect song based on your Spotify ratings? Or the ideal book according to Goodreads reviews? In keeping with the book’s nature theme, however, I’ll illustrate how interactive selection works by using a population of digital flowers like the ones in Figure 9.12.
-
- 9.12: Flower design for interactive selection
+
+ Figure 9.12: Flower design for interactive selection
Each flower will have a set of properties: petal color, petal size, petal count, center color, center size, stem length, and stem color. A flower’s DNA (genotype) is an array of floating-point numbers from 0 to 1, with a single value for each property:
@@ -1411,7 +1411,7 @@
Interactive Selection
// The DNA values are assigned to flower properties
// such as petal color, petal size, and number of petals.
let genes = this.dna.genes;
- // I’ll set the RGB range to from 0 to 1 with colorMode() and use map() as needed elsewhere for drawing the flower.
+ // I’ll set the RGB range from 0 to 1 with colorMode() and use map() as needed elsewhere for drawing the flower.
let petalColor = color(genes[0], genes[1], genes[2], genes[3]);
let petalSize = map(genes[4], 0, 1, 4, 24);
let petalCount = floor(map(genes[5], 0, 1, 2, 16));
@@ -1421,7 +1421,7 @@
Interactive Selection
let stemLength = map(genes[13], 0, 1, 50, 100);
Up to this point, I haven’t done anything new. This is the same process I’ve followed in every GA example so far. What’s different is that I won’t be writing a fitness() function that computes the score based on a mathematical formula. Instead, I’ll ask the user to assign the fitness.
-
How exactly to ask a user to assign fitness is best approached as an interaction design problem and isn’t really within the scope of this book. I’m not going to launch into an elaborate discussion of how to program sliders or build your own hardware dials or create a web app enabling people to submit online scores. How you choose to acquire fitness scores is up to you and the particular application you’re developing. For this demonstration, I'll take inspiration from Sims’s Galapagos installation and simply increase a flower’s fitness whenever the mouse is over it. Then the next generation of flowers is created when an Evolve Next Generation button is pressed.
+
How exactly to ask a user to assign fitness is best approached as an interaction design problem and isn’t really within the scope of this book. I’m not going to launch into an elaborate discussion of how to program sliders or build your own hardware dials or create a web app enabling people to submit online scores. How you choose to acquire fitness scores is up to you and the particular application you’re developing. For this demonstration, I’ll take inspiration from Sims’s Galapagos installation and simply increase a flower’s fitness whenever the mouse is over it. Then the next generation of flowers is created when an Evolve Next Generation button is pressed.
Look at how the steps of the GA—selection and reproduction—are applied in the nextGeneration() function, which is triggered by the mousePressed() event attached to the p5.js button element. Fitness is increased as part of the Population class’s rollover() method, which detects the presence of the mouse over any given flower design. You can find more details about the sketch in the accompanying example code on the book’s website.
Example 9.4: Interactive Selection
@@ -1462,10 +1462,10 @@
Example 9.4: Interactive Selection
-
This example is just a demonstration of the idea of interactive selection and doesn’t achieve a particularly meaningful result. For one, I didn’t take much care in the visual design of the flowers; they’re just a few simple shapes with different sizes and colors. (See if you can spot the use of polar coordinates in the code, though!) Sims used more elaborate mathematical functions as the genotype for his images. You might also consider a vector-based approach, in which a design’s genotype is a set of points and/or paths.
-
The more significant problem here, however, is one of time. In the natural world, evolution occurs over millions of years. In the computer simulation world of the chapter’s first examples, the populations are able to evolve behaviors relatively quickly because the new generations are being produced algorithmically. In the typing cat example, a new generation is born in each cycle through draw() (approximately 60 per second). Each generation of smart rockets has a lifespan of 250 frames—still a mere blink of the eye in evolutionary time. In the case of interactive selection, however, you have to sit and wait for a person to rate each and every member of the population before you can get to the next generation. A large population would be unreasonably tedious for the user to evaluate—not to mention, how many generations could you stand to sit through?
+
This example is just a demonstration of the idea of interactive selection and doesn’t achieve a particularly meaningful result. For one, I didn’t take much care in the visual design of the flowers; they’re just a few simple shapes with different sizes and colors. (See if you can spot the use of polar coordinates in the code, though!) Sims used more elaborate mathematical functions as the genotype for his images. You might also consider a vector-based approach, in which a design’s genotype is a set of points or paths.
+
The more significant problem here, however, is one of time. In the natural world, evolution occurs over millions of years. In the computer simulation world of the chapter’s first examples, the populations are able to evolve behaviors relatively quickly because the new generations are being produced algorithmically. In the typing cat example, a new generation is born in each cycle through draw() (approximately 60 per second). Each generation of smart rockets has a life span of 250 frames—still a mere blink of the eye in evolutionary time. In the case of interactive selection, however, you have to sit and wait for a person to rate each and every member of the population before you can get to the next generation. A large population would be unreasonably tedious for the user to evaluate—not to mention, how many generations could you stand to sit through?
You can certainly get around this problem in clever ways. Sims’s Galapagos exhibit concealed the rating process from the viewers, as it occurred through the normal behavior of looking at artwork in a gallery setting. Building a web application that would allow many people to rate a population in a distributed fashion is also a good strategy for achieving ratings for large populations quickly.
-
In the end, the key to a successful interactive selection system boils down to the same keys previously established. What is the genotype and phenotype? And how do you calculate fitness—or in this case, what’s your strategy for assigning fitness according to interaction?
+
In the end, the key to a successful interactive selection system boils down to the same keys previously established. What are the genotype and phenotype? And how do you calculate fitness—or in this case, what’s your strategy for assigning fitness according to interaction?
Exercise 9.13
Build your own interactive selection project. In addition to a visual design, consider evolving sounds—for example, a short sequence of tones. Can you devise a strategy, such as a web application or physical sensor system, to acquire ratings from many people over time?
@@ -1548,12 +1548,12 @@
Ecosystem Simulation
/* All the rest of update() */
If health drops below 0, the bloop dies:
-
// A method to test whether the bloop is alive or dead.
+
// A method to test whether the bloop is alive or dead
dead() {
return (this.health < 0.0);
}
This is a good first step, but I haven’t really achieved anything. After all, if all bloops start with 100 health points and lose health at the same rate, then all bloops will live for the exact same amount of time and die together. If every single bloop lives the same amount of time, each one has an equal chance of reproducing, and therefore no evolutionary change will occur.
-
You can achieve variable lifespans in several ways with a more sophisticated world. One approach is to introduce predators that eat bloops. Faster bloops would be more likely to escape being eaten, leading to the evolution of increasingly faster bloops. Another option is to introduce food. When a bloop eats food, its health points increase, extending its life.
+
You can achieve variable life spans in several ways with a more sophisticated world. One approach is to introduce predators that eat bloops. Faster bloops would be more likely to escape being eaten, leading to the evolution of increasingly faster bloops. Another option is to introduce food. When a bloop eats food, its health points increase, extending its life.
Let’s assume I have an array of vector positions called food. I could test each bloop’s proximity to each food position. If the bloop is close enough, it eats the food (which is then removed from the world) and increases its health:
eat(food) {
// Check all the food vectors.
@@ -1596,7 +1596,7 @@
Genotype and Phenotype
class Bloop {
constructor(x, y, dna) {
this.dna = dna;
- // DNA will determine size and maxspeed.
+ // DNA will determine size and max speed.
// The bigger the bloop, the slower it is.
this.maxSpeed = map(this.dna.genes[0], 0, 1, 15, 0);
this.r = map(this.dna.genes[0], 0, 1, 0, 25);
@@ -1611,7 +1611,7 @@
Selection and Reproduction
To implement this selection algorithm, I can write a method in the Bloop class that picks a random number every frame. If the number is less than 0.01 (1 percent), a new bloop is born:
// This method will return a new child bloop.
reproduce() {
- // A 1% chance of executing the code inside the if statement
+ // A 1% chance of executing the code inside the if statement
if (random(1) < 0.01) {
/* A Bloop baby! */
}
@@ -1634,7 +1634,7 @@
Selection and Reproduction
copy() {
// Create new DNA (with random genes).
let newDNA = new DNA();
- //{!1} Overwrite the random genes with a copy this DNA’s genes.
+ //{!1} Overwrite the random genes with a copy of this DNA’s genes.
newDNA.genes = this.genes.slice();
return newDNA;
}
@@ -1703,7 +1703,7 @@
Example 9.5: An Evolving Ecosystem
-
If you guessed medium-sized bloops with medium speed, you’re right. With the design of this system, bloops that are large are simply too slow to find food. And bloops that are fast are too small to find food. The ones that are able to live the longest tend to be in the middle, large enough and fast enough to find food (but not too large or too fast). Some anomalies also exist. For example, if a bunch of large bloops happen to end up in the same position (and barely move because they are so large), they may all die out suddenly, leaving a lot of food for one large bloop that happens to be there to eat and allowing a mini-population of large bloops to sustain themselves for a period of time in one position.
+
If you guessed medium-sized bloops with medium speed, you’re right. With the design of this system, bloops that are large are simply too slow to find food. And bloops that are fast are too small to find food. The ones that are able to live the longest tend to be in the middle, large enough and fast enough to find food (but not too large or too fast). Some anomalies also exist. For example, if a bunch of large bloops happen to end up in the same position (and barely move because they are so large), they may all die out suddenly, leaving a lot of food for one large bloop that happens to be there to eat and allowing a mini population of large bloops to sustain themselves for a period of time in one position.
This example is rather simplistic given its single gene and cloning instead of crossover. Here are some suggestions for applying the bloop example in a more elaborate ecosystem simulation.
-
- Photo by Pi3.124, Museo Machu Picchu, Casa Concha, Cusco, CC BY-SA 4.0.
+
+ Khipu on display at the Machu Picchu Museum, Cusco, Peru (photo by Pi3.124)
Khipu
The khipu (or quipu) is an ancient Incan device used for recordkeeping and communication. It comprised a complex system of knotted cords to encode and transmit information. Each colored string and knot type and pattern represented specific data, such as census records or calendrical information. Interpreters, known as quipucamayocs, acted as a kind of accountant and decoded the stringed narrative into understandable information.
@@ -25,7 +25,7 @@
Khipu
Fortunately, as you’ve seen throughout this book, developing engaging animated systems with code doesn’t require scientific rigor or accuracy. Designing a smart rocket isn’t rocket science, and neither is designing an artificial neural network brain science. It’s enough to simply be inspired by the idea of brain function.
In this chapter, I’ll begin with a conceptual overview of the properties and features of neural networks and build the simplest possible example of one, a network that consists of a single neuron. I’ll then introduce you to more complex neural networks by using the ml5.js library. This will serve as a foundation for Chapter 11, the grand finale of this book, where I’ll combine GAs with neural networks for physics simulation.
Introducing Artificial Neural Networks
-
Computer scientists have long been inspired by the human brain. In 1943, Warren S. McCulloch, a neuroscientist, and Walter Pitts, a logician, developed the first conceptual model of an artificial neural network. In their paper, “A Logical Calculus of the Ideas Immanent in Nervous Activity,” they describe a neuron as a single computational cell living in a network of cells that receives inputs, processes those inputs, and generates an output.
+
Computer scientists have long been inspired by the human brain. In 1943, Warren S. McCulloch, a neuroscientist, and Walter Pitts, a logician, developed the first conceptual model of an artificial neural network. In their paper “A Logical Calculus of the Ideas Immanent in Nervous Activity,” they describe a neuron as a single computational cell living in a network of cells that receives inputs, processes those inputs, and generates an output.
Their work, and the work of many scientists and researchers who followed, wasn’t meant to accurately describe how the biological brain works. Rather, an artificial neural network (hereafter referred to as just a neural network) was intended as a computational model based on the brain, designed to solve certain kinds of problems that were traditionally difficult for computers.
Some problems are incredibly simple for a computer to solve but difficult for humans like you and me. Finding the square root of 964,324 is an example. A quick line of code produces the value 982, a number my computer can compute in less than a millisecond, but if you asked me to calculate that number myself, you’d be in for quite a wait. On the other hand, certain problems are incredibly simple for you or me to solve, but not so easy for a computer. Show any toddler a picture of a kitten or puppy, and they’ll quickly be able to tell you which one is which. Listen to a conversation in a noisy café and focus on just one person’s voice, and you can effortlessly comprehend their words. But need a machine to perform one of these tasks? Scientists have spent entire careers researching and implementing complex solutions, and neural networks are one of them.
Here are some of the easy-for-a-human, difficult-for-a-machine applications of neural networks in software today:
@@ -34,13 +34,13 @@
Introducing Artificial Neural Ne
Time-series prediction and anomaly detection: Neural networks are utilized both in forecasting, such as predicting stock market trends or weather patterns, and in recognizing anomalies, which can be applied to areas like cyberattack detection and fraud prevention.
Natural language processing (NLP): One of the biggest developments in recent years has been the use of neural networks for processing and understanding human language. They’re used in various tasks including machine translation, sentiment analysis, and text summarization, and are the underlying technology behind many digital assistants and chatbots.
Signal processing and soft sensors: Neural networks play a crucial role in devices like cochlear implants and hearing aids by filtering noise and amplifying essential sounds. They’re also involved in soft sensors, software systems that process data from multiple sources to give a comprehensive analysis of the environment.
-
Control and adaptive decision-making systems: These applications range from autonomous vehicles like self-driving cars and drones, to adaptive decision-making used in game playing, pricing models, and recommendation systems on media platforms.
+
Control and adaptive decision-making systems: These applications range from autonomous vehicles like self-driving cars and drones to adaptive decision-making used in game playing, pricing models, and recommendation systems on media platforms.
Generative models: The rise of novel neural network architectures has made it possible to generate new content. These systems can synthesize images, enhance image resolution, transfer style between images, and even generate music and video.
-
Covering the full gamut of applications for neural networks would merit an entire book (or series of books), and by the time that book was printed, it would probably be out-of-date. Hopefully, this list it gives you an overall sense of the features and possibilities.
+
Covering the full gamut of applications for neural networks would merit an entire book (or series of books), and by the time that book was printed, it would probably be out of date. Hopefully, this list gives you an overall sense of the features and possibilities.
How Neural Networks Work
In some ways, neural networks are quite different from other computer programs. The computational systems I’ve been writing so far in this book are procedural: a program starts at the first line of code, executes it, and goes on to the next, following instructions in a linear fashion. By contrast, a true neural network doesn’t follow a linear path. Instead, information is processed collectively, in parallel, throughout a network of nodes, with each node representing a neuron. In this sense, a neural network is considered a connectionist system.
-
In other ways, neural networks aren’t so different from some of the programs you’ve seen. A neural network exhibits all the hallmarks of a complex system, much like a cellular automaton or a flock of boids. Remember that each individual boid was simple to understand, yet by following only three rules—separation, alignment, cohesion—it contributed to complex behaviors? Each individual element in a neural network is equally simple to understand. It reads an input (a number), processes it, and generates an output (another number). That’s all there is to it, and yet a network of many neurons can exhibit incredibly rich and intelligent behaviors, echoing the complex dynamics seen in a flock of boids.
+
In other ways, neural networks aren’t so different from some of the programs you’ve seen. A neural network exhibits all the hallmarks of a complex system, much like a cellular automaton or a flock of boids. Remember how each individual boid was simple to understand, yet by following only three rules—separation, alignment, cohesion—it contributed to complex behaviors? Each individual element in a neural network is equally simple to understand. It reads an input (a number), processes it, and generates an output (another number). That’s all there is to it, and yet a network of many neurons can exhibit incredibly rich and intelligent behaviors, echoing the complex dynamics seen in a flock of boids.
@@ -69,7 +69,7 @@
The Perceptron
Figure 10.3: A simple perceptron with two inputs and one output
A perceptron follows the feed-forward model: data passes (feeds) through the network in one direction. The inputs are sent into the neuron, are processed, and result in an output. This means the one-neuron network diagrammed in Figure 10.3 reads from left to right (forward): inputs come in, and output goes out.
-
Say I have a perceptron with two inputs, the values 12 and 4. In machine learning, it’s customary to denote each input with an x, so I’ll call these inputs x_0 and x_1:
+
Say I have a perceptron with two inputs, the values 12 and 4. In machine learning, it’s customary to denote each input with an x, so I’ll call these inputs x_0 and x_1:
@@ -134,13 +134,13 @@
Step 1: Weight the Inputs
Step 2: Sum the Inputs
The weighted inputs are then added together:
-
6 + -4 = 2
+
6 + -4 = 2
Step 3: Generate the Output
The output of a perceptron is produced by passing the sum through an activation function that reduces the output to one of two possible values. Think of this binary output as an LED that’s only off or on, or as a neuron in an actual brain that either fires or doesn’t fire. The activation function determines whether the perceptron should “fire.”
Activation functions can get a little bit hairy. If you start reading about them in an AI textbook, you may soon find yourself reaching in turn for a calculus textbook. However, your new friend the simple perceptron provides an easier option that still demonstrates the concept. I’ll make the activation function the sign of the sum. If the sum is a positive number, the output is 1; if it’s negative, the output is –1:
-
\text{sign}(2) = +1
+
\text{sign}(2) = +1
Putting It All Together
-
Putting the preceding three parts together, here are the steps of the perceptron algorithm:
+
Putting the preceding three parts together, here are the steps of the perceptron algorithm:
For every input, multiply that input by its weight.
Sum all the weighted inputs.
@@ -168,7 +168,7 @@
Putting It All Together
return -1;
}
}
-
You might be wondering about how I’m handling the value of 0 in the activation function. Is 0 positive or negative? The deep philosophical implications of this question aside, I’m choosing here to arbitrarily return a –1 for 0, but I could easily change the > to >= to go the other way. Depending on the application, this decision could be significant, but for demonstration purposes here, I can just pick one.
+
You might be wondering how I’m handling the value of 0 in the activation function. Is 0 positive or negative? The deep philosophical implications of this question aside, I’m choosing here to arbitrarily return a –1 for 0, but I could easily change the > to >= to go the other way. Depending on the application, this decision could be significant, but for demonstration purposes here, I can just pick one.
Now that I’ve explained the computational process of a perceptron, let’s look at an example of one in action.
Simple Pattern Recognition Using a Perceptron
I’ve mentioned that neural networks are commonly used for pattern recognition. The scenarios outlined earlier require more complex networks, but even a simple perceptron can demonstrate a fundamental type of pattern recognition in which data points are classified as belonging to one of two groups. For instance, imagine you have a dataset of plants and want to identify them as either xerophytes (plants that have evolved to survive in an environment with little water and lots of sunlight, like the desert) or hydrophytes (plants that have adapted to living submerged in water, with reduced light). That’s how I’ll use my perceptron in this section.
@@ -178,10 +178,10 @@
Simple Pattern Recognitio
Figure 10.4: A collection of points in 2D space divided by a line, representing plant categories according to their water and sunlight intake
In truth, I don’t need a neural network—not even a simple perceptron—to tell me whether a point is above or below a line. I can see the answer for myself with my own eyes, or have my computer figure it out with simple algebra. But just like solving a problem with a known answer—“to be or not to be”—was a convenient first test for the GA in Chapter 9, training a perceptron to categorize points as being on one side of a line versus the other will be a valuable way to demonstrate the algorithm of the perceptron and verify that it’s working properly.
-
To solve this problem, I’ll give my perceptron two inputs: x_0 is the x-coordinate of a point, representing a plant’s amount of sunlight, and x_1 is the y-coordinate of that point, representing the plant’s amount of water. The perceptron then guesses the plant’s classification according to the sign of the weighted sum of these inputs. If the sum is positive, the perceptron outputs a +1, signifying a hydrophyte (above the line). If the sum is negative, it outputs a –1, signifying a xerophyte (below the line). Figure 10.5 shows this perceptron, (note the shorthand of w_0 and w_1 for the weights).
+
To solve this problem, I’ll give my perceptron two inputs: x_0 is the x-coordinate of a point, representing a plant’s amount of sunlight, and x_1 is the y-coordinate of that point, representing the plant’s amount of water. The perceptron then guesses the plant’s classification according to the sign of the weighted sum of these inputs. If the sum is positive, the perceptron outputs a +1, signifying a hydrophyte (above the line). If the sum is negative, it outputs a –1, signifying a xerophyte (below the line). Figure 10.5 shows this perceptron (note the shorthand of w_0 and w_1 for the weights).
-
- Figure 10.5: A perceptron with two inputs (x_0 and x_1), a weight for each input (w_0 and w_1), as well as a processing neuron that generates the output
+
+ Figure 10.5: A perceptron with two inputs (x_0 and x_1), a weight for each input (w_0 and w_1), and a processing neuron that generates the output
This scheme has a pretty significant problem, however. What if my data point is (0, 0), and I send this point into the perceptron as inputs x_0 = 0 and x_1=0? No matter what the weights are, multiplication by 0 is 0. The weighted inputs are therefore still 0, and their sum will be 0 too. And the sign of 0 is . . . hmmm, there’s that deep philosophical quandary again. Regardless of how I feel about it, the point (0, 0) could certainly be above or below various lines in a 2D world. How is the perceptron supposed to interpret it accurately?
To avoid this dilemma, the perceptron requires a third input, typically referred to as a bias input. This extra input always has the value of 1 and is also weighted. Figure 10.6 shows the perceptron with the addition of the bias.
@@ -319,14 +319,14 @@
The Perceptron Code
With steering, however, I had an additional variable that controlled the vehicle’s ability to steer: the maximum force. A high maximum force allowed the vehicle to accelerate and turn quickly, while a lower force resulted in a slower velocity adjustment. The neural network will use a similar strategy with a variable called the learning constant:
A high learning constant causes the weight to change more drastically. This may help the perceptron arrive at a solution more quickly, but it also increases the risk of overshooting the optimal weights. A small learning constant will adjust the weights more slowly and require more training time, but will allow the network to make small adjustments that could improve overall accuracy.
-
Assuming the addition of a learningConstant property to the Perceptronclass, I can now write a training method for the perceptron following the steps I outlined earlier:
+
Assuming the addition of a learningConstant property to the Perceptron class, I can now write a training method for the perceptron following the steps I outlined earlier:
// Step 1: Provide the inputs and known answer.
// These are passed in as arguments to train().
train(inputs, desired) {
// Step 2: Guess according to those inputs.
let guess = this.feedforward(inputs);
- // Step 3: Compute the error (the difference between desired and guess).
+ // Step 3: Compute the error (the difference between desired and guess).
let error = desired - guess;
//{!3} Step 4: Adjust all the weights according to the error and learning constant.
@@ -399,7 +399,7 @@
The Perceptron Code
let x = random(-100, 100);
let y = random(-100, 100);
How do I know if this point is above or below the line? The line function f(x) returns the y value on the line for that x-position. I’ll call that y_\text{line}:
-
// The y position on the line
+
// The y position on the line
let yline = f(x);
If the y value I’m examining is above the line, it will be greater than y_\text{line}, as in Figure 10.9.
@@ -410,7 +410,7 @@
The Perceptron Code
// Start with a value of –1.
let desired = -1;
if (y > yline) {
- //{!1} The answer becomes +1 if y is above the line.
+ //{!1} The answer becomes +1 if y is above the line.
desired = 1;
}
I can then make an input array to go with the desired output:
@@ -441,8 +441,8 @@
Example 10.1: The Perceptron
function setup() {
createCanvas(640, 240);
- // The perceptron has three inputs (including bias) and a learning rate of 0.01.
- perceptron = new Perceptron(3, 0.01);
+ // The perceptron has three inputs (including bias) and a learning rate of 0.0001.
+ perceptron = new Perceptron(3, 0.0001);
//{!1} Make 2,000 training data points.
for (let i = 0; i < 2000; i++) {
@@ -514,7 +514,7 @@
Putting the “Network” in Neur
Figure 10.10: Data points that are linearly separable (left) and data points that are nonlinearly separable, as a curve is required to separate the points (right)
Now imagine you’re classifying plants according to soil acidity (x-axis) and temperature (y-axis). Some plants might thrive in acidic soils but only within a narrow temperature range, while other plants prefer less acidic soils but tolerate a broader range of temperatures. A more complex relationship exists between the two variables, so a straight line can’t be drawn to separate the two categories of plants, acidophilic and alkaliphilic (see Figure 10.10, right). A lone perceptron can’t handle this type of nonlinearly separable problem. (Caveat here: I’m making up these scenarios. If you happen to be a botanist, please let me know if I’m anywhere close to reality.)
-
One of the simplest examples of a nonlinearly separable problem is XOR (exclusive or). This is a logical operator, similar to the more familiar AND and OR. For A AND B to be true, both A and B must be true. With OR, either A or B (or both) can be true. These are both linearly separable problems. The truth tables in Figure 10.11 show their solution space. Each true/false value in the table shows the output for a particular combination of true/false inputs.
+
One of the simplest examples of a nonlinearly separable problem is XOR (exclusive or). This is a logical operator, similar to the more familiar AND and OR. For A AND B to be true, both A and B must be true. With OR, either A or B (or both) can be true. These are both linearly separable problems. The truth tables in Figure 10.11 show their solution space. Each true or false value in the table shows the output for a particular combination of true or false inputs.
Figure 10.11: Truth tables for the AND and OR logical operators. The true and false outputs can be separated by a line.
@@ -526,7 +526,7 @@
Putting the “Network” in Neur
-
The XOR operator is the equivalent of (OR) AND (NOT AND). In other words, A XOR B evaluates to true only if one of the inputs is true. If both inputs are false or both are true, the output is false. To illustrate, let’s say you’re having pizza for dinner. You love pineapple on pizza, and you love mushrooms on pizza, but put them together, and yech! And plain pizza, that’s no good either! Here’s a table to describe that scenario and whether you want to eat the pizza or not.
+
The XOR operator is the equivalent of (OR) AND (NOT AND). In other words, A XOR B evaluates to true only if one of the inputs is true. If both inputs are false or both are true, the output is false. To illustrate, let’s say you’re having pizza for dinner. You love pineapple on pizza, and you love mushrooms on pizza, but put them together, and yech! And plain pizza, that’s no good either! Here’s a table to describe that scenario and whether you want to eat the pizza or not.
The truth table version of this is shown in Figure 10.12.
@@ -543,7 +543,7 @@
Putting the “Network” in Neur
Up until now, I’ve been visualizing a singular perceptron with one circle representing a neuron processing its input signals. Now, as I move on to larger networks, it’s more typical to represent all the elements (inputs, neurons, outputs) as circles, with arrows that indicate the flow of data. In Figure 10.13, you can see the inputs and bias flowing into the hidden layer, which then flows to the output.
Training a simple perceptron is pretty straightforward: you feed the data through and evaluate how to change the input weights according to the error. With a multilayered perceptron, however, the training process becomes more complex. The overall output of the network is still generated in essentially the same manner as before: the inputs multiplied by the weights are summed and fed forward through the various layers of the network. And you still use the network’s guess to calculate the error (desired result – guess). But now so many connections exist between layers of the network, each with its own weight. How do you know how much each neuron or connection contributed to the overall error of the network, and how it should be adjusted?
-
The solution to optimizing the weights of a multilayered network is backpropagation. This process takes the error and feeds it backward through the network so it can adjust the weights of all the connections in proportion to how much they’ve contributed to the total error. The details of backpropagation are beyond the scope of this book. The algorithm uses a variety of activation functions (one classic example is the sigmoid function) as well as some calculus. If you’re interested in continuing down this road and learning more about how backpropagation works, you can find my “Toy Neural Network” project at at the Coding Train website with accompanying video tutorials. They go through all the steps of solving XOR using a multilayered feed-forward network with backpropagation. For this chapter, however, I’d instead like to get some help and phone a friend.
+
The solution to optimizing the weights of a multilayered network is backpropagation. This process takes the error and feeds it backward through the network so it can adjust the weights of all the connections in proportion to how much they’ve contributed to the total error. The details of backpropagation are beyond the scope of this book. The algorithm uses a variety of activation functions (one classic example is the sigmoid function) as well as some calculus. If you’re interested in continuing down this road and learning more about how backpropagation works, you can find my “Toy Neural Network” project at the Coding Train website with accompanying video tutorials. They go through all the steps of solving XOR using a multilayered feed-forward network with backpropagation. For this chapter, however, I’d instead like to get some help and phone a friend.
Machine Learning with ml5.js
That friend is ml5.js. This machine learning library can manage the details of complex processes like backpropagation so you and I don’t have to worry about them. As I mentioned earlier in the chapter, ml5.js aims to provide a friendly entry point for those who are new to machine learning and neural networks, while still harnessing the power of Google’s TensorFlow.js behind the scenes.
To use ml5.js in a sketch, you must import it via a <script> element in your index.html file, much as you did with Matter.js and Toxiclibs.js in Chapter 6:
@@ -557,7 +557,7 @@
The Machine Learning Life Cycle
Choose a model. Design the architecture of the neural network. Different models are more suitable for certain types of data and outputs.
Train the model. Feed the training portion of the data through the model and allow the model to adjust the weights of the neural network based on its errors. This process is known as optimization: the model tunes the weights so they result in the fewest number of errors.
Evaluate the model. Remember the testing data that was set aside in step 2? Since that data wasn’t used in training, it provides a means to evaluate how well the model performs on new, unseen data.
-
Tune the parameters. The training process is influenced by a set of parameters (often called hyperparameters) such as the learning rate, which dictates how much the model should adjust its weights based on errors in prediction. I called this the learningConstant in the perceptron example. By fine-tuning these parameters and revisiting steps 4 (training), 3 (model selection), or even 2 (data preparation), you can often improve the model’s performance.
+
Tune the parameters. The training process is influenced by a set of parameters (often called hyperparameters) such as the learning rate, which dictates how much the model should adjust its weights based on errors in prediction. I called this the learningConstant in the perceptron example. By fine-tuning these parameters and revisiting steps 4 (training), 3 (model selection), and even 2 (data preparation), you can often improve the model’s performance.
Deploy the model. Once the model is trained and its performance is evaluated satisfactorily, it’s time to use the model out in the real world with new data!
These steps are the cornerstone of supervised machine learning. However, even though 7 is a truly excellent number, I think I missed one more critical step. I’ll call it step 0.
@@ -572,10 +572,10 @@
Classification and Regression
Figure 10.14: Labeling images as cats or dogs
Classification doesn’t happen by magic. The model must first be shown many examples of dogs and cats with the correct labels in order to properly configure the weights of all the connections. This is the training part of supervised learning.
-
The classic “Hello, world!” demonstration of machine learning and supervised learning is a classification problem of the MNIST dataset. Short for Modified National Institute of Standards and Technology, MNIST is a dataset that was collected and processed by Yann LeCun (Courant Institute, NYU), Corinna Cortes (Google Labs), and Christopher J. C. Burges (Microsoft Research). Widely used for training and testing in the field of machine learning, this dataset consists of 70,000 handwritten digits from 0 to 9; each is a 28\times28 pixel grayscale image (see Figure 10.15 for examples). Each image is labeled with its corresponding digit.
+
The classic “Hello, world!” demonstration of machine learning and supervised learning is a classification problem of the MNIST dataset. Short for Modified National Institute of Standards and Technology, MNIST is a dataset that was collected and processed by Yann LeCun (Courant Institute, NYU), Corinna Cortes (Google Labs), and Christopher J.C. Burges (Microsoft Research). Widely used for training and testing in the field of machine learning, this dataset consists of 70,000 handwritten digits from 0 to 9; each is a 28\times28-pixel grayscale image (see Figure 10.15 for examples). Each image is labeled with its corresponding digit.
-
- Figure 10.15: A selection of handwritten digits 0–9 from the MNIST dataset (image by Suvanjanprasai, CC-SA-4.0)
+
+ Figure 10.15: A selection of handwritten digits 0–9 from the MNIST dataset (courtesy of Suvanjanprasai)
MNIST is a canonical example of a training dataset for image classification: the model has a discrete number of categories to choose from (10 to be exact—no more, no less). After the model is trained on the 70,000 labeled images, the goal is for it to classify new images and assign the appropriate label, a digit from 0 to 9.
Regression, on the other hand, is a machine learning task for which the prediction is a continuous value, typically a floating-point number. A regression problem can involve multiple outputs, but thinking about just one is often simpler to start. For example, consider a machine learning model that predicts the daily electricity usage of a house based on input factors like the number of occupants, the size of the house, and the temperature outside (see Figure 10.16).
@@ -585,8 +585,8 @@
Classification and Regression
Rather than picking from a discrete set of output options, the goal of the neural network is now to guess a number—any number. Will the house use 30.5 kilowatt-hours of electricity that day? Or 48.7 kWh? Or 100.2 kWh? The output prediction could be any value from a continuous range.
Network Design
-
Knowing what problem you’re trying to solve (step 0) also has a significant bearing on the design of the neural network—in particular, on its input and output layers. I’ll demonstrate with another classic “Hello, world!” classification example from the field of data science and machine learning: the iris dataset. This dataset, which can be found in the University of California, Irvine, Machine Learning Repository, originated from the work of American botanist Edgar Anderson.
-
Anderson collected flower data over many years across multiple regions of the United States and Canada. (For more on the origins of this famous dataset, see “The Iris Data Set: In Search of the Source of Virginica” by Antony Unwin and Kim Kleinman.) After carefully analyzing the data, he built a table to classify iris flowers into three distinct species: Iris setosa, Iris virginica, and Iris versicolor (see Figure 10.17).
+
Knowing what problem you’re trying to solve (step 0) also has a significant bearing on the design of the neural network—in particular, on its input and output layers. I’ll demonstrate with another classic “Hello, world!” classification example from the field of data science and machine learning: the iris dataset. This dataset, which can be found in the Machine Learning Repository at the University of California, Irvine, originated from the work of American botanist Edgar Anderson.
+
Anderson collected flower data over many years across multiple regions of the United States and Canada. For more on the origins of this famous dataset, see “The Iris Data Set: In Search of the Source of Virginica” by Antony Unwin and Kim Kleinman. After carefully analyzing the data, Anderson built a table to classify iris flowers into three distinct species: Iris setosa, Iris virginica, and Iris versicolor (see Figure 10.17).
Figure 10.17: Three distinct species of iris flowers
@@ -716,8 +716,8 @@
Network Design
Unlike the iris classification network, which is choosing from three labels and therefore has three outputs, this network is trying to predict just one number, so it has only one output. I’ll note, however, that a single output isn’t a requirement of regression. A machine learning model can also perform a regression that predicts multiple continuous values, in which case the model would have multiple outputs.
ml5.js Syntax
-
The ml5.js library is a collection of machine learning models that can be accessed using the syntax ml5.functionName(). For example, to use a pretrained model that detects hand positions, you can use ml5.handpose(). For classifying images, you can use ml5.imageClassifier(). While I encourage you to explore all that ml5.js has to offer (I’ll reference some of these pretrained models in upcoming exercise ideas), for this chapter I’ll focus on only one function in ml5.js, ml5.neuralNetwork(), which creates an empty neural network for you to train.
-
To use this function, you must first create a JavaScript object that will configure the model being created. Here’s where some of the big-picture factors I just discussed—Is this a classification or a regression task? How many inputs and outputs?—come into play. I’ll begin by specifying the task I want the model to perform: "regression" or "classification":
+
The ml5.js library is a collection of machine learning models that can be accessed using the syntax ml5.functionName(). For example, to use a pretrained model that detects hand positions, you can use ml5.handpose(). For classifying images, you can use ml5.imageClassifier(). While I encourage you to explore all that ml5.js has to offer (I’ll reference some of these pretrained models in upcoming exercise ideas), for this chapter I’ll focus on only one function in ml5.js, ml5.neuralNetwork(), which creates an empty neural network for you to train.
+
To use this function, you must first create a JavaScript object that will configure the model being created. Here’s where some of the big-picture factors I just discussed—is this a classification or a regression task? How many inputs and outputs?—come into play. I’ll begin by specifying the task I want the model to perform ("regression" or "classification"):
let options = { task: "classification" };
let classifier = ml5.neuralNetwork(options);
This, however, gives ml5.js little to go on in terms of designing the network architecture. Adding the inputs and outputs will complete the rest of the puzzle. The iris flower classification has four inputs and three possible output labels. This can be configured as part of the options object with a single integer for the number of inputs and an array of strings listing the output labels:
@@ -734,7 +734,7 @@
ml5.js Syntax
task: "regression",
};
let energyPredictor = ml5.neuralNetwork(options);
-
You can set many other properties of the model through the options object. For example, you could specify the number of hidden layers between the inputs and outputs (there are typically several), the number of neurons in each layer, which activation functions to use, and more. In most cases, however, you can leave out these extra settings and let ml5.js make its best guess on how to design the model based on the task and data at hand.
+
You can set many other properties of the model through the options object. For example, you could specify the number of hidden layers between the inputs and outputs (there are typically several), the number of neurons in each layer, which activation functions to use, and more. In most cases, however, you can leave out these extra settings and let ml5.js make its best guess on how to design the model based on the task and data at hand.
Building a Gesture Classifier
I’ll now walk through the steps of the machine learning life cycle with an example problem well suited for p5.js, building all the code for each step along the way using ml5.js. I’ll begin at step 0 by articulating the problem. Imagine for a moment that you’re working on an interactive application that responds to gestures. Maybe the gestures are ultimately meant to be recorded via body tracking, but you want to start with something much simpler—a single stroke of the mouse (see Figure 10.20).
@@ -777,12 +777,12 @@
Choosing a Model
debug: true
};
let classifier = ml5.neuralNetwork(options);
-
That’s it! I’m done! Thanks to ml5.js, I can bypass a host of complexities such as the number of layers and neurons per layer to have, the kind of activation functions to use, and how to set up the algorithms for training the network. The library will make these decisions for me.
-
Of course, the default ml5.js model architecture may not be perfect for all cases. I encourage you to read the ml5.js reference for additional details on how to customize the model. I’ll also point out that ml5.js is able to infer the inputs and outputs from the data, so those properties aren’t entirely necessary to include here in the options object. However, for the sake of clarity (and since I’ll need to specify them for later examples), I’m including them here.
+
That’s it! I’m done! Thanks to ml5.js, I can bypass a host of complexities such as the number of layers and neurons per layer to have, the kinds of activation functions to use, and how to set up the algorithms for training the network. The library will make these decisions for me.
+
Of course, the default ml5.js model architecture may not be perfect for all cases. I encourage you to read the ml5.js documentation for additional details on how to customize the model. I’ll also point out that ml5.js is able to infer the inputs and outputs from the data, so those properties aren’t entirely necessary to include here in the options object. However, for the sake of clarity (and since I’ll need to specify them for later examples), I’m including them here.
The debug property, when set to true, turns on a visual interface for the training process. It’s a helpful tool for spotting potential issues during training and for getting a better understanding of what’s happening behind the scenes. You’ll see what this interface looks like later in the chapter.
Training the Model
Now that I have the data in a data variable and a neural network initialized in the classifier variable, I’m ready to train the model. That process starts with adding the data to the model. And for that, it turns out I’m not quite done with preparing the data.
-
Right now, my data is neatly organized in an array of objects, each containing the x- and y-components of a vector and a corresponding string label. This is a typical format for training data, but it isn’t directly consumable by ml5.js. (Sure, I could have initially organized the data into a format that ml5.js recognizes, but I’m including this extra step because it will likely be necessary when you’re using a dataset that has been collected or sourced elsewhere.) To add the data to the model, I need to separate the inputs from the outputs, so the model understands which are which.
+
Right now, my data is neatly organized in an array of objects, each containing the x- and y-components of a vector and a corresponding string label. This is a typical format for training data, but it isn’t directly consumable by ml5.js. (Sure, I could have initially organized the data into a format that ml5.js recognizes, but I’m including this extra step because it will likely be necessary when you’re using a dataset that has been collected or sourced elsewhere.) To add the data to the model, I need to separate the inputs from the outputs so that the model understands which are which.
The ml5.js library offers a fair amount of flexibility in the kinds of formats it will accept, but I’ll choose to use arrays—one for the inputs and one for the outputs. I can use a loop to reorganize each data item and add it to the model:
for (let item of data) {
// An array of two numbers for the inputs
@@ -793,7 +793,7 @@
Training the Model
classifier.addData(inputs, outputs);
}
What I’ve done here is set the shape of the data. In machine learning, this term describes the data’s dimensions and structure. It indicates how the data is organized in terms of rows, columns, and potentially even deeper, into additional dimensions. Understanding the shape of your data is crucial because it determines the way the model should be structured.
-
Here, the input data’s shape is a 1D array containing two numbers (representing x and y). The output data, similarly, is a 1D array containing just a single string label. Every piece of data going in and out of the network will follow this pattern. While this is a small and simple example, it nicely mirrors many real-world scenarios in which the inputs are numerically represented in an array, and outputs are string labels.
+
Here, the input data’s shape is a 1D array containing two numbers (representing x and y). The output data, similarly, is a 1D array containing just a single string label. Every piece of data going in and out of the network will follow this pattern. While this is a small and simple example, it nicely mirrors many real-world scenarios in which the inputs are numerically represented in an array, and the outputs are string labels.
After passing the data into the classifier, ml5.js provides a helper function to normalize it. As I’ve mentioned, normalizing data (adjusting the scale to a standard range) is a critical step in the machine learning process:
// Normalize the data.
classifier.normalizeData();
@@ -807,12 +807,12 @@
Training the Model
console.log("Training complete!");
}
Yes, that’s it! After all, the hard work has already been completed. The data was collected, prepared, and fed into the model. All that remains is to call the train() method, sit back, and let ml5.js do its thing.
-
In truth, it isn’t quite that simple. If I were to run the code as written and then test the model, the results would probably be inadequate. Here’s where another key term in machine learning comes into play: epochs. The train() method tells the neural network to start the learning process. But how long should it train for? You can think of an epoch as one round of practice, one cycle of using the entire training dataset to update the weights of the neural network. Generally speaking, the more epochs you go through, the better the network will perform, but at a certain point you’ll have diminishing returns. The number of epochs can be set by passing in an options object into train():
+
In truth, it isn’t quite that simple. If I were to run the code as written and then test the model, the results would probably be inadequate. Here’s where another key term in machine learning comes into play: epochs. The train() method tells the neural network to start the learning process. But how long should it train for? You can think of an epoch as one round of practice, one cycle of using the entire training dataset to update the weights of the neural network. Generally speaking, the more epochs you go through, the better the network will perform, but at a certain point you’ll have diminishing returns. The number of epochs can be set by passing in an options object into train():
//{!1} Set the number of epochs for training.
let options = { epochs: 25 };
classifier.train(options, finishedTraining);
-
The number of epochs is an example of a hyperparameter, a global setting for the training process. You can set others through the options object (the learning rate, for example), but I’m going to stick with the defaults. You can read more about customization options in the ml5.js reference.
-
The second argument to train() is optional, but it’s good to include one. It specifies a callback function that runs when the training process is complete—in this case, finshedTraining(). (See the “Callbacks” box for more on callback functions.) This is useful for knowing when you can proceed to the next steps in your code. Another optional callback, which I usually name whileTraining(), is triggered after each epoch. However, for my purposes, knowing when the training is done is plenty!
+
The number of epochs is an example of a hyperparameter, a global setting for the training process. You can set others through the options object (the learning rate, for example), but I’m going to stick with the defaults. You can read more about customization options in the ml5.js documentation.
+
The second argument to train() is optional, but it’s good to include one. It specifies a callback function that runs when the training process is complete—in this case, finshedTraining(). (See the “Callbacks” box for more on callback functions.) This is useful for knowing when you can proceed to the next steps in your code. Another optional callback, which I usually name whileTraining(), is triggered after each epoch. However, for my purposes, knowing when the training is done is plenty!
Callbacks
A callback function in JavaScript is a function you don’t actually call yourself. Instead, you provide it as an argument to another function, intending for it to be called back automatically at a later time (typically associated with an event, like a mouse click). You’ve seen this before when working with Matter.js in Chapter 6, where you specified a function to call whenever a collision was detected.
@@ -916,7 +916,7 @@
Example 10.2: Gesture Classifier
classifier.classify(inputs, gotResults);
}
-// Store the resulting label in the status variable for showing in the canvas.
+// Store the resulting label in the status variable for showing in the canvas.
function gotResults(error, results) {
status = results[0].label;
}
@@ -936,14 +936,14 @@
Incorporate machine learning into your ecosystem to enhance the behavior of creatures. How could classification or regression be applied?
Can you classify the creatures of your ecosystem into multiple categories? What if you use an initial population as a training dataset, and as new creatures are born, the system classifies them according to their features? What are the inputs and outputs for your system?
-
Can you use a regression to predict the lifespan of a creature based on its properties? Think about how size and speed affected the lifespan of the bloops from Chapter 9. Could you analyze how well the regression model’s predictions align with the actual outcomes?
+
Can you use a regression to predict the life span of a creature based on its properties? Think about how size and speed affected the life span of the bloops from Chapter 9. Could you analyze how well the regression model’s predictions align with the actual outcomes?
-
- Image courtesy of New York Public Library, public domain, c. 1826–1828.
+
+ Image courtesy of New York Public Library, c. 1826–1828
Star-Nosed Mole
The star-nosed mole (Condylura cristata), found mainly in the northeastern United States and eastern Canada, has a unique and highly specialized nasal organ. Evolved over numerous generations, its nose consists of 22 tentacles with over 25,000 minute sensory receptors. Despite the moles being functionally blind, these tentacles allow them to create a detailed spatial map of their surroundings. They can navigate their dark underground habitat with astonishing precision and speed, quickly identifying and consuming edible items in a matter of milliseconds.
@@ -22,7 +22,7 @@
Star-Nosed Mole
Throughout this book, you’ve explored the fundamental principles of interactive physics simulations with p5.js, dived into the complexities of agent and other rule-based behaviors, and dipped your toe into the exciting realm of machine learning. You’ve become a natural!
-
However, Chapter 10 merely scratched the surface of working with data and neural network–based machine learning—a vast landscape that would require countless sequels to this book to cover comprehensively. My goal was never go deep into neural networks, but simply to establish the core concepts in preparation for a grand finale, where I find a way to integrate machine learning into the world of animated, interactive p5.js sketches and bring together as many of our new Nature of Code friends as possible for one last hurrah.
+
However, Chapter 10 merely scratched the surface of working with data and neural network–based machine learning—a vast landscape that would require countless sequels to this book to cover comprehensively. My goal was never to go deep into neural networks, but simply to establish the core concepts in preparation for a grand finale, where I find a way to integrate machine learning into the world of animated, interactive p5.js sketches and bring together as many of our new Nature of Code friends as possible for one last hurrah.
The path forward passes through the field of neuroevolution, a style of machine learning that combines the GAs from Chapter 9 with the neural networks from Chapter 10. A neuroevolutionary system uses Darwinian principles to evolve the weights (and in some cases, the structure itself) of a neural network over generations of trial-and-error learning. In this chapter, I’ll demonstrate how to use neuroevolution with a familiar example from the world of gaming. I’ll then finish off by varying Craig Reynolds’s steering behaviors from Chapter 5 so that they are learned through neuroevolution.
Reinforcement Learning
Neuroevolution shares many similarities with another machine learning methodology that I briefly referenced in Chapter 10, reinforcement learning, which incorporates machine learning into a simulated environment. A neural network–backed agent learns by interacting with the environment and receiving feedback about its decisions in the form of rewards or penalties. It’s a strategy built around observation.
@@ -77,9 +77,9 @@
Evolving Neural Networks Is NEAT!
Instead of using traditional backpropagation, a policy, and a reward function, neuroevolution applies principles of GAs and natural selection to train the weights in a neural network. This technique unleashes many neural networks on a problem at once. Periodically, the best-performing neural networks are “selected,” and their “genes” (the network connection weights) are combined and mutated to create the next generation of networks. Neuroevolution is especially effective in environments where the learning rules aren’t precisely defined or the task is complex, with numerous potential solutions.
One of the first examples of neuroevolution can be found in the 1994 paper “Genetic Lander: An Experiment in Accurate Neuro-genetic Control” by Edmund Ronald and Marc Schoenauer. In the 1990s, traditional neural network training methods were still nascent, and this work explored an alternative approach. The paper describes how a simulated spacecraft—in a game aptly named Lunar Lander—can learn how to safely descend and land on a surface. Rather than use handcrafted rules or labeled datasets, the researchers opted to use GAs to evolve and train neural networks over multiple generations. And it worked!
In 2002, Kenneth O. Stanley and Risto Miikkulainen expanded on earlier neuroevolutionary approaches with their paper “Evolving Neural Networks Through Augmenting Topologies.” Unlike the lunar lander method that focused on evolving the weights of a neural network, Stanley and Miikkulainen introduced a method that also evolved the network’s structure itself! Their NEAT algorithm—NeuroEvolution of Augmenting Topologies—starts with simple networks and progressively refines their topology through evolution. As a result, NEAT can discover network architectures tailored to specific tasks, often yielding more optimized and effective solutions.
-
A comprehensive NEAT implementation would require going deeper into neural network architectures and working directly with TensorFlow.js. My goal instead is to emulate Ronald and Schoenauer’s original research in the modern context of the web browser with ml5.js. Rather than use the lunar lander game, I’ll give this a try with Flappy Bird. And for that, I first need to code a version of Flappy Bird where my neuroevolutionary network can operate.
+
A comprehensive NEAT implementation would require going deeper into neural network architectures and working directly with TensorFlow.js. My goal instead is to emulate Ronald and Schoenauer’s original research in the modern context of the web browser with ml5.js. Rather than use the Lunar Lander game, I’ll give this a try with Flappy Bird. And for that, I first need to code a version of Flappy Bird where my neuroevolutionary network can operate.
Coding Flappy Bird
-
Flappy Bird was created by Vietnamese game developer Dong Nguyen in 2013. In January 2014, it became the most downloaded app on the Apple App Store. However, on February 8, Nguyen announced that he was removing the game because of its addictive nature. Since then, it has become one of the most cloned games in history.
+
Flappy Bird was created by Vietnamese game developer Dong Nguyen in 2013. In January 2014, it became the most downloaded app on the Apple App Store. However, on February 8 of that year, Nguyen announced that he was removing the game because of its addictive nature. Since then, it has become one of the most cloned games in history.
Flappy Bird is a perfect example of Nolan’s law, an aphorism attributed to the founder of Atari and creator of Pong, Nolan Bushnell: “All the best games are easy to learn and difficult to master.” It’s also a terrific game for beginner coders to re-create as a learning exercise, and it fits perfectly with the concepts in this book.
To program the game with p5.js, I’ll start by defining a Bird class. This may shock you, but I’m going to skip using p5.Vector for this demonstration and instead use separate x and y properties for the bird’s position. Since the bird moves only along the vertical axis in the game, x remains constant! Therefore, the velocity (and all the relevant forces) can be a single scalar value for just the y-axis.
To simplify the code even further, I’ll add the forces directly to the bird’s velocity instead of accumulating them into an acceleration variable. In addition to the usual update(), I’ll include a flap() method for the bird to fly upward. The show() method isn’t included here as it only draws a circle. Here’s the code:
@@ -145,7 +145,7 @@
Coding Flappy Bird
}
}
To be clear, the game depicts a bird flying through pipes—the bird is moving along two dimensions while the pipes remain stationary. However, it’s simpler to code the game as if the bird is stationary in its horizontal position and the pipes are moving.
-
With a Bird and Pipe class written, I’m almost set to run the game. However, a key piece is missing: collisions. The whole game rides on the bird attempting to avoid the pipes! Fortunately, this is nothing new. You’ve seen many examples of objects checking their positions against others throughout this book. I have a design choice to make, though. A method to check collisions could logically be placed in either the Bird class (to check whether the bird hits a pipe) or in the Pipe class (to check whether a pipe hits the bird). Either can be justified, depending on your point of view.
+
With a Bird and Pipe class written, I’m almost set to run the game. However, a key piece is missing: collisions. The whole game rides on the bird attempting to avoid the pipes! Fortunately, this is nothing new. You’ve seen many examples of objects checking their positions against others throughout this book. I have a design choice to make, though. A method to check collisions could logically be placed in either the Bird class (to check whether the bird hits a pipe) or the Pipe class (to check whether a pipe hits the bird). Either can be justified, depending on your point of view.
I’ll place the method in the Pipe class and call it collides(). The code itself is a little trickier than you might think at first glance, as the method needs to check both the top and bottom rectangles of a pipe against the position of the bird. I could approach this in a variety of ways. One way is to first check whether the bird is vertically within the bounds of either rectangle (either above the bottom of the top pipe or below the top of the bottom one). But the bird is colliding with the pipe only if the bird is also horizontally within the boundaries of the pipe’s width. An elegant way to write this is to combine each of these checks with a logical and:
collides(bird) {
// Is the bird within the vertical range of the top or bottom pipe?
@@ -215,7 +215,7 @@
The Bird Brain
The y-position of the next pipe’s top (or bottom!) opening
The x-distance to the next pipe
-
The two outputs represent the bird’s two options: to flap or not to flap. With the inputs and outputs set, I can add a brain property to the bird’s constructor to hold an ml5.js neural network with the appropriate configuration. Just to demonstrate a different coding style here, I’ll skip including a separate options variable and pass the properties as an object literal directly into the ml5.neuralNetwork() function. Note the addition of a neuroEvolution property set to true. This is necessary to enable some of the features I’ll be using later in the code:
+
The two outputs represent the bird’s two options: to flap or not to flap. With the inputs and outputs set, I can add a brain property to the bird’s constructor to hold an ml5.js neural network with the appropriate configuration. Just to demonstrate a different coding style here, I’ll skip including a separate options variable and pass the properties as an object literal directly into the ml5.neuralNetwork() function. Note the addition of a neuroEvolution property set to true. This is necessary to enable some of the features I’ll be using later in the code.
constructor() {
this.brain = ml5.neuralNetwork({
// A bird’s brain receives four inputs and classifies them into one of two labels.
@@ -307,7 +307,7 @@
Variation: A Flock of Flappy Birds
GPU vs. CPU
-
Graphics processing unit (GPU): Originally designed for rendering graphics, GPUs are adept at handling a massive number of operations in parallel. This makes them excellent for the kind of math operations and computations that machine learning models frequently perform.
+
Graphics processing unit (GPU): Originally designed for rendering graphics, GPUs are adept at handling a massive number of operations in parallel. This makes them excellent for the kinds of math operations and computations that machine learning models frequently perform.
Central processing unit (CPU): Often considered the brain or general-purpose heart of a computer, a CPU handles a wider variety of tasks than the specialized GPU, but it isn’t built to do as many tasks simultaneously.
@@ -369,7 +369,7 @@
Selection: Flappy Bird Fitness
index++;
}
index--;
- //{!1} Instead of returning the entire Bird object, just the brain is returned.
+ //{!1} Instead of returning the entire Bird object, just the brain is returned.
return birds[index].brain;
}
For this algorithm to function properly, I need to first normalize the fitness values of the birds so that they collectively add up to 1:
@@ -387,7 +387,7 @@
Selection: Flappy Bird Fitness
Once normalized, each bird’s fitness is equal to its probability of being selected.
Heredity: Baby Birds
Only one step is left in the GA—reproduction. In Chapter 9, I explored in great detail the two-step process for generating a child element: crossover and mutation. Crossover is where the third key principle of heredity arrives: the DNA from the two selected parents is combined to form the child’s DNA.
-
At first glance, the idea of inventing a crossover algorithm for two neural networks might seem daunting, and yet it’s quite straightforward. Think of the individual “genes” of a bird’s brain as the weights within the neural network. Mixing two such brains boils down to creating a new neural network with each weight chosen by a virtual coin flip—the weight comes either from the first or second parent:
+
At first glance, the idea of inventing a crossover algorithm for two neural networks might seem daunting, and yet it’s quite straightforward. Think of the individual “genes” of a bird’s brain as the weights within the neural network. Mixing two such brains boils down to creating a new neural network with each weight chosen by a virtual coin flip—the weight comes from either the first or the second parent:
// Pick two parents and create a child with crossover.
let parentA = weightedSelection();
let parentB = weightedSelection();
@@ -396,7 +396,7 @@
Heredity: Baby Birds
// Mutate the child.
child.mutate(0.01);
My luck continues! The ml5.js library also provides a mutate() method that accepts a mutation rate as its primary argument. The rate determines how often a weight will be altered. For example, a rate of 0.01 indicates a 1 percent chance that any given weight will mutate. During mutation, ml5.js adjusts the weight slightly by adding a small random number to it, rather than selecting a completely new random value. This behavior mimics real-world genetic mutations, which typically introduce minor changes rather than entirely new traits. Although this default approach works for many cases, ml5.js offers more control over the process by allowing the use of a custom mutation function as an optional second argument to mutate().
-
The crossover and mutation steps need to be repeated for the size of the population to create an entire new generation of birds. This is accomplished by populating an empty local array nextBirds with the new birds. Once the population is full, the global birds array is then updated to this fresh generation:
+
The crossover and mutation steps need to be repeated for the size of the population to create an entirely new generation of birds. This is accomplished by populating an empty local array nextBirds with the new birds. Once the population is full, the global birds array is then updated to this fresh generation:
function reproduction() {
//{!1} Start with a new empty array.
let nextBirds = [];
@@ -439,7 +439,7 @@
Example 11.2: Flappy Bird w
function draw() {
- /* all the rest of draw */
+ /* All the rest of draw */
//{!4} Create the next generation when all the birds have died.
if (allBirdsDead()) {
@@ -456,7 +456,7 @@
Example 11.2: Flappy Bird w
Note the addition of a new resetPipes() function. If I don’t remove the pipes before starting a new generation, the birds may immediately restart at a position colliding with a pipe, in which case even the best bird won’t have a chance to fly! The full online code for Example 11.2 also adjusts the behavior of the birds so that they die when they leave the canvas, either by crashing into the ground or soaring too high above the top.
Exercise 11.2
-
It takes a very long time for Example 11.2 to produce any results. Could you “speed up time” by skipping the drawing of every single frame of the game to reach an optimal bird faster? (A solution is presented in “Speeding Up Time" on page XX.) Additionally, could you add an overlay that displays information about the simulation’s status, such as the number of birds still in play, the current generation, and the lifespan of the best bird?
+
It takes a very long time for Example 11.2 to produce any results. Could you “speed up time” by skipping the drawing of every single frame of the game to reach an optimal bird faster? (A solution is presented in “Speeding Up Time” on page XX.) Additionally, could you add an overlay that displays information about the simulation’s status, such as the number of birds still in play, the current generation, and the life span of the best bird?
Exercise 11.3
@@ -494,7 +494,7 @@
Steering the Neuroevolutionary Way
this.applyForce(force);
}
The neural network brain outputs two values: one for the angle of the vector and one for the magnitude. You might think to instead use these outputs for the vector’s x- and y-components. The default output range for an ml5.js neural network is from 0 to 1, however, and I want the forces to be capable of pointing in both positive and negative directions. Mapping the first output to an angle by multiplying it by TWO_PI offers the full range.
-
You may have noticed that the code includes a variable called inputs that I have yet to declare or initialize. Defining the inputs to the neural network is where you, as the designer of the system, can be the most creative. You have to consider the nature of the environment and the simulated biology and capabilities of your creatures, and decide which features are most important.
+
You may have noticed that the code includes a variable called inputs that I have yet to declare or initialize. Defining the inputs to the neural network is where you, as the designer of the system, can be the most creative. You have to consider the nature of the environment and the simulated biology and capabilities of your creatures, and then decide which features are most important.
As a first try, I’ll assign something basic for the inputs and see if it works. Since the smart rockets’ environment is static, with fixed obstacles and targets, what if the brain could learn and estimate a flow field to navigate toward its goal? As I demonstrated in Chapter 5, a flow field receives a position and returns a vector, so the neural network can mirror this functionality and use the rocket’s current x- and y-position as input. I just have to normalize the values according to the canvas dimensions:
let inputs = [this.position.x / width, this.position.y / height];
That’s it! Virtually everything else from the original example can remain unchanged: the population, the fitness function, and the selection process.
As the glow moves, the creature should take the glow’s position into account in its decision-making process, as an input to its brain. However, it isn’t sufficient to know only the light’s position; it’s the position relative to the creature’s own that’s key. A nice way to synthesize this information as an input feature is to calculate a vector that points from the creature to the glow. Essentially, I’m reinventing the seek() method from Chapter 5, using a neural network to estimate the steering force.
+
As the glow moves, the creature should take the glow’s position into account in its decision-making process, as an input to its brain. However, it isn’t sufficient to know only the light’s position; it’s the position relative to the creature’s own that’s key. A nice way to synthesize this information as an input feature is to calculate a vector that points from the creature to the glow. Essentially, I’m reinventing the seek() method from Chapter 5, using a neural network to estimate the steering force:
seek(target) {
//{!1} Calculate a vector from the position to the target.
let v = p5.Vector.sub(target.position, this.position);
@@ -668,8 +668,8 @@
A Neuroevolutionary Ecosystem
A few elements in this chapter’s examples don’t quite fit with my dream of simulating a natural ecosystem. The first goes back to an issue I raised in Chapter 9 with the introduction of the bloops. A system of creatures that all live and die together, starting completely over with each subsequent generation—that isn’t how the biological world works! I’d like to revisit this dilemma in this chapter’s context of neuroevolution.
Second, and perhaps more important, a major flaw exists in the way I’m extracting features from a scene to train a model. The creatures in Example 11.4 are all-knowing. Sure, it’s reasonable to assume that a creature is aware of its own current velocity, but I’ve also allowed each creature to know the glow’s exact location, regardless of how far away it is or what might be blocking the creature’s vision or senses. This is a bridge too far. It flies in the face of one of the main tenets of autonomous agents I introduced in Chapter 5: an agent should have a limited ability to perceive its environment.
Sensing the Environment
-
A common approach to simulating how a real-world creature (or robot) would have a limited awareness of its surroundings is to attach sensors to an agent. Think back to that mouse in the maze from the beginning of the chapter (hopefully it’s been thriving on the cheese it’s been getting as a reward), and now imagine it has to navigate the maze in the dark. Its whiskers might act as proximity sensors to detect walls and turns. The mouse whiskers can’t see the entire maze, but only sense the immediate surroundings. Another example of sensors is a bat using echolocation to navigate, or a car on a winding road that can see only what’s projected in front of its headlights.
-
I’d like to build on this idea of the whiskers (or more formally the vibrissae) found in mice, cats, and other mammals. In the real world, animals use their vibrissae to navigate and detect nearby objects, especially in dark or obscured environments (see Figure 11.5). How can I attach whisker-like sensors to my neuroevolutionary seeking creatures?
+
A common approach to simulating how a real-world creature (or robot) would have a limited awareness of its surroundings is to attach sensors to an agent. Think back to that mouse in the maze from the beginning of the chapter (hopefully it’s been thriving on the cheese it’s been getting as a reward), and now imagine it has to navigate the maze in the dark. Its whiskers might act as proximity sensors to detect walls and turns. The mouse whiskers can’t see the entire maze, but only sense the immediate surroundings. Another example of sensors is a bat using echolocation to navigate, or a car on a winding road where the driver can see only what’s projected in front of the car’s headlights.
+
I’d like to build on this idea of the whiskers (or more formally the vibrissae) found in mice, cats, and other mammals. In the real world, animals use their vibrissae to navigate and detect nearby objects, especially in dark or obscured environments (see Figure 11.5). How can I attach whisker-like sensors to my neuroevolutionary-seeking creatures?
Figure 11.5: Clawdius the cat sensing his environment with his vibrissae
@@ -710,7 +710,7 @@
Sensing the Environment
}
}
How can I determine if a creature’s sensor is touching the food? One approach could be to use raycasting. This technique is commonly employed in computer graphics to project straight lines (often representing beams of light) from an origin point in a scene to determine which objects they intersect with. Raycasting is useful for visibility and collision checks, exactly what I’m doing here!
-
While raycasting would provide a robust solution, it requires more mathematics than I’d like to delve into here. For those interested, an explanation and implementation are available in Coding Challenge #145 on the Coding Train. For this example, I’ll opt for a more straightforward approach and check whether the endpoint of a sensor lies inside the food circle (see Figure 11.6).
The logical next step might be to incorporate all the usual parts of the GA, writing a fitness function (how much food did each creature eat?) and performing selection after a fixed generational time period. But this is a great opportunity to revisit the principles of a continuous ecosystem and aim for a more sophisticated environment and set of potential behaviors for the creatures themselves. Instead of a fixed lifespan cycle for each generation, I’ll bring back Chapter 9’s health score for each creature. For every cycle through draw() that a creature lives, its health deteriorates a little bit:
+
The logical next step might be to incorporate all the usual parts of the GA, writing a fitness function (how much food did each creature eat?) and performing selection after a fixed generational time period. But this is a great opportunity to revisit the principles of a continuous ecosystem and aim for a more sophisticated environment and set of potential behaviors for the creatures themselves. Instead of a fixed life span cycle for each generation, I’ll bring back Chapter 9’s health score for each creature. For every cycle through draw() that a creature lives, its health deteriorates a little bit:
class Creature {
constructor() {
- /* All of the creature’s properties */
+ /* All of the creature's properties */
// The health starts at 100.
this.health = 100;
@@ -856,7 +856,7 @@
Learning from the Sensors
}
}
}
-
In reproduce(), I’ll use the copy() method (cloning) instead of the crossover() method (mating), with a higher than usual mutation rate to help introduce variation. (I encourage you to consider ways to incorporate crossover instead.) Here’s the code:
+
In reproduce(), I’ll use the copy() method (cloning) instead of the crossover() method (mating), with a higher-than-usual mutation rate to help introduce variation. (I encourage you to consider ways to incorporate crossover instead.) Here’s the code:
reproduce() {
//{!2} Copy and mutate rather than use crossover and mutate.
let brain = this.brain.copy();
diff --git a/content/examples/04_particles/4_1_single_particle/screenshot.png b/content/examples/04_particles/4_1_single_particle/screenshot.png
index 53618bd0..1cbe8038 100644
Binary files a/content/examples/04_particles/4_1_single_particle/screenshot.png and b/content/examples/04_particles/4_1_single_particle/screenshot.png differ
diff --git a/content/examples/04_particles/4_1_single_particle/sketch.js b/content/examples/04_particles/4_1_single_particle/sketch.js
index c7ff29ea..6596fecc 100644
--- a/content/examples/04_particles/4_1_single_particle/sketch.js
+++ b/content/examples/04_particles/4_1_single_particle/sketch.js
@@ -5,7 +5,7 @@
let particle;
function setup() {
- createCanvas(640, 240);
+ createCanvas(640,240);
particle = new Particle(width / 2, 10);
}
diff --git a/content/examples/04_particles/4_2_array_particles/screenshot.png b/content/examples/04_particles/4_2_array_particles/screenshot.png
index b457ac64..e73ed3cb 100644
Binary files a/content/examples/04_particles/4_2_array_particles/screenshot.png and b/content/examples/04_particles/4_2_array_particles/screenshot.png differ
diff --git a/content/examples/04_particles/4_2_array_particles/sketch.js b/content/examples/04_particles/4_2_array_particles/sketch.js
index e7485024..3b141ff1 100644
--- a/content/examples/04_particles/4_2_array_particles/sketch.js
+++ b/content/examples/04_particles/4_2_array_particles/sketch.js
@@ -21,5 +21,4 @@ function draw() {
particles.splice(i, 1);
}
}
-}
-
+}
\ No newline at end of file
diff --git a/content/examples/04_particles/4_3_particle_emitter/screenshot.png b/content/examples/04_particles/4_3_particle_emitter/screenshot.png
index 7c812185..dc7cad93 100644
Binary files a/content/examples/04_particles/4_3_particle_emitter/screenshot.png and b/content/examples/04_particles/4_3_particle_emitter/screenshot.png differ
diff --git a/content/examples/04_particles/4_3_particle_emitter/sketch.js b/content/examples/04_particles/4_3_particle_emitter/sketch.js
index 61a6c4c5..db6e3804 100644
--- a/content/examples/04_particles/4_3_particle_emitter/sketch.js
+++ b/content/examples/04_particles/4_3_particle_emitter/sketch.js
@@ -5,7 +5,7 @@
let emitter;
function setup() {
- createCanvas(640, 240);
+ createCanvas(640,240);
emitter = new Emitter(width / 2, 50);
}
diff --git a/content/examples/04_particles/4_4_emitters_1/screenshot.png b/content/examples/04_particles/4_4_emitters_1/screenshot.png
index 71efc48e..d4f6d732 100644
Binary files a/content/examples/04_particles/4_4_emitters_1/screenshot.png and b/content/examples/04_particles/4_4_emitters_1/screenshot.png differ
diff --git a/content/examples/04_particles/4_4_emitters_2/screenshot.png b/content/examples/04_particles/4_4_emitters_2/screenshot.png
index ac975e2c..1e3e541d 100644
Binary files a/content/examples/04_particles/4_4_emitters_2/screenshot.png and b/content/examples/04_particles/4_4_emitters_2/screenshot.png differ
diff --git a/content/examples/04_particles/4_4_multiple_emitters/screenshot.png b/content/examples/04_particles/4_4_multiple_emitters/screenshot.png
index d0ea19e8..5815172a 100644
Binary files a/content/examples/04_particles/4_4_multiple_emitters/screenshot.png and b/content/examples/04_particles/4_4_multiple_emitters/screenshot.png differ
diff --git a/content/examples/04_particles/4_6_particle_system_forces/screenshot.png b/content/examples/04_particles/4_6_particle_system_forces/screenshot.png
index baf23363..a223d14a 100644
Binary files a/content/examples/04_particles/4_6_particle_system_forces/screenshot.png and b/content/examples/04_particles/4_6_particle_system_forces/screenshot.png differ
diff --git a/content/examples/04_particles/4_6_particle_system_forces/sketch.js b/content/examples/04_particles/4_6_particle_system_forces/sketch.js
index faf2a5c6..d935ec2d 100644
--- a/content/examples/04_particles/4_6_particle_system_forces/sketch.js
+++ b/content/examples/04_particles/4_6_particle_system_forces/sketch.js
@@ -5,12 +5,12 @@
let emitter;
function setup() {
- createCanvas(640, 240);
+ createCanvas(1280, 480);
emitter = new Emitter(width / 2, 50);
}
function draw() {
- background(255);
+ background(255,30);
// Apply gravity force to all Particles
let gravity = createVector(0, 0.1);
@@ -18,4 +18,8 @@ function draw() {
emitter.addParticle();
emitter.run();
+}
+
+function mousePressed(){
+ save('screenshot.png')
}
\ No newline at end of file
diff --git a/content/examples/04_particles/example_4_7_particle_system_with_repeller/screenshot.png b/content/examples/04_particles/example_4_7_particle_system_with_repeller/screenshot.png
index 3631a1b6..e13df99d 100644
Binary files a/content/examples/04_particles/example_4_7_particle_system_with_repeller/screenshot.png and b/content/examples/04_particles/example_4_7_particle_system_with_repeller/screenshot.png differ
diff --git a/content/examples/04_particles/example_4_7_particle_system_with_repeller/sketch.js b/content/examples/04_particles/example_4_7_particle_system_with_repeller/sketch.js
index cad0679f..b0ce6a6c 100644
--- a/content/examples/04_particles/example_4_7_particle_system_with_repeller/sketch.js
+++ b/content/examples/04_particles/example_4_7_particle_system_with_repeller/sketch.js
@@ -5,9 +5,9 @@ let emitter;
let repeller;
function setup() {
- createCanvas(640, 240);
- emitter = new Emitter(width / 2, 20);
- repeller = new Repeller(width / 2, 200);
+ createCanvas(640 , 240);
+ emitter = new Emitter(width / 2, 60);
+ repeller = new Repeller(width / 2, 250);
}
function draw() {
@@ -21,4 +21,4 @@ function draw() {
emitter.run();
repeller.show();
-}
+}
\ No newline at end of file
diff --git a/content/examples/04_particles/noc_4_05_particle_system_inheritance_polymorphism/screenshot.png b/content/examples/04_particles/noc_4_05_particle_system_inheritance_polymorphism/screenshot.png
index 1882d413..646e5169 100644
Binary files a/content/examples/04_particles/noc_4_05_particle_system_inheritance_polymorphism/screenshot.png and b/content/examples/04_particles/noc_4_05_particle_system_inheritance_polymorphism/screenshot.png differ
diff --git a/content/examples/06_libraries/6_2_boxes_solved/screenshot.png b/content/examples/06_libraries/6_2_boxes_solved/screenshot.png
index 1cc07e7b..cb148b34 100644
Binary files a/content/examples/06_libraries/6_2_boxes_solved/screenshot.png and b/content/examples/06_libraries/6_2_boxes_solved/screenshot.png differ
diff --git a/content/examples/06_libraries/6_8_mouse_constraint/screenshot.png b/content/examples/06_libraries/6_8_mouse_constraint/screenshot.png
index 4b718a80..6d316fb3 100644
Binary files a/content/examples/06_libraries/6_8_mouse_constraint/screenshot.png and b/content/examples/06_libraries/6_8_mouse_constraint/screenshot.png differ
diff --git a/content/examples/10_nn/10_2_gesture_classifier/screenshot.png b/content/examples/10_nn/10_2_gesture_classifier/screenshot.png
index 7d19551c..0d4a39ad 100644
Binary files a/content/examples/10_nn/10_2_gesture_classifier/screenshot.png and b/content/examples/10_nn/10_2_gesture_classifier/screenshot.png differ
diff --git a/content/xx_1_creature_design.html b/content/xx_1_creature_design.html
index c87dedc7..deda94c2 100644
--- a/content/xx_1_creature_design.html
+++ b/content/xx_1_creature_design.html
@@ -3,7 +3,7 @@
Appendix: Creature Design
This guide is by Zannah Marsh, who created all the illustrations you see in this book.
If you aren’t sure how to start the creature design task for your Ecosystem Project, or if the thought of populating a multi-creature ecosystem feels daunting, don’t worry! You can start developing creatures by using a few visual building blocks, like basic shapes and lines, and reuse them for various results. This design task is similar to programming by reusing and repurposing code.
Though p5.js draws shapes and lines easily, I recommend using paper and pencil to sketch out designs. Working directly on paper allows you to focus on your design and to quickly evaluate and compare iterations. You won’t need to switch back and forth between thinking visually and typing code. Create your creature on paper first, then replicate it in code!
-
The cartoonists Greg Stump and David Lasky suggest that nearly everything can be drawn with just nine Ingredients; the first six are considered the basics, and the last three are extras:
+
The cartoonists Greg Stump and David Lasky suggest that nearly everything can be drawn with just nine ingredients; the first six are considered the basics, and the last three are extras:
Square, circle, and triangle
Rectangle, stretched oval, and tall triangle
@@ -14,7 +14,7 @@
Appendix: Creature Design
Figure A.1: Starting with nine ingredients for your drawing
-
Now you can start putting these visual elements together to create a creature. Your creature will live in the imaginary space of the p5 canvas, so you don’t need to make a “real” creature; you can invent something totally new!
+
Now you can start putting these visual elements together to create a creature. Your creature will live in the imaginary space of the p5.js canvas, so you don’t need to make a “real” creature; you can invent something totally new!
Here’s a design scheme, familiar to residents of Planet Earth:
A body
@@ -33,7 +33,7 @@
Appendix: Creature Design
Figure A.3: Adding details to indicate orientation
Do we love these drawings? Are they perfect? Well, maybe not. But don’t erase your work, even if you don’t like it. You’ll need all your drawings as data points to reference as you iterate on your character. Think of creature design as the process of arranging visual elements and observing how they make you feel—how you respond to them and what they suggest to you.
-
You’ll likely start with very simple creatures. Then, as you add to your ecosystem, you’ll implement behaviors and interactions. Modifying your creatures’ appearances can help visually organize and emphasize these behaviors and interactions—and perhaps even inspire them.
+
You’ll likely start with very simple creatures. Then, as you add to your ecosystem, you’ll implement behaviors and interactions. Modifying your creatures’ appearances can help you visually organize and emphasize these behaviors and interactions—and perhaps even inspire them.
Try varying elements such as these, as shown in Figure A.4:
The size and roundness or narrowness of the body
@@ -47,8 +47,8 @@
Appendix: Creature Design
As you sketch, you may discover that the form of your creature suggests a behavior or feeling—one that you can execute in code. Does your creature dart around, creep, or drift slowly? Does it have a huge mouth for gulping big meals or a tiny mouth for nibbles? Does it have massive eyes for finding tasty snacks, as shown in Figure A.5, or for spotting predators in search of snacks? Let your drawings inspire your code and vice versa.
-
- Figure A.5: Matching your creature's form to its environment
+
+ Figure A.5: Matching your creature’s form to its environment
When you’re ready to build your creatures in code, functions like translate(), rotate(), push(), and pop() are your friends, since all your character features are arranged in relation to one another. Remember that OOP will, of course, save you time and trouble. You’ll be able to reuse and modify patterns quickly.