{
"cells": [
{
"cell_type": "markdown",
"id": "7dcf0692",
"metadata": {},
"source": [
"# vector movement and Py5vector\n",
"\n",
"This tutorial is best paired with the tutorial on Object-Oriented Programming, and will use the code created within that tutorial. If you aren't yet familiar with this concept, it's worth going back and reading through the OOP tutorial. \n",
"\n",
"The word *vector* is used in a few different ways, so let's clarify: these are not the vectors used in *vector graphics*, but Euclidean vectors, also known as *geometric vectors* or *spatial vectors*. Essentially, a vector is just a way of measuring direction and magnitude (or strength) of movement. \n",
"\n",
"When we're discussing movement in a real or virtual space, we might refer to the *force* at which something moves, or the *speed*, or the *distance*. If we give something (say, an amoeba, as we'll be using in the following exercises) ten seconds to move, the *distance* that it covers in that time will depend on how fast it's going (which depends on the strength of the force that was pushing it that way). This strength of movement is the *magnitude* of the vector. It doesn't inherently say anything about the distance traveled, or the direction, but it can tell you how hard something is being \"pushed\" in that direction. \n",
"\n",
"If the amoeba below is going to move 400 pixels to the right, we don't necessarily know how fast it's moving. However, we do know its *direction* -- to the right, along the X axis of this two-dimensional space. If we wanted to move it this direction in py5, we'd say that its X position is *increasing*. \n",
"\n",
"\n",
"\n",
"Of course, much of the time directional movement isn't along just a single axis. The amoeba below is moving up and to the right, so it's moving along both the X and Y axis. Again, even without knowing its *magnitude*, we can at least know its *direction*. In py5, its X position would be *increasing* and its Y position would be *decreasing*. To accomodate for this sort of diagonal movement, two-dimensional vectors can store both an X and a Y value. In this case, the vector might be `(1, -1)` to represent movement to the right and upwards. You might not know its magnitude just by looking at it, but you can use the `.mag` method (appended to the end of any vector) to find out. In general, it's enough to point your vector in the right direction, and increase the values (like `(3, -3)`) to increase the magnitude of movement. \n",
"\n",
"We'll be using the amoeba code from our OOP tutorial to start us off. Depending on if you want to have your `Amoeba` class as a separate *module* file, you'll either have two files (my suggestions for names are at the top of the following code snippets) or a single, consolidated, all-in-one file. Either will work."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3ce54155",
"metadata": {},
"outputs": [],
"source": [
"# amoeba.py\n",
"\n",
"from py5 import *\n",
"cs = get_current_sketch()\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter):\n",
" self.x = x\n",
" self.y = y\n",
" self.d = diameter\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.x + self.nucleus['x'],\n",
" self.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(cs.frame_count/(r/2), r/8) \n",
" xp, xm = self.x+cpx, self.x-cpx\n",
" yp, ym = self.y+cpy, self.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.x, self.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.x+r, self.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.x, self.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.x-r, self.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.x, self.y-r # (back to) top vertex\n",
" )\n",
" end_shape()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8bbc54b9",
"metadata": {},
"outputs": [],
"source": [
"# microscope.py\n",
"\n",
"from amoeba import Amoeba\n",
"\n",
"bob = Amoeba(400, 200, 100)\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" bob.display()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "122f9823",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter):\n",
" self.x = x\n",
" self.y = y\n",
" self.d = diameter\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.x + self.nucleus['x'],\n",
" self.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.x+cpx, self.x-cpx\n",
" yp, ym = self.y+cpy, self.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.x, self.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.x+r, self.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.x, self.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.x-r, self.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.x, self.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"bob = Amoeba(400, 200, 100)\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" bob.display()"
]
},