Beginning Processing - the Temperature Visualizer
1842 Views, 13 Favorites, 0 Comments
Beginning Processing - the Temperature Visualizer
The Processing programming language, in very simple terms, is pure madness! If you find that hard to believe, a visit to the Official Processing Exhibition page will surely make you think otherwise. Initially developed by Ben Fry and Casey Reas at the MIT Media Lab, the Java-based visual programming environment has made some significant strides over the years. Currently, it is being supported by a large community of programmers and creative individuals. Here are some of the popular domains where Processing is employed:
- Interactive art and museum installations; sensory experiences
- Generative Art and Computational Design
- Physical Computing
- Image Processing and video manipulation
- Data Visualization
- Jumbo sized real-time video walls
- Interactive web-pages (P5.js, processing.js)
- A bizarre combination of some of the aforementioned domains
- *insert other super cool magical creative examples here*
The following Instructable is dedicated towards familiarizing you, the reader, with some of the fundamentals of Processing which will, hopefully, allow you to dive in further on your own accord. We shall accomplish this by building a Processing-based project from the ground up.
Project Description:
The “Temperature Visualizer” is a simple Processing sketch (Processing programs are better known as sketches) that receives temperature and humidity data from a sensor via an Arduino, and then visualizes it in the following manner:
- Two circular dials that display real-time temperature and humidity respectively.
- A collection of haphazardly moving circles in the background, whose movement is influenced by temperature changes. The color of the circles also tend to change along a blue-ish range in accordance to humidity changes. The circles are meant to vaguely illustrate the kinetic movement of molecules with change in temperature.
I will be elucidating the core concepts one by one at first. Afterwards, we will combine the concepts to bring our visualizer to life. Let's get started!
Processing can be downloaded from here.
The Basics
Coordinate System:
When it comes to Computer Graphics, the coordinate system is a tad different than our usual high-school/college Cartesian Coordinate System (See above, first picture).
Here, each unit is a pixel. So, if we invoke a function in Processing like this:
ellipse(100,200,30,50);
It simply means the following: draw an ellipse that is 100 pixels from the x-axis, 200 pixels from the y-axis, 30 pixels wide, and has a height of 50 pixels.
The first sketch:
Imagine Processing as an empty canvas inviting you to draw stuff on it with code. Moreover, Processing makes it very easy for you to draw basic shapes and manipulate them on the fly with the help of a bunch of useful built-in functions.
The structure of any Processing sketch will be the one below:
void setup(){
//stuff that runs only once
}
void draw(){
//runs in an infinite loop, unless you stop the program.
}
The above two functions control the flow of your sketch. Simply put, any function or anything else that you want to run just once at the beginning of the program (such as the size and color of your canvas, for example) goes inside the setup() block. Hence, everything else goes inside the draw() block. The block of code inside draw() will run in an infinite loop. We will see why this is useful later on, but for now, let's draw a circle inside the draw loop, simply because it's called "draw"!
void setup(){
size(400,400); //sets the size of the canvas
background(255); //sets the background canvas
}
void draw(){
ellipse(120,256,40,40);//draws an ellipse
}
The outcome should display an ellipse (see above, second picture).
As expected, a circle appears right where we coded it to be. Yay!
Notice the built-in functions that were implemented. Here's a list comprising of a few of them (check the Processing Reference for more):
size(width, height) - sets the canvas according to the parameters provided.
background(color) - sets the background to the color specified by the user (more on interpreting colors in a bit).
fill(color) - sets the color for the entities in your canvas.
ellipse(x,y,width,height) - draws an ellipse. The x and y coordinates are that of the ellipse's center by default.
rect(x,y,width,height) - draws a rectangle. The x and y coordinates are that of the rectangle's top left corner by default.
arc(x,y,width,height,start angle, end angle) - draws an arc, which is similar to drawing an ellipse, except, here, you specify from which angle the ellipse starts and where it will end.
*Note: all spacial parameters for the above functions are in pixels.
Let's use the above functions to draw something random:
void setup(){
size(400,400);
background(255); smooth();
}
void draw(){
noStroke();
fill(140);
ellipse(190,256,40,40);
fill(224,131,131);
rect(width/2,height/2,40,90);
arc(190,200,90,90,0,radians(270));
}
See outcome above (third picture)
It should be noted that:
1. fill() and background() takes a number, or three different numbers, and translates it to a color. Each of these numbers have a range from 0 to 255. If you pass along just one number, you will get shades of gray, 0 being completely black, and 255 being white. On the other hand, if you pass three numbers, each represents the amount of Red, Green and Blue you want to mix to achieve the color you want. Thankfully, if you can't remember all this jargon, Processing has a "Color Selector" tool, which can be found under "Tools".
2. Notice the order of the code. The gray circle appears first because we invoked the ellipse() function at the top. Then, the pink rectangle overlaps the gray circle, and then comes the arc. Bottom line: Shapes will appear in the same order as you invoke the corresponding functions in the code.
3. The radians() function converts a degree angle to a radian value, since the arc() function does not accept angles in degrees. But, for us, calculating angles in degrees is mostly convenient.
4. The "width" and "height" are special keywords that returns the width and height of the canvas. These are very useful, since they make your code adaptable to any canvas size changes in the future.
Animation 101
The Flip-book analogy:
Remember the olden days, when people used to take a stack of paper and draw a series of stick-figures, one on each page, each figure being slightly different than the last one? Afterwards, when you flipped past all the pages, it created the illusion of motion.
Well, Processing somewhat works like that. In the infinite draw() loop, each iteration or each pass through the loop can be regarded as a single page in the metaphorical flip-book. Up until now, we didn't see anything moving because we kept drawing the same shapes in all the pages. So, how do we change that? We incrementally change some aspect of the sketch at each iteration:
int x = 20;
void setup(){
size(400,400);
background(255);
smooth();
}
void draw(){
noStroke();
fill(140);
ellipse(x,256,40,40);
x = x + 1; //the incremental change
}
See the outcome above (first picture).
Well, at least something is moving. Before we figure out why the circle leaves a trail as it moves, let's focus on what causes the motion. We declared an integer variable 'x', and passed it to ellipse() as it's x-position. Then, at the end of the draw() loop, we incremented the value of x by one. This means, for every page (or iteration), the center of the circle will be 1 pixel away from the x-axis, and thus, creating the illusion of motion, just like in the olden days!
Now, for the trail - notice that we invoked the background() function in setup(), meaning that it will run only once. Hence, the background() never gets refreshed, and we see a history of the locations the circle travels. Moving the background() at the beginning of the draw() loop solves this. Try it! (see second picture above for illustration).
Whether to place background() in setup() or in draw() will depend on the type of sketch you are attempting to produce.
I'll leave it to the reader to figure out why I declared the variable x outside both setup() and draw().
Circle Variation 1:
Since we got the basics of animation down, let's make our circle a bit more interesting - what if we wanted our circle to stop at the center of the screen, and not run away or getting out of view?
One way to do this is to use conditionals (if, else statements) to limit the circle from going past the center. But, just for fun, let's try it the following way:
int x = 20;
void setup(){
size(400,400);
smooth();
}
void draw(){
background(255);
noStroke();
fill(140);
ellipse(x,256,40,40);
x = x + 1; //the incremental change
x = constrain(x,0,width/2); //the constrain function
}
The circle obediently stops in the middle, right?
Introducing, the constrain() function:
constrain(variable, lower threshold, higher threshold) - this function ensures that the value contained within the "variable" does not, in any way, get lower than "lower threshold", or higher than "higher threshold".
In the above code, by using constrain(), we made sure that the circle's x-position does not exceed the provided range (0 to half of the width, ergo, the center). The constrain() function comes in handy in many other situations, like constraining the brightness of a light for an installation, or constraining sensor values within a certain range.
Circle Variation 2 (Jiggling circle):
Moving a circle across the screen was pretty sweet at first. However, since we have got some nifty tools under our belt by now, let's try something a bit more exciting - making the circle jiggle!
We can make the circle seem like it's jiggling by continuously making it move one pixel to the left or right or up or down in a random manner (e.g, up, down, down, left, left, right,right,right..and so on). How do we select a direction randomly? Let's find out:
float x = 200;
float y = 200;
void setup(){
size(400,400);
smooth();
}
void draw(){
background(255);
noStroke();
fill(140);
ellipse(x,y,80,80);
x = x + random(-1,1);
y = y + random(-1,1);
}
random(lower value, upper value) - This built-in function generates a random number within the range provided to it.
Note, the random function generates floating point numbers, not integers. This is why we had to change the data types of the x and y variables from "int" to "float", since adding an integer number to a float ultimately yields a float, and we can't store that in int.
Hence, by adding a number randomly to both the x and y coordinates of the circle at each iteration of the draw() loop, we succeeded in making it jiggle. If you want the circle to jiggle faster, make the range larger, like from -10 to +10 for example.
Object Oriented Programming Primer - Classes and Objects
Functions in a nutshell:
A function is essentially a chunk of code that you can reuse more than once. Breaking down a complex sketch into smaller functions ensure readability (the code is easier to read, since all the complex code gets abstracted; easier to debug at this stage), modularity (turns code into modular pieces, like legos), and prevents the code from getting DRY (DRY stands for Don't Repeat Yourself; repeating means manually copy/pasting code multiple times).
Here's the generic structure of a function:
data_type_of_returned_value" 'void' if it returns nothing " functionName(parameters){
//the function definition
}
Let's implement a function that displays our circle, and another function for its motion (that is, jiggling):
float x = 200;
float y = 200;
void setup(){
size(400,400);
smooth();
}
void draw(){
background(255);
display(); //calling a function
move(); //calling the second function
}
//the functions
void display(){
noStroke();
fill(140);
ellipse(x,y,80,80);
}
void move(){
x = x + random(-5,5);
y = y + random(-5,5);
}
Even though the code does exactly the same thing as before, look how organized the draw() loop is now! Note how I "called" or "invoked" the respective functions from within the draw() loop.
Functions can have "parameters" too. Parameters are "place-holders" for values that you can "pass" on to a function. On receiving the values, the function may compute them in accordance to how you have defined it. For example, here's a function that takes in two parameters (x and y), and uses the passed arguments to render this weird shape:
void drawShape(float x, float y){
noStroke();
fill(140);
ellipse(x,y,80,80);
ellipse(x-151,y+2,44,44);
fill(68,234,66);
rect(x-131,y-5,92,18);
}
See picture above (first picture).
In the above function, we used x and y as reference, and set the parameters for all the other shapes "relative" to x and y. This technique is called "relative positioning". For example, if reference x is 400, and the x-value of some shape is 89, then the position of that shape relative to the reference x is "x-311"(400-89 = 311).
Functions can also "return" values, but that's for another day!
An Object and Class Story:
As we have seen a while ago, functions bring about a certain level of organization to our code. However, making use of objects in our code is a far efficient organization strategy.
An object essentially binds together data (variables) and functionality(functions) of a given entity into one neat package. In order to create objects, we need a blue-print for it. This is called the class.
Instead of babbling about any more complex concepts, let's create a class for generating jiggly circle objects:
class Circle {
//class variables (also called instance variables)
float x;
float y;
float d;
color c;
//the object constructor
Circle(float x, float y, float d, color c){
this.x = x;
this.y = y;
this.d = d;
this.c = c;
}
//the methods
void display(){
noStroke();
fill(c);
ellipse(this.x,this.y,this.d,this.d);
}
void jiggle(){
this.x = this.x + random(-2,2);
this.y = this.y + random(-2,2);
}
}
Things to note:
- There's this bizarre thing in the code called the "constructor". The constructor is the function that actually creates an object, in conjunction with the "new" keyword (more on this in a bit). The constructor definition should contain code on how to construct objects. The constructor name should be the same as the class name.
- the "this" keyword is used to access variables the belongs to the class, and only the class! You can see that I have used the same variable names for the class variables and the constructor parameters. By using "this", I allow Processing to distinguish between the two. Hence, "this.x = x" indicates: store the value passed to x while making the object to "this.x", the class variable.
- The same functions we have seen before is being used in the class. A function associated with a class is called a "method".
It's a good practice to keep your Classes in a separate file, so that your main code looks neat. You can do this by creating a second tab on the Processing environment. Note, the Tab's name must be same as that of the class. (see picture above;third picture).
Using the class to make objects:
//declaring objects
Circle c;
Circle d;
void setup(){
size(400,400);
smooth();
//initializing objects
c = new Circle(200,200,80,color(100));
d = new Circle(344,355,20,color(62,177,137));
}
void draw(){
background(255);
//using objects
c.display();
c.jiggle();
d.display();
d.jiggle();
}
see outcome above (second picture).
Declaring Objects - While declaring objects, it's easier to consider the class as a newly created data-type. So, "Circle c" means declare a variable "c" of type "Circle" (compare with "int c").
Initializing Objects - See how we invoked the constructor via the "new" keyword? Since the circle constructor definition had parameters, we have to pass arguments to initialize the object.These arguments are assigned to the object's variables (x,y,c etc.). This allows us to use the same class to create objects with different properties. In the above example, Circles c and d are different from each other in size and color.
Also, notice that the objects are initialized at setup(). This is logical, since we have no intention of initializing the same object over and over again in draw().
Using Objects - We use the dot "." operator to access the object's methods, as seen above.
Populating Objects WIth Arrays
Two circles are great. But what if we wanted to generate 200 circles all at once? One way to do this would be to initialize 200 objects by hand, and then invoking all of their methods one by one.
To save us from all that typing, we can simply use an array that can hold 200 objects. The advantage of using an array is that, we can use a "for" loop to iterate through all the array elements, and do something to each of them.
The code for populating 200 circles goes something like this:
int numCircle = 200;
Circle[] circles = new Circle[numCircle]; //array declaration
void setup(){
size(400,400);
smooth();
//initializing 200 circles
for(int i=0;i<numCircle;i++){
circles[i] = new Circle(random(width),random(height),20,color(130,0,250,60));
}
}
void draw(){
background(255);
//invoking method of each object
for(int i=0;i<circles.length;i++){
circles[i].display();
circles[i].jiggle();
}
}
see outcome above.
Here, in setup(), we used a for loop to go to each cell of the "circles" array and initialize an object for each one. Later, in draw(), we iterated through each Circle object stored in the "Circle type" array, and invoked methods of each.
The best part is, now, no matter how many circles we want, may it be two or two million (though, that would slow down your computer significantly!), we just have to tweak the "numCircle" variable, and the rest can remain the same.
Serial Communication
Processing can communicate with other physical devices (such as microcontrollers) using Serial Communication. During this form of communication, the device (in our case, an Arduino) sends serial data to the Processing sketch one bit at a time. The number of bits sent per second depends on the Baud rate. By default, the baud rate for both the Arduino and Processing environment is 9600. You could change this, but make sure the baud rate is the same for both the end-applications.
To demonstrate this, I'll be using an Arduino to send data from a DHT11 temperature/humidity sensor to the Processing sketch. The Arduino code is attached herewith.
Most of the weird functions that you see on the Arduino sketch are part of the DHT11 library. Check the example sketch that comes with the library for reference.
The crucial thing to note is how we're sending each string of data. Firstly, we print the temperature value using Serial.print(). Secondly, we print a comma, and then comes the humidity value, followed by a new-line (the Serial.println() ). Hence, we are generating strings comprising of two sensor values, separated by a comma and each string ends with a new-line character.
Now, let's he how Processing parses the strings. Here's a simple sketch that just displays the sensor value:
//importing the serial library
import processing.serial.*;
Serial myPort; //serial object
PFont font; //PFont object
//global variables
int temp = 0;
int hum = 0;
void setup(){
size(400,400);
myPort = new Serial(this,"COM3",9600);
myPort.bufferUntil('\n');
font = loadFont("ARESSENCE-48.vlw");//created font
textFont(font);
}
void draw(){
background(255);
fill(140);
text("Temperature: " + temp, 72, 120);
text("Humidity: " + hum, 104, 220);
}
void serialEvent(Serial myPort){
//read the serial buffer
String myString = myPort.readStringUntil('\n');
if(myString != null){
myString = trim(myString);
int sensorData[] = int(split(myString,','));
temp = sensorData[0];
hum = sensorData[1];
}
}
See outcome above
Things to note:
- The Processing's Serial library is imported at the very top
- A Serial type object named "myPort" is created.
- In the setup, the "myPort.bufferUntil('\n')" extracts the serial data only when it finds a new-line character. This ensures that a serial event is triggered only when a whole data-string is extracted, not somewhere in the middle.
- the serialEvent() is a Processing built-in event function, which gets called everytime a serial event is triggered. Once it's called, it reads the extracted string from the serial buffer, trims it and splits the string into two string, each being a sensor value. These values are temporarily stored in an array, and then are assigned to the global variables "temp" and "hum" respectively.
- The "PFont" is a built-in class for drawing text. In order to use this, you must make a font using Processing's "Create Font...." option under tools. Afterwards, just follow what I did!
Serial Communication is somewhat a vast topic. Anybody wishing to dive in further is strongly encouraged to check out Tom Igoe's Physical Computing class notes.
Downloads
Putting It All Together
Cograts on getting this far! At this point, you might be wondering, "what happened to making the visualizer?". I'll let you in on a little secret - we have been making it all along! The Processing code for the visualizer is attached herewith. Once you see the code, you will instantly realise that it's just a combination of everything we have done so far!
Things to note:
1. The sketch consists of two Classes - Dial and Molecules
2. The Dial class makes use of the arc() function we saw earlier to display temperature change.
3. So, what's a map()? That's new! Well, this is probably one of the most important function when it comes to visualizing any form of data in Processing. It works like this:
Suppose, you want to change the color of a circle in a map based on how densely populated that area is. Let's say the population is something within the range of 1000-10,000. But, as we know, all colors have a range of 0-255. Now what?
map(variable, old low, old high, new low, new high) - The map() function takes in a variable, and converts it's values from it's old existing range to a new given range. So, If I did something like this:
int colorValue = map(colorValue,1000,10000,0,255);
This means: map any value within 1000-10000 accordingly to the new range, that is 0-255. So, 1000 is equivalent to 0 and 10000 is equivaent to 255, and any value inbetween gets mapped accordingly.
4. In the Dial class, we mapped the value coming from the sensor to a suitable angle from 0 degrees to 360 degrees. I set the low and high thresholds according to the DHT11 data-sheet (temp: 0-50, humidity: 20-90).
5. The Molecules class is similar to the Circle class, except for the fact that I mapped the temperature value to the intensity variable. This makes the molecules jiggle in accordance to changing temperature. Also, I mapped the humidity value to change the color of the molecules with changing humidity.
Downloads
Conclusion
Confession: To be honest, my Processing code does not exactly conform to all the features an Object Oriented Program should have. In fact, an experienced Java programmer would probably cringe with disgust after taking a look at my code ("all those global variables flying all over the place!"). Nevertheless, I believe a certain level of organization has been attained, and everything works fine!
Further reading:
5 words: Dan Shiffman's books on Processing. They are THE best!
I actually saw the "jiggly circle" example in one of Dan's videos!
Processing Reference: Click here!