A variable is a placeholder for a value
New syntax:
creatureX = 50
rect( creatureX, 100, 20, 50 )
New vocabulary:
You create a variable by assigning it a value, as in the first line above. (Unlike in many other programming languages, in Python you do not need to declare a variable before using it.)
Then you can use that variable throughout your code. You can use basic arithmetic to modify it's value. For example using:
+
— addition
-
— subtraction
*
— multiplication
/
— division
And you can even make one variable depend on the value of others. For example:
creature1X = 100
creature2X = 500
midpoint = ( creature1X + creature2X ) / 2
# What would the value of midpoint be now?
Anytime you want something in your code to change (or "vary") you need to add one or more variables.
We thought a bit about how many variables you would need to represent a given composition, and this can be a useful first step in project planning.
Question: If you wanted to draw 3 creatures and you want them to move around on the screen horizontally and vertically, how many variables would you need?
Answer: You would need 6 variables: one for the x and y values of each creature.
And if you then also wanted the height of each creature to change? You would need three more.
So far we have been working with Processing in what is known as static mode. We've been creating sketches that draw some shapes and then end.
Today we are going to introduce dynamic movement and respond to user input in what Processing refers to as interactive mode.
Let's think about ways of representing movement, motion, and gestures.
Below is an image from Jean Paul Riopelle, an abstract expressionist painter. The period in which Abstract Expressionism was popular occured just after WWII. AE was understood to be using color and shape to create visual compositions that were often wholly abstract, not representations of any actual objects in the world, and were often interpreted as being highly expressive, depicting only feelings, energy, mood, or affect.
Taking up some of the main themes in abstract expressionism, computer art pioneer Vera Molnar prioritized chance and the mistake in her work. This allowed for her works to have a kind of motion and gesture akin to motion and gesture you might find in early abstract expressionist work.
In an interview with Hans Ulrich Obrist, he asks Vera: what is the role of randomness in your work? Is randomness at the heart of what you do? Or not? And she replies:
Listen, if you replace the word ‘random’ with ‘intuition’, there you have it. With intuition, suddenly you say – now what if I used a curve instead of a straight line and what sort of a curve? And then you try it – that’s intuition. Randomness does the same thing.
For this week, as we begin to learn about using code to respond to user input, let's keep in mind this idea of creating compositions that capture or represent movement and motion, and do so in a way that aim to be expressive of mood, affect, or feeling of the person creating them.
There is much precedent within digital art for thinking about these kinds of representations. Artists like Camille Utterback, Golan Levin, Zach Lieberman, and others, have created work in a genre often referred to as interactive art. (And we can think of what Lev Manovich's "myth of interactivity" here: that human-computer interaction is always already interactive and so using the word to describe work in this genre is tautological.) But these kinds of "interactive" works rely on user input, in the form of movement, gesture, or play, is often necessary to enact or complete the piece.
Utterback's pieces, for example, use the movement and actions of viewers to generated colorful expressive compositions full of wide arrays of texture and form. They use sophisticated computer vision techniques to sense user motion via the motion of bodies standing in front of a large screen. For this week we will focus only on mouse movement. But towards the end of the semester there will be the opportuntity to play with more advanced computer vision techniques.
Golan Levin's project Yellowtail (1998-2010) is another great example. This project was originally created before Processing existed, but then was re-written for Processing in 2007. At one time it was easy to run in a web browser, but due to various technology and business decisions, most browsers no longer support Java applets, and so it probably won't run for most of you. Levin has made it available as an iOS app for 99¢. You can watch some video documentation of the project here.
Bringing together these threads (interactive art, abstract expressionism) is a fun project simply called "Jackson Pollock" by designer Miltos Manetas: jacksonpollock.org. (In order to run it you may need to download the .swf file and double click on it.)
Processing comes with certain pre-defined
variables. For example width
and height
, which will always reference the
width and height of the draw window, no matter what.
Another special variable is mouseX
which tracks the x position of the
mouse.
Let's try to create a sketch that draws a rectangle
whose x
position is the x
position
of the mouse. Our initial attempt might be something like this:
size(400,400)
background(255)
stroke(50,50,255)
fill(200,200,255)
rect(mouseX,150,100,100)
But that doesn't work. Why not?
This is using Processing's static
mode. That means that this code runs once and then stops. In
the language of digital media, we might say that it creates
one single frame. But in order to create
movement, we need to create many frames in sequence. That
code draws a rectangle at the initial value
of mouseX
, which happens to be 0, and then it
never draws another frame, so even as the mouse moves (and
the variable mouseX
is updated) the sketch
never has the chance to redraw the rectangle.
In other words, it's almost like that sketch creates a photograph. Remember this is all we we're doing in our first assignment. What we want to do now is create a movie: multiple photographs that go by in sequence.
In Processing, we do this with some new syntax:
def setup():
# This block of code runs once
def draw():
# This block of code runs many times
You can use this in Processing in the following way:
# Create your variables here
def setup():
# Initializing code that you want to run once goes here
def draw():
# All drawing code goes here
But now you have to pay close attention to indentation: the amount of spaces at the start of each line. Last week I told you that Python and Processing don't care about whitespace (spaces, newlines, tabs) and this is mostly true. But one situation in which Python requires you to be very precise about whitespace is with indentation.
The term for these new code structures introduced here is
a code block, or
just block. A block is a chunk of code that
gets run together in some way. We will look at several other
types of code blocks in the coming weeks, but right now
we're just looking at the setup()
and draw()
blocks.
In Python, you indicate the start of a new block with a
colon (:
), and then you indicate all the code
in that block by indenting it with the same number of
spaces. The block continues on every line until you stop
indenting. We will be using 4 spaces. (You are allowed to
use different numbers of spaces or tabs, as long as you are
consistent. Programmers love to fight over which is the best
to use. This semester we will use 4 spaces for consistency
and later on you can do whatever you'd like!)
Let's demonstrate this with the following example:
def setup():
println("Doing this once.")
def draw():
println("Doing this many times.")
If you run this code and look in the
Processing Console, you will see some
interesting output. Press the Stop button (so that you are able to scroll back) and scroll back to the top of
the Console. Notice that the code inside setup()
does happen
only once, and the code inside draw()
happens
many times.
This code introduces a new command:
println()
.
Usually pronounced as "print line", this command prints
arbitrary text in the debugging window at the bottom of the
PDE. You write this text as characters in double quotes
“ ”, which are
called strings. The most famous example of
this in introductory programming is the well
known "Hello,
world!" example:
println("Hello, world!")
In addition to strings, we can also print variables:
x = 5
println(x)
This can be extremely useful for debugging later on when you are trying to figure out what the value of a variable is.
And we can combine words and variables using the plus sign
(+
). When the arguments to +
are
strings, it does not do arithmetic, but instead
does string concatenation, just mashing
together the text of the string with the value of the
variable. Having one operator that can be used for more than
one purpose is called operator overloading,
and it can be very confusing, but it can also be a
convenient shortcut. We'll try not to use too many
other examples of operator overloading this semester.
But if you try to run this example:
x = 5
println("x is now equal to " + x)
You will get an error. Processing is telling you exactly what and where
the problem is. You can fix it by using
the str()
command.
x = 5
println("x is now equal to " + str(x))
This idea of doing things many times introduces a new concept called a frame. Similar to a single frame in a movie, a single frame in Processing (as well as in most digital media) is one single static image. Like in a movie or stop-motion animation, flipping through many static images in sequence gives the impression of movement.
How many times per second does it run? Processing provides a
special variable to tell you this
called frameRate
. You can
use println()
to examine the value of this
variable:
def draw():
println(frameRate)
As you can see, the frame rate is slightly changing all the
time! This is because the actual frame rate (the number of
times per second that Processing is able to run
the draw()
block) depends on things like how fast
the computer's processor is, how many other programs are
running, etc. For reference, the frame rate of most cinema is 24 frames per
second (or "fps").
In Processing, you can adjust this with:
frameRate(24) # 24 is now the number of frames per second
Note that the parameter here indicates the maximum frame rate. Processing may not be able to acheive that frame rate (due to processor speed, etc.) but that is what it will try to do.
Returning to the original example, we can merge together the
code that tries to draw a rectangle
using mouseX
with the
above setup()
and draw()
code:
def setup():
size(400,400)
def draw():
background(255)
stroke(50,50,255)
fill(200,200,255)
rect(mouseX,150,100,100)
We should always put size()
inside setup()
because we only need to do that
once, and we put all the other draw code
inside draw()
so that it will happen many times
per second. Now each time that Processing runs
the draw()
, the special variable
mouseX
will contain the current location of the
mouse and the rectangle will be drawn there.
Using the setup()
and draw()
code blocks like this introduces a
new concept called scope. This important
term describes the parts of your sketch in which different
variables are accessible. The following code gives an error
message:
def setup():
i = 0
def draw():
rect(i,0,50,50)
Why? Because the scope of i
is limited to
setup()
, or in other words, the variable can
only be used inside setup()
,
meaning within the chunk of code that is indented together
after the colon following
setup()
. In the above example, i
is being used inside draw()
, but the variable
is not accessible there.
So another way of describing this would be: since the
variable i
is defined inside
the setup()
block, its scope is limited to that
block and it is not accessible from within
the draw()
block.
To fix this error, you can move your declaration
of i
into global scope, in
other words, outside of all indented code blocks. In that
way, your variable will be accessible in all blocks. Like
this:
i = 25
def setup():
size(100,100)
def draw():
rect(i,0,50,50)
This is called a global variable. People usually don't like global variables. They can create code that is very confusing to read. But in Processing they are required.
Confusingly, whenever you assign a variable a value within a block, Python tries to create a new variable within that block. So for example, look what happens when you run this code:
i = 0
def setup():
size(100,100)
i = 25
def draw():
rect(i,0,50,50)
You might think the rectangle would get drawn with x at 25,
but it gets drawn with x at 0. Why? Because i =
25
inside setup()
creates a new variable
limited to that scope. The solution is the global
keyword. Whenever you want to use a variable in more than one
scope, you must declare it to be global
within
the various code blocks where you will use it. So modify the
above code like this:
i = 0
def setup():
size(100,100)
global i
i = 25
def draw():
rect(i,0,50,50)
and now it will do the expected thing.
To simplify: you should create your variables
in global block (this is just at the very top of your code, no indentation),
and then declare them as global
inside each
block where you want to use them.
Now that we have introduced the concept of frames into our sketches, and our programs unfold in time, it begs an imporant question. In regards to a command or code instruction, we can now ask: When does that happen? Before today, this question didn't really matter. Over the last two weeks, the only sense of time that mattered was the order in which your instructions were run — from top to bottom, which created visual objects from back to front — but all that happened in a split second and didn't really matter in terms of "human" time. But now the sketch has a duration that we humans can perceive, and things can happen over time. So, when?
The original example of this lesson draws a rectangle at
the x
location of the mouse:
def setup():
size(400,400)
def draw():
background(255)
stroke(50,50,255)
fill(200,200,255)
rect(mouseX,150,100,100)
But look what happens if we move background()
out of draw()
and into setup()
:
def setup():
size(400,400)
background(255)
def draw():
stroke(50,50,255)
fill(200,200,255)
rect(mouseX,150,100,100)
Running this now leaves a trail of rectangles from all
previous mouse positions. Why?
Because background()
only runs once, only
wiping the screen at the beginning of the program, but
Processing runs the draw()
block many times per
second, each time creating a frame, and
without background()
, Processing draws new
rectangles without erasing the previous ones. Depending on
your intention, there are times when you may or may not want
this effect.
As another example, think about how this relates
to random()
. You used this command to set
variables to random values. But now whenever you
use random()
you must ask yourself an
important question when is the random
command being called? When is that random number being
chosen? Consider this example:
def setup():
size(400,400)
background(255)
r = random(0,255)
g = random(0,255)
b = random(0,255)
fill(r,g,b)
def draw():
rect(150,150,100,100)
Each time you stop and run that sketch, the rectangle will
have a different color. Three random values are chosen
inside setup()
and the fill color is
set once at the beginning of the sketch.
Now let's make a slight change:
def setup():
size(400,400)
background(255)
def draw():
r = random(0,255)
g = random(0,255)
b = random(0,255)
fill(r,g,b)
rect(150,150,100,100)
We have moved the selection of random values and the setting
of the fill color into draw()
, meaning that new
random values are selected and the fill color is
set each frame. This creates a very different effect.
Again, there are different times that you may want each of these different behaviors.
So far we've talked about the special Processing
variables mouseX
and mouseY
. We saw that as
the mouse moves, Processing automatically sets the value of
these variables to the x
and y
position of the mouse in every frame.
There are several other special variables that Processing provides us, and now I want to talk about two more:
pmouseX
, and pmouseY
.
Every frame, Processing sets the values of these variables to
the x
and y
position that the mouse
was at on the previous frame. This can be very useful
for achieving some interesting effects.
Let's look at the
line()
command.
This draws a line (a direct path between two points) to the screen.
Syntax
line(x1, y1, x2, y2)
Parameters
x1 float: x-coordinate of the first point
y1 float: y-coordinate of the first point
x2 float: x-coordinate of the second point
y2 float: y-coordinate of the second point
Putting these two things together, let's try to draw a line from the location of the mouse in the previous frame to the location of the mouse in the current frame:
def setup():
size(400, 400)
strokeWeight(10)
stroke(155,155,255,50)
def draw():
background(255)
line(pmouseX, pmouseY, mouseX, mouseY)
You might see a little something there, but not much. Why? Because the frame rate is so fast that as the user moves the mouse, even quickly, there is still only a small distance between the location of the mouse on the previous frame and the location of the mouse on the current frame. Let's lower the frame rate to make this a little easier to see:
def setup():
size(400, 400)
strokeWeight(10)
stroke(155,155,255,50)
frameRate(4)
def draw():
background(255)
line(pmouseX, pmouseY, mouseX, mouseY)
Now that there are only four frames per second, we can move the mouse quickly and better see the relationship between the location of the mouse in the previous frame and the location of the mouse in the current frame.
There is a lot that can be done with this principle. Let's use the distance between the previous and current mouse positions to do something besides draw a line, like specify the width and height of a shape.
Simply by subtracting the previous mouse position from the current mouse position, we are calculating the distance that the mouse moved between these two frames.
Writing that up in code form:
def setup():
size(400, 400)
noStroke()
fill(155, 155, 255, 50)
frameRate(4)
def draw():
background(255)
ellipse(200, 200, mouseX-pmouseX, mouseY-pmouseY)
Distance is calculated by
subtracting. (What about negative values? Fortunately
Processing handles that for us in this case, but ideally we
should do an absolute value here to make sure we
only have positive values. The need for this will be
eliminated when we use dist()
below.)
Now let's run that and see what happens. We are using the distance as the width and height of the ellipse. Try moving the mouse horizontally, vertivally, and diagonally to see how the width and height are affected. As you can see, distance is also related to speed. The faster the mouse is moved, the more distance it covers between frames.
Now what if we want to do something a little more complicated with distance? What if we wanted to set the thickness of a line based on how fast the user is moving the mouse. So, the faster the movement (the more distance between frames), the thicker the line.
As many of you have already experimented with, line
thickness is specified with strokeWeight()
, and
as you can see
from the
Processing reference, this takes one parameter, which
corresponds to the thickness of the line in pixels. Our
distance calculation above produced two values (distance
in x
and distance in y
). How can
we get just one value? Simple subtraction was enough for
determining distance in x
and in y
separately, but to actually determine the distance between
any two arbitrary points requires some more complicated
geometry calculations.
Anyone remember how to calculate the distance of this line? (Hint: your old friend Pythagoras ...)
Fortunately, we don't have to worry about that because
Processing offers us a shortcut for doing this
calculation: dist()
.
As you can see
from the
reference, dist()
takes four parameters:
the x
and y
values of one point,
and the x
and y
values of
another. And it automatically calculates the distance (in
pixels) between them.
So how would we calculate the distance between the mouse position of the previous frame and the mouse position of the current frame?
dist(pmouseX,pmouseY, mouseX,mouseY)
Putting that into a complete sketch:
def setup():
size(400, 400)
stroke(155, 155, 255, 50)
frameRate(4)
def draw():
background(255)
d = dist(pmouseX,pmouseY, mouseX,mouseY)
strokeWeight(d)
line(pmouseX,pmouseY, mouseX,mouseY)
There are some issues with this. For one, the line gets really thick really quickly! Also, what if we wanted to create something that felt kind of like a pen or a brush, in the sense that the line got thinner as the user moved faster, and thicker as the user moved slower. Fixing these two issues leads us to the next topic ...
To address the above questions, let's look
at map()
. This is a simple thing that can be
very useful, but it can be a little tricky to understand.
map()
takes a variable and translates (or
"maps") it from one range of values proportionally into
another range of values. It will be very useful to anyone
who elects to do a data visualization for their midterm
project.
map()
takes a number, in some range, and
translates ("maps") that proportionally into some other
range.
As the reference explains, it takes four parameters:
map(value, low1, high1, low2, high2)
It assumes that value
is between
low1
and high1
, and then it
translates that into the range of low2
and high2
.
It might be best to start by illustrating this with some examples:
map( 5, 0,10, 0,100 ) # Answer: 50
map( 5, 0,10, 50,100 ) # Answer: 75
map( 5, 0,10, 60,70 ) # Answer: 65
map( 3, 0,4, 0,100 ) # Answer: 75
You can also go from high to low, instead of low to high:
map( 3, 0,4, 100,0 ) # Answer: 25
Here are some practical examples that you can re-use in your code:
If you wanted a rectangle to move up/down in the same direction as the mouse, but limit it to a region smaller than the full window, you could do this:
rectY = map(mouseY, 0,height, 100,200)
rect( 200,rectY, 50,50 )
If you wanted a rectangle to move left/right in the opposite direction as the mouse, you could do this:
rectX = map(mouseX, 0,width, width,0)
rect( rectX,200, 50,50 )
If you wanted to create a fill color controlled by the mouse x that was always shades of red, you could do this:
r = map(mouseX, 0,width, 0,255)
fill(r,0,0)
The following sketch illustrates some things that you can do with this command:
def setup():
size(400, 400)
def draw():
background(255)
strokeWeight(1)
stroke(0)
noFill()
rect( 5, 150, 10,100)
strokeWeight(10)
stroke( 55, 55, 255)
line( 10, mouseY-10, 10, mouseY+10)
strokeWeight(1)
fill(155, 155, 255, 150)
y1 = map( mouseY, 0, height, 150, 250)
rect( 200, y1, 50,50)
y2 = map( mouseY, 0,height, 250,150)
ellipse( 250, y2, 50,50)
x1 = map( mouseY, 150, 250, 200,width)
triangle( x1,350, x1,375, x1+25,375)
size1 = map( mouseY, 150, 250, 25,200)
ellipse( 50, 350, size1,size1)
amt = map( mouseY, 150, 250, 0,1)
c1 = color(255,55,55,100)
c2 = color(55,255,55,100)
c = lerpColor( c1, c2, amt)
fill(c)
noStroke()
ellipse(300,100,75,75)
ellipse(350,110,30,30)
ellipse(300,50,30,30)
ellipse(350,75,50,50)
ellipse(275,50,20,20)
Returning the original example that motivated this, how could
we make a line that behaves somewhat like a pen or a brush, in
that the line gets thinner as the user moved faster, and
thicker as the user moved slower. We can
use pmouseX
and pmouseY
along
with map()
:
def setup():
size(600, 600)
stroke(155, 155, 255, 50)
background(255)
def draw():
distance = dist(pmouseX, pmouseY, mouseX, mouseY)
mappedDistance = map(distance, 0,200, 40,1)
strokeWeight( max(mappedDistance,2) ) # using max() to set a maximum value here
line(pmouseX, pmouseY, mouseX, mouseY)
Another application of this is to create a parallax
effect using map()
with raster images:
"""
Inspired by:
http://1morecastle.com/wp-content/uploads/2013/01/parallax-scrolling-mario.gif
The images referenced below are available here:
- http://composingdigitalmedia.org/ccr/week03-mountains.png
- http://composingdigitalmedia.org/ccr/week03-hills.png
- http://composingdigitalmedia.org/ccr/week03-foreground.png
Download these and save them into your sketch folder.
"""
def setup():
size(800,375)
global mountainsImage
global hillsImage
global foregroundImage
mountainsImage = loadImage("week03-mountains.png")
hillsImage = loadImage("week03-hills.png")
foregroundImage = loadImage("week03-foreground.png")
def draw():
rg = map(mouseX, 0,width, 155,10)
b = map(mouseX, 0,width, 255,50)
background(rg,rg,b)
mtnX = map(mouseX, 0,width, 0,-20 );
image(mountainsImage, mtnX,50)
hillsX = map(mouseX, 0,width, -20,-200 )
image(hillsImage, hillsX,250)
fgX = map(mouseX, 0,width, 0,-2000 )
image(foregroundImage, fgX,225)
darkness = map(mouseX, 0,width, 0,225)
fill(0,darkness)
rect(0,0,width,height)
That sketch uses three images. You can download them here:
Mountains Hills Foreground