
{
"cell_type": "markdown",
"id": "c638421f",
"metadata": {},
"source": [
"Whatever method you use, the above code will display a single amoeba, Bob, on the screen. Let's define a new vector and use it to give Bob some movement. \n",
"\n",
"First, we'll be adding two new parameters, `xspeed` and `yspeed`, to our `Amoeba` class's initialization method. We'll then use these to form a new attribute, *propulsion*, that determines our amoeba's movement. \n",
"\n",
"We'll also need to pass those new arguments in when Bob is created. Next, in our `draw()` function in the main sketch, we'll actually adjust Bob's X and Y position based on that propulsion stat."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0b0ff3ee",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed):\n",
" self.x = x\n",
" self.y = y\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.x + self.nucleus['x'],\n",
" self.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.x+cpx, self.x-cpx\n",
" yp, ym = self.y+cpy, self.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.x, self.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.x+r, self.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.x, self.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.x-r, self.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.x, self.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"bob = Amoeba(400, 200, 100, 3, -1)\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" bob.x += bob.propulsion.x\n",
" bob.y += bob.propulsion.y\n",
" bob.display()"
]
},
{
"cell_type": "markdown",
"id": "9b360fa7",
"metadata": {},
"source": [
"Run this sketch, and Bob will fly off the screen. Each frame, their X position is increasing by 3 pixels, and their Y position is decreasing by 1 pixel. \n",
"\n",
"We've got a lot of code referencing Bob's X and Y position now, and you can notice that we add to these values separately on these lines:\n",
"\n",
"```\n",
"bob.x += bob.propulsion.x\n",
"bob.y += bob.propulsion.y\n",
"```\n",
"\n",
"If we also store the amoeba's *position* with a vector, we can do this a little more efficiently. First, let's define a new attribute, *location*, inside of `__init__()`, and use it instead of those x and y variables. After that, we'll just have to find and replace each instance of `self.x` and `self.y` with `self.location.x` and `self.location.y`. (If you're using a dedicated IDE or even a text editing program, you may have a find-and-replace feature to make things easier.) \n",
"\n",
"After that, those two lines can be replaced with just one:\n",
"\n",
"```\n",
"bob.location += bob.propulsion\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "af889c4a",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed):\n",
" self.location = Py5Vector(x, y)\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.location.x + self.nucleus['x'],\n",
" self.location.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.location.x+cpx, self.location.x-cpx\n",
" yp, ym = self.location.y+cpy, self.location.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.location.x, self.location.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.location.x+r, self.location.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.location.x, self.location.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.location.x-r, self.location.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.location.x, self.location.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"bob = Amoeba(400, 200, 100, 3, -1)\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" bob.location += bob.propulsion\n",
" bob.display()"
]
},
{
"cell_type": "markdown",
"id": "b1b97a57",
"metadata": {},
"source": [
"If you like, you can add additional forces to this simulation by adding more vectors. For example, since many single-celled organisms live in water, you could define some vector for a *current* and add this *current* to Bob's location each frame, too. Of course, in addition to adding vectors, you can always subtract them. This can be useful for the reasons you might expect (like a force that pushes your amoeba in the opposite direction), but we can also use it to force our amoebas to move towards the mouse pointer. \n",
"\n",
"In py5, we have access to the `mouse_x` and `mouse_y` variables, which track the position of the mouse cursor. Since we're using vectors, and thus directions (instead of absolute positions), we won't simply be moving the amoeba to this location; we want to move it *towards* the position of `mouse_x` and `mouse_y`. This means that we'll do a bit of subtraction to find the *difference* in space between the current location of the amoeba and the position of the mouse, and move the amoeba in that direction."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5b103dc3",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed):\n",
" self.location = Py5Vector(x, y)\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.location.x + self.nucleus['x'],\n",
" self.location.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.location.x+cpx, self.location.x-cpx\n",
" yp, ym = self.location.y+cpy, self.location.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.location.x, self.location.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.location.x+r, self.location.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.location.x, self.location.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.location.x-r, self.location.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.location.x, self.location.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"bob = Amoeba(400, 200, 100, 3, -1)\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" pointer = Py5Vector(mouse_x, mouse_y)\n",
" difference = pointer - bob.location\n",
" bob.location += difference\n",
" bob.display()"
]
},
{
"cell_type": "markdown",
"id": "7d60a37e",
"metadata": {},
"source": [
"Currently, the magnitude of this movement is a little too high. Bob will instantly \"jump\" to the mouse pointer, and stay there. \n",
"\n",
"\n",
"\n",
"Luckily, there's a method for this. We can use the `.set_limit()` method on any vector to limit its magnitude. By setting the limit of our *difference* vector to a small number, we can make this a bit more sensible."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f096059d",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed):\n",
" self.location = Py5Vector(x, y)\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.location.x + self.nucleus['x'],\n",
" self.location.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.location.x+cpx, self.location.x-cpx\n",
" yp, ym = self.location.y+cpy, self.location.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.location.x, self.location.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.location.x+r, self.location.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.location.x, self.location.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.location.x-r, self.location.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.location.x, self.location.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"bob = Amoeba(400, 200, 100, 3, -1)\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" pointer = Py5Vector(mouse_x, mouse_y)\n",
" difference = pointer - bob.location\n",
" bob.location += difference.set_limit(1)\n",
" bob.display()"
]
},
{
"cell_type": "markdown",
"id": "4baf8ae8",
"metadata": {},
"source": [
"Now, the movement added to the amoeba's location each frame is clamped at a sensibly low value. \n",
"\n",
"\n",
"\n",
"We have one amoeba on the screen, but there's no reason we can't have a dozen. You *could* manually go through naming each amoeba, creating it, updating its position... and so on, and so forth. However, we can automate this process using a *for loop*. We'll replace our current line to create Bob with a loop creating many amoebas (and storing them in a list), and then replace our lines in `draw()` to move Bob with a loop accessing this list of amoebas and calling the correct methods on all of them."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3977ab7b",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed):\n",
" self.location = Py5Vector(x, y)\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.location.x + self.nucleus['x'],\n",
" self.location.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.location.x+cpx, self.location.x-cpx\n",
" yp, ym = self.location.y+cpy, self.location.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.location.x, self.location.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.location.x+r, self.location.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.location.x, self.location.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.location.x-r, self.location.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.location.x, self.location.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"amoebas = []\n",
"\n",
"for i in range(12):\n",
" diameter = random(50, 200)\n",
" speed = 1000 / (diameter * 50)\n",
" x, y = random(800), random(400)\n",
" amoebas.append(Amoeba(x, y, diameter, speed, speed))\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" pointer = Py5Vector(mouse_x, mouse_y)\n",
" for a in amoebas:\n",
" difference = pointer - a.location\n",
" a.location += difference.set_limit(1)\n",
" a.display()"
]
},
{
"cell_type": "markdown",
"id": "6ff03f2b",
"metadata": {},
"source": [
"So that we can fully appreciate the differing movements of all of these randomized amoebas, we'll make a couple of changes. First, let's give each amoeba another attribute, a *direction* vector (created with the arguments `xdirection` and `ydirection`), that will kick in when it isn't following the mouse. \n",
"\n",
"So that we actually get to see this behavior, we'll also add a new *if* statement where the difference is being calculated. If the mouse is being held down, the amoebas will move towards it. Otherwise, they can use this new *direction* vector to move towards instead."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0ceed99c",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed, xdirection, ydirection):\n",
" self.direction = Py5Vector(xdirection, ydirection)\n",
" self.location = Py5Vector(x, y)\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.location.x + self.nucleus['x'],\n",
" self.location.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.location.x+cpx, self.location.x-cpx\n",
" yp, ym = self.location.y+cpy, self.location.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.location.x, self.location.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.location.x+r, self.location.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.location.x, self.location.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.location.x-r, self.location.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.location.x, self.location.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"amoebas = []\n",
"\n",
"for i in range(12):\n",
" diameter = random(50, 200)\n",
" speed = 1000 / (diameter * 50)\n",
" x, y = random(800), random(400)\n",
" xdirection, ydirection = random(-5, 5), random(-5, 5)\n",
" amoebas.append(Amoeba(x, y, diameter, speed, speed, xdirection, ydirection))\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" pointer = Py5Vector(mouse_x, mouse_y)\n",
" for a in amoebas:\n",
" if is_mouse_pressed:\n",
" difference = pointer - a.location\n",
" else:\n",
" difference = a.direction\n",
" a.location += difference.set_limit(1)\n",
" a.display()"
]
},
{
"cell_type": "markdown",
"id": "c0a12462",
"metadata": {},
"source": [
"\n",
"\n",
"Of course, now the amoebas are liable to drift off the screen entirely, and it can take a while for them to move back to your mouse cursor. To fix this, we can add some code so that if an amoeba exits the screen, it reappears on the opposite side of the window, preserving its original momentum."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "09aabcaf",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed, xdirection, ydirection):\n",
" self.direction = Py5Vector(xdirection, ydirection)\n",
" self.location = Py5Vector(x, y)\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.location.x + self.nucleus['x'],\n",
" self.location.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.location.x+cpx, self.location.x-cpx\n",
" yp, ym = self.location.y+cpy, self.location.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.location.x, self.location.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.location.x+r, self.location.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.location.x, self.location.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.location.x-r, self.location.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.location.x, self.location.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"amoebas = []\n",
"\n",
"for i in range(12):\n",
" diameter = random(50, 200)\n",
" speed = 1000 / (diameter * 50)\n",
" x, y = random(800), random(400)\n",
" xdirection, ydirection = random(-5, 5), random(-5, 5)\n",
" amoebas.append(Amoeba(x, y, diameter, speed, speed, xdirection, ydirection))\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" pointer = Py5Vector(mouse_x, mouse_y)\n",
" for a in amoebas:\n",
" if is_mouse_pressed:\n",
" difference = pointer - a.location\n",
" else:\n",
" difference = a.direction\n",
" a.location += difference.set_limit(1)\n",
" \n",
" # Wrapping around to the opposite edge of the screen!\n",
" r = a.d / 2\n",
" if a.location.x - r > width:\n",
" a.location.x = 0 - r\n",
" if a.location.x + r < 0:\n",
" a.location.x = width + r\n",
" if a.location.y - r > height:\n",
" a.location.y = 0 - r\n",
" if a.location.y + r < 0:\n",
" a.location.y = height + r\n",
"\n",
" a.display()"
]
},
{
"cell_type": "markdown",
"id": "a02c17ce",
"metadata": {},
"source": [
"These new *if* statements check the X and Y positions of the amoeba, and its radius, against the size of the window (with the 0 values representing the top and left sides of the screen, and *height* and *width* representing the bottom and right sides). If an amoeba crosses over, they'll appear on the opposite side of the screen -- but the position they're heading towards is preserved, since it relies on the direction of a vector!\n",
"\n",
"\n",
"\n",
"## collision detection task\n",
"\n",
"Currently, the amoebas can overlap freely, but it would be interesting if they avoided or even bounced off of each other. Your challenge is to add a *collision detection* feature to facilitate this. \n",
"\n",
"To test whether an amoeba is overlapping another amoeba, you'll add another *for loop* inside of the `a in amoebas` loop. (Remember, *a* is a name we decide ourselves, to refer to a single amoeba in the loop.) We'll also want to add some code to prevent trying to compare an amoeba's location to itself. This will use the `continue` keyword, which moves to the next iteration of the loop."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d80d7ceb",
"metadata": {},
"outputs": [],
"source": [
"# all-in-one file\n",
"\n",
"class Amoeba():\n",
" def __init__(self, x, y, diameter, xspeed, yspeed, xdirection, ydirection):\n",
" self.direction = Py5Vector(xdirection, ydirection)\n",
" self.location = Py5Vector(x, y)\n",
" self.d = diameter\n",
" self.propulsion = Py5Vector(xspeed, yspeed) # Look! A vector!\n",
" \n",
" self.nucleus = {\n",
" 'fill': ['#FF0000', '#FF9900', '#FFFF00',\n",
" '#00FF00', '#0099FF'][int(random(5))],\n",
" 'x': self.d * random(-0.15, 0.15),\n",
" 'y': self.d * random(-0.15, 0.15),\n",
" 'd': self.d / random(2.5, 4)\n",
" }\n",
" \n",
" def circle_point(self, t, r):\n",
" x = cos(t) * r\n",
" y = sin(t) * r\n",
" return [x, y]\n",
" \n",
" def display(self):\n",
" # nucleus\n",
" fill(self.nucleus['fill'])\n",
" no_stroke()\n",
" circle(\n",
" self.location.x + self.nucleus['x'],\n",
" self.location.y + self.nucleus['y'],\n",
" self.nucleus['d'])\n",
" \n",
" # cell membrane\n",
" fill(0x880099FF)\n",
" stroke('#FFFFFF')\n",
" stroke_weight(3)\n",
" \n",
" r = self.d / 2.0\n",
" cpl = r * 0.55 \n",
" cpx, cpy = self.circle_point(frame_count/(r/2), r/8) \n",
" xp, xm = self.location.x+cpx, self.location.x-cpx\n",
" yp, ym = self.location.y+cpy, self.location.y-cpy\n",
" begin_shape()\n",
" vertex(\n",
" self.location.x, self.location.y-r # top vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+cpl, yp-r, xm+r, ym-cpl,\n",
" self.location.x+r, self.location.y # right vertex\n",
" )\n",
" bezier_vertex(\n",
" xp+r, yp+cpl, xm+cpl, ym+r,\n",
" self.location.x, self.location.y+r # bottom vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-cpl, yp+r, xm-r, ym+cpl,\n",
" self.location.x-r, self.location.y # left vertex\n",
" )\n",
" bezier_vertex(\n",
" xp-r, yp-cpl, xm-cpl, ym-r,\n",
" self.location.x, self.location.y-r # (back to) top vertex\n",
" )\n",
" end_shape()\n",
" \n",
"amoebas = []\n",
"\n",
"for i in range(10):\n",
" diameter = random(50, 200)\n",
" speed = 1000 / (diameter * 50)\n",
" x, y = random(800), random(400)\n",
" xdirection, ydirection = random(-5, 5), random(-5, 5)\n",
" amoebas.append(Amoeba(x, y, diameter, speed, speed, xdirection, ydirection))\n",
"\n",
"def setup():\n",
" size(800, 400)\n",
"\n",
"def draw():\n",
" background('#004477')\n",
" pointer = Py5Vector(mouse_x, mouse_y)\n",
" for a in amoebas:\n",
" if is_mouse_pressed:\n",
" difference = pointer - a.location\n",
" else:\n",
" difference = a.direction\n",
" a.location += difference.set_limit(1)\n",
" \n",
" # Wrapping around to the opposite edge of the screen!\n",
" r = a.d / 2\n",
" if a.location.x - r > width:\n",
" a.location.x = 0 - r\n",
" if a.location.x + r < 0:\n",
" a.location.x = width + r\n",
" if a.location.y - r > height:\n",
" a.location.y = 0 - r\n",
" if a.location.y + r < 0:\n",
" a.location.y = height + r\n",
" \n",
"\n",
" a.display()\n",
" \n",
" for b in amoebas:\n",
"\n",
" if a is b:\n",
" continue\n",
"\n",
" # ... otherwise, what should happen?"
]
},
{
"cell_type": "markdown",
"id": "2f5e8242",
"metadata": {},
"source": [
"Think about how you might use a vector to push apart two overlapping amoeba. You can use the radius of each amoeba (which you might already know you can get by dividing the diameter of each amoeba by two - `a.d/2` and `b.d/2`) to determine if they're overlapping. If the distance between their locations is less than their radii added together, they're overlapping. Otherwise, they're far enough away to avoid an overlap. \n",
"\n",
"To test what happens when an overlap does occur, you can always draw just *two* amoebas (by changing the loop to use `range(2)` and get them to move towards your mouse cursor until they intersect. \n",
"\n",
"You may find it helpful to draw a line between their locations, or to calculate a *distance* vector somehow and print the value of `distance.mag` to see how it changes as they move closer to each other. \n",
"\n",
"Good luck!"
]
}
],
"metadata": {
"jupytext": {
"formats": "ipynb,md:myst",
"text_representation": {
"extension": ".md",
"format_name": "myst",
"format_version": 0.13,
"jupytext_version": "1.14.0"
}
},
"kernelspec": {
"display_name": "py5",
"language": "python",
"name": "py5"
},
"source_map": [
13,
31,
98,
113,
184,
192,
266,
285,
357,
363,
437,
445,
519,
527,
608,
614,
700,
706,
804,
816,
922
]
},
"nbformat": 4,
"nbformat_minor": 5
}