(For more resources related to this topic, see here.)
Drawing basics
The screens of modern computers consist of a number of small squares, called pixels ( picture elements ). Each pixel can light in one color. You create pictures on the screen by changing the colors of the pixels.
Graphics based on pixels is called raster graphics. Another kind of graphics is vector graphics, which is based on primitives such as lines and circles. Today, most computer screens are arrays of pixels and represent raster graphics. But images based on vector graphics (vector images) are still used in computer graphics. Vector images are drawn on raster screens using the rasterization procedure.
The openFrameworks project can draw on the whole screen (when it is in fullscreen mode) or only in a window (when fullscreen mode is disabled). For simplicity, we will call the area where openFrameworks can draw, the screen . The current width and height of the screen in pixels may be obtained using the ofGetWidth() and ofGetHeight() functions.
For pointing the pixels, openFrameworks uses the screen's coordinate system. This coordinate system has its origin on the top-left corner of the screen. The measurement unit is a pixel. So, each pixel on the screen with width w and height h pixels can be pointed by its coordinates (x, y), where x and y are integer values lying in the range 0 to w-1 and from 0 to h-1 respectively.
In this article, we will deal with two-dimensional (2D) graphics, which is a number of methods and algorithms for drawing objects on the screen by specifying the two coordinates (x, y) in pixels.
The other kind of graphics is three-dimensional (3D) graphics, which represents objects in 3D space using three coordinates (x, y, z) and performs rendering on the screen using some kind of projection of space (3D) to the screen (2D).
The background color of the screen
The drawing on the screen in openFrameworks should be performed in the testApp::draw() function. Before this function is called by openFrameworks, the entire screen is filled with a fixed color, which is set by the function ofSetBackground( r, g, b ). Here r, g, and b are integer values corresponding to red, green, and blue components of the background color in the range 0 to 255. Note that each of the ofSetBackground() function call fills the screen with the specified color immediately.
You can make a gradient background using the ofBackgroundGradient() function.
You can set the background color just once in the testApp::setup() function, but we often call ofSetBackground() in the beginning of the testApp::draw() function to not mix up the setup stage and the drawing stage.
Pulsating background example
You can think of ofSetBackground() as an opportunity to make the simplest drawings, as if the screen consists of one big pixel. Consider an example where the background color slowly changes from black to white and back using a sine wave.
This is example 02-2D/01-PulsatingBackground.
The project is based on the openFrameworks emptyExample example. Copy the folder with the example and rename it. Then fill the body of the testApp::draw() function with the following code:
float time = ofGetElapsedTimef(); //Get time in seconds//Get periodic value in [-1,1],
with wavelength equal to 1 second float value = sin( time * M_TWO_PI );//Map value from [-1,1] to [0,255] float v = ofMap( value, -1, 1, 0, 255 );ofBackground( v, v, v ); //Set background color
This code gets the time lapsed from the start of the project using the ofGetElapsedTimef() function, and uses this value for computing value = sin( time * M_TWO_PI ). Here, M_TWO_PI is an openFrameworks constant equal to 2π; that is, approximately 6.283185. So, time * M_TWO_PI increases by 2π per second. The value 2π is equal to the period of the sine wave function, sin(). So, the argument of sin(...) will go through its wavelength in one second, hence value = sin(...) will run from -1 to 1 and back. Finally, we map the value to v, which changes in range from 0 to 255 using the ofMap() function, and set the background to a color with red, green, and blue components equal to v.
Run the project; you will see how the screen color pulsates by smoothly changing its color from black to white and back.
Replace the last line, which sets the background color to ofBackground( v, 0, 0 );, and the color will pulsate from black to red.
Replace the argument of the sin(...) function to the formula time * M_TWO_PI * 2 and the speed of the pulsating increases by two times.
We will return to background in the Drawing with an uncleared background section. Now we will consider how to draw geometric primitives.
Geometric primitives
In this article we will deal with 2D graphics. 2D graphics can be created in the following ways:
- Drawing geometric primitives such as lines, circles, and other curves and shapes like triangles and rectangles. This is the most natural way of creating graphics by programming. Generative art and creative coding projects are often based on this graphics method. We will consider this in the rest of the article.
- Drawing images lets you add more realism to the graphics.
- Setting the contents of the screen directly, pixel-by-pixel, is the most powerful way of generating graphics. But it is harder to use for simple things like drawing curves. So, such method is normally used together with both of the previous methods. A somewhat fast technique for drawing a screen pixel-by-pixel consists of filling an array with pixels colors, loading it in an image, and drawing the image on the screen. The fastest, but a little bit harder technique, is using fragment shaders.
openFrameworks has the following functions for drawing primitives:
- ofLine( x1, y1, x2, y2 ): This function draws a line segment connecting points (x1, y1) and (x2, y2)
- ofRect( x, y, w, h ): This function draws a rectangle with the top-left corner (x, y), width w, and height h
- ofTriangle( x1, y1, x2, y2, x3, y3 ): This function draws a triangle with vertices (x1, y1), (x2, y2), and (x3, y3)
- ofCircle( x, y, r ): This function draws a circle with center (x, y) and radius r
openFrameworks has no special function for changing the color of a separate pixel. To do so, you can draw the pixel (x, y) as a rectangle with width and height equal to 1 pixel; that is, ofRect( x, y, 1, 1 ). This is a very slow method, but we sometimes use it for educational and debugging purposes.
All the coordinates in these functions are float type. Although the coordinates (x, y) of a particular pixel on the screen are integer values, openFrameworks uses float numbers for drawing geometric primitives. This is because a video card can draw objects with the float coordinates using modeling, as if the line goes between pixels. So the resultant picture of drawing with float coordinates is smoother than with integer coordinates.
Using these functions, it is possible to create simple drawings.
The simplest example of a flower
Let's consider the example that draws a circle, line, and two triangles, which forms the simplest kind of flower.
This is example 02-2D/02-FlowerSimplest.
This example project is based on the openFrameworks emptyExample project. Fill the body of the testApp::draw() function with the following code:
ofBackground( 255, 255, 255 ); //Set white background ofSetColor( 0, 0, 0 ); //Set black colorofCircle
( 300, 100, 40 ); //Blossom ofLine( 300, 100, 300, 400 ); //Stem ofTriangle( 300, 270, 300, 300, 200, 220 ); //Left leaf ofTriangle( 300, 270, 300, 300, 400, 220 ); //Right leaf
On running this code, you will see the following picture of the "flower":
Controlling the drawing of primitives
There are a number of functions for controlling the parameters for drawing primitives.
- ofSetColor( r, g, b ): This function sets the color of drawing primitives, where r, g, and b are integer values corresponding to red, green, and blue components of the color in the range 0 to 255. After calling ofSetColor(), all the primitives will be drawn using this color until another ofSetColor() calling. We will discuss colors in more detail in the Colors section.
- ofFill() and ofNoFill(): These functions enable and disable filling shapes like circles, rectangles, and triangles. After calling ofFill() or ofNoFill(), all the primitives will be drawn filled or unfilled until the next function is called. By default, the shapes are rendered filled with color. Add the line ofNoFill(); before ofCircle(...); in the previous example and you will see all the shapes unfilled, as follows:
- ofSetLineWidth( lineWidth ): This function sets the width of the rendered lines to the lineWidth value, which has type float. The default value is 1.0, and calling this function with larger values will result in thick lines. It only affects drawing unfilled shapes. The line thickness is changed up to some limit depending on the video card. Normally, this limit is not less than 8.0.
Add the line ofSetLineWidth( 7 ); before the line drawing in the previous example, and you will see the flower with a thick vertical line, whereas all the filled shapes will remain unchanged. Note that we use the value 7; this is an odd number, so it gives symmetrical line thickening.
Note that this method for obtaining thick lines is simple but not perfect, because adjacent lines are drawn quite crudely. For obtaining smooth thick lines, you should draw these as filled shapes.
- ofSetCircleResolution( res ): This function sets the circle resolution; that is, the number of line segments used for drawing circles to res. The default value is 20, but with such settings only small circles look good. For bigger circles, it is recommended to increase the circle resolution; for example, to 40 or 60. Add the line ofSetCircleResolution( 40 ); before ofCircle(...); in the previous example and you will see a smoother circle. Note that a large res value can decrease the performance of the project, so if you need to draw many small circles, consider using smaller res values.
- ofEnableSmoothing() and ofDisableSmoothing(): These functions enable and disable line smoothing. Such settings can be controlled by your video card. In our example, calling these functions will not have any effect.
Performance considerations
The functions discussed work well for drawings containing not more than a 1000 primitives. When you draw more primitives, the project's performance can decrease (it depends on your video card). The reason is that each command such as ofSetColor() or ofLine() is sent to drawing separately, which takes time. So, for drawing 10,000, 100,000, or even 1 million primitives, you should use advanced methods, which draw many primitives at once. In openFrameworks, you can use the ofMesh and ofVboMesh classes for this.
Using ofPoint
Maybe you noted a problem when considering the preceding flower example: drawing primitives by specifying the coordinates of all the vertices is a little cumbersome. There are too many numbers in the code, so it is hard to understand the relation between primitives. To solve this problem, we will learn about using the ofPoint class and then apply it for drawing primitives using control points.
ofPoint is a class that represents the coordinates of a 2D point. It has two main fields: x and y, which are float type.
Actually, ofPoint has the third field z, so ofPoint can be used for representing 3D points too. If you do not specify z, it sets to zero by default, so in this case you can think of ofPoint as a 2D point indeed.
Operations with points
To represent some point, just declare an object of the ofPoint class.
ofPoint p;
To initialize the point, set its coordinates.
p.x = 100.0; p.y = 200.0;
Or, alternatively, use the constructor.
p = ofPoint( 100.0, 200.0 );
You can operate with points just as you do with numbers. If you have a point q, the following operations are valid:
- p + q or p - q provides points with coordinates (p.x + q.x, p.y + q.y) or (p.x - q.x, p.y - q.y)
- p * k or p / k, where k is the float value, provides the points (p.x * k, p.y * k) or (p.x / k, p.y / k)
- p += q or p -= q adds or subtracts q from p
There are a number of useful functions for simplifying 2D vector mathematics, as follows:
- p.length(): This function returns the length of the vector p, which is equal to sqrt( p.x * p.x + p.y * p.y ).
- p.normalize(): This function normalizes the point so it has the unit length p = p / p.length(). Also, this function handles the case correctly when p.length() is equal to zero.
See the full list of functions for ofPoint in the libs/openFrameworks/math/ofVec3f.h file. Actually, ofPoint is just another name for the ofVec3f class, representing 3D vectors and corresponding functions.
All functions' drawing primitives have overloaded versions working with ofPoint:
- ofLine( p1, p2 ) draws a line segment connecting the points p1 and p2
- ofRect( p, w, h ) draws a rectangle with top-left corner p, width w, and height h
- ofTriangle( p1, p2, p3 ) draws a triangle with the vertices p1, p2, and p3
- ofCircle( p, r ) draws a circle with center p and radius r
Using control points example
We are ready to solve the problem stated in the beginning of the Using ofPoint section. To avoid using many numbers in drawing code, we can declare a number of points and use them as vertices for primitive drawing. In computer graphics, such points are called control points .
Let's specify the following control points for the flower in our simplest flower example:
Now we implement this in the code.
This is example 02-2D/03-FlowerControlPoints.
Add the following declaration of control points in the testApp class declaration in the testApp.h file:
ofPoint stem0, stem1, stem2, stem3, leftLeaf, rightLeaf;
Then set values for points in the testApp::update() function as follows:
stem0 = ofPoint( 300, 100 ); stem1 = ofPoint( 300, 270 ); stem2 = ofPoint( 300, 300 ); stem3 = ofPoint( 300, 400 ); leftLeaf = ofPoint( 200, 220 ); rightLeaf = ofPoint( 400, 220 );
Finally, use these control points for drawing the flower in the testApp::draw() function:
ofBackground( 255, 255, 255 ); //Set white background ofSetColor( 0, 0, 0 ); //Set black colorofCircle ( stem0, 40 ); //Blossom ofLine( stem0, stem3 ); //Stem ofTriangle( stem1, stem2, leftLeaf ); //Left leaf ofTriangle( stem1, stem2, rightLeaf ); //Right leaf
You will observe that when drawing with control points the code is much easier to understand.
Furthermore, there is one more advantage of using control points: we can easily change control points' positions and hence obtain animated drawings. See the full example code in 02-2D/03-FlowerControlPoints. In addition to the already explained code, it contains a code for shifting the leftLeaf and rightLeaf points depending on time. So, when you run the code, you will see the flower with moving leaves.
Coordinate system transformations
Sometimes we need to translate, rotate, and resize drawings. For example, arcade games are based on the characters moving across the screen.
When we perform drawing using control points, the straightforward solution for translating, rotating, and resizing graphics is in applying desired transformations to control points using corresponding mathematical formulas. Such idea works, but sometimes leads to complicated formulas in the code (especially when we need to rotate graphics). The more elegant solution is in using coordinate system transformations. This is a method of temporarily changing the coordinate system during drawing, which lets you translate, rotate, and resize drawings without changing the drawing algorithm.
The current coordinate system is represented in openFrameworks with a matrix. All coordinate system transformations are made by changing this matrix in some way. When openFrameworks draws something using the changed coordinate system, it performs exactly the same number of computations as with the original matrix. It means that you can apply as many coordinate system transformations as you want without any decrease in the performance of the drawing.
Coordinate system transformations are managed in openFrameworks with the following functions:
- ofPushMatrix(): This function pushes the current coordinate system in a matrix stack. This stack is a special container that holds the coordinate system matrices. It gives you the ability to restore coordinate system transformations when you do not need them.
- ofPopMatrix(): This function pops the last added coordinate system from a matrix stack and uses it as the current coordinate system. You should take care to see that the number of ofPopMatrix() calls don't exceed the number of ofPushMatrix() calls.
Though the coordinate system is restored before testApp::draw() is called, we recommend that the number of ofPushMatrix() and ofPopMatrix() callings in your project should be exactly the same. It will simplify the project's debugging and further development.
- ofTranslate( x, y ) or ofTranslate( p ): This function moves the current coordinate system at the vector (x, y) or, equivalently, at the vector p. If x and y are equal to zero, the coordinate system remains unchanged.
- ofScale( scaleX, scaleY ): This function scales the current coordinate system at scaleX in the x axis and at scaleY in the y axis. If both parameters are equal to 1.0, the coordinate system remains unchanged. The value -1.0 means inverting the coordinate axis in the opposite direction.
- ofRotate( angle ): This function rotates the current coordinate system around its origin at angle degrees clockwise. If the angle value is equal to 0, or k * 360 with k as an integer, the coordinate system remains unchanged.
All transformations can be applied in any sequence; for example, translating, scaling, rotating, translating again, and so on.
The typical usage of these functions is the following:
- Store the current transformation matrix using ofPushMatrix().
- Change the coordinate system by calling any of these functions: ofTranslate(), ofScale(), or ofRotate().
- Draw something.
- Restore the original transformation matrix using ofPopMatrix().
Step 3 can include steps 1 to 4 again.
For example, for moving the origin of the coordinate system to the center of the screen, use the following code in testApp::draw():
ofPushMatrix(); ofTranslate( ofGetWidth() / 2, ofGetHeight() / 2 ); //Draw something ofPopMatrix();
If you replace the //Draw something comment to ofCircle( 0, 0, 100 );, you will see the circle in the center of the screen.
This transformation significantly simplifies coding the drawings that should be located at the center of the screen.
Now let's use coordinate system transformation for adding triangular petals to the flower.
For further exploring coordinate system transformations.
Flower with petals example
In this example, we draw petals to the flower from the 02-2D/03-FlowerControlPoints example, described in the Using control points example section.
This is example 02-2D/04-FlowerWithPetals.
We want to draw unfilled shapes here, so add the following lines at the beginning of testApp::draw():
ofNoFill(); //Draw shapes unfilled
Now add the following code to the end of testApp::draw() for drawing the petals:
ofPushMatrix(); //Store the coordinate system //Translate the coordinate system center to stem0 ofTranslate( stem0 ); //Rotate the coordinate system depending on the time float angle = ofGetElapsedTimef() * 30; ofRotate( angle );int petals = 15; //Number of petals for (int i=0; i<petals; i++) { //Rotate the coordinate system ofRotate( 360.0 / petals ); //Draw petal as a triangle ofPoint p1( 0, 20 ); ofPoint p2( 80, 0 ); ofTriangle( p1, -p1, p2 ); }//Restore the coordinate system ofPopMatrix();
This code moves the coordinate system origin to the point stem0 (the blossom's center) and rotates it depending on the current time. Then it rotates the coordinate system on a fixed angle and draws a triangle petals times. As a result, we obtain a number of triangles that slowly rotate around the point stem0.
Colors
Up until now we have worked with colors using the functions ofSetColor( r, g, b ) and ofBackground( r, g, b ). By calling these functions, we specify the color of the current drawing and background as r, g, and b values, corresponding to red, green, and blue components, where r, g and b are integer values lying in the range 0 to 255.
When you need to specify gray colors, you can use overloaded versions of these functions with just one argument: ofSetColor( gray ) and ofBackground( gray ), where gray is in the range 0 to 255.
These functions are simple, but not enough. Sometimes, you need to pass the color as a single parameter in a function, and also do color modifications like changing the brightness. To solve this problem, openFrameworks has the class ofColor. It lets us operate with colors as we do with single entities and modify these.
ofColor is a class representing a color. It has four float fields: r, g, b, and a. Here r, g, and b are red, green, and blue components of a color, and a is the alpha component, which means the opacity of a color. The alpha component is related to transparency.
In this article we will not consider the alpha component. By default, its value is equal to 255, which means truly opaque color, so all colors considered in this article are opaque.
The ofSetColor() , ofBackground() , and ofColor() functions include the alpha component as an optional last argument, so you can specify it when needed.
Operations with colors
To represent some color, just declare an object of the ofColor class.
ofColor color;
To initialize the color, set its components.
color.r = 0.0; color.g = 128.0; color.b = 255.0;
Or, equivalently, use the constructor.
color = ofColor( 0.0, 128.0, 255.0 );
You can use color as an argument in the functions ofSetColor() and ofBackground(). For example, ofSetColor( color ) and ofBackground( color ).
openFrameworks has a number of predefined colors, including white, gray, black, red, green, blue, cyan, magenta, and yellow. See the full list of colors in the libs/openFrameworks/types/ofColors.h file. To use the predefined colors, add the ofColor:: prefix before these names. For example, ofSetColor( ofColor::yellow ) sets the current drawing color to yellow.
You can modify the color using the following functions:
- setHue( hue ), setSaturation( saturation ), and setBrightness( brightness ): These functions change the hue, saturation, and brightness of the color to specified values. All the arguments are float values in the range 0 to 255.
- setHsb( hue, saturation, brightness ): This function creates a color by specifying its hue, saturation, and brightness values, where arguments are float values in the range 0 to 255.
- getHue() and getSaturation(): These functions return the hue and saturation values of the color.
- getBrightness(): This function returns the brightest color component.
- getLightness(): This function returns the average of the color components.
- invert(): This function inverts color components; that is, the r, g, and b fields of the color become 255-r, 255-g, and 255-b respectively.
Let's consider an example that demonstrates color modifications.
Color modifications example
In this example, we will modify the red color by changing its brightness, saturation, and hue through the whole range and draw three resultant stripes.
This is example 02-2D/05-Colors.
This example project is based on the openFrameworks emptyExample project. Fill the body of the testApp::draw() function with the following code:
ofBackground( 255, 255, 255 ); //Set white background//Changing brightness for ( int i=0; i<256; i++ ) { ofColor color = ofColor::red; //Get red color color.setBrightness( i ); //Modify brightness ofSetColor( color ); ofLine( i, 0, i, 50 ); }//Changing saturation for ( int i=0; i<256; i++ ) { ofColor color = ofColor::red; //Get red color color.setSaturation( i ); //Modify saturation ofSetColor( color ); ofLine( i, 80, i, 130 ); }//Changing hue for ( int i=0; i<256; i++ ) { ofColor color = ofColor::red; //Get red color color.setHue( i ); //Modify hue ofSetColor( color ); ofLine( i, 160, i, 210 ); }
Run the project and you will see three stripes consisting of the red color with changed brightness, saturation, and hue.
As you can see, changing brightness, saturation, and hue is similar to the color-corrections methods used in photo editors like Adobe Photoshop and Gimp. From a designer's point of view, this is a more powerful method for controlling colors as compared to directly specifying the red, green, and blue color components.
Now we will consider how to perform drawings with uncleared background, which can be useful in many creative coding projects related to 2D graphics.
Drawing with an uncleared background
By default, the screen is cleared each time before testApp:draw() is called, so you need to draw all the contents of the screen inside testApp::draw() again and again. It is appropriate in most cases, but sometimes we want the screen to accumulate our drawings. In openFrameworks, you can do this by disabling screen clearing using the ofSetBackgroundAuto( false ) function. All successive drawings will accumulate on the screen. (In this case you should call ofBackground() rarely, only for clearing the current screen).
This method is very simple to use, but is not flexible enough for serious projects. Also, currently it has some issues:
- In Mac OS X, the screen can jitter.
- In Windows, screen grabbing does not work (more details on screen grabbing can be seen in the Screen grabbing section later in this article)
So, when you need to accumulate drawings, we recommend you to use the FBO buffer, which we will explain now.
Using FBO for offscreen drawings
FBO in computer graphics stands for frame buffer object . This is an offscreen raster buffer where openFrameworks can draw just like on the screen. You can draw something in this buffer, and then draw the buffer contents on the screen. The picture in the buffer is not cleared with each testApp::draw() calling, so you can use FBO for accumulated drawings.
In openFrameworks, FBO is represented by the class ofFBO.
The typical scheme of its usage is the following:
- Declare an ofFbo object, fbo, in the testApp class declaration.
ofFbo fbo;
- Initialize fbo with some size in the testApp::setup() function.
int w = ofGetWidth(); int h = ofGetHeight(); fbo.allocate( w, h );
- Draw something in fbo. You can do it not only in testApp::draw() but also in testApp::setup() and testApp::update(). To begin drawing, call fbo.begin() . After this, all drawing commands, such as ofBackground() and ofLine(), will draw to fbo. To finish drawing, call fbo.end(). For example, to fill fbo with white color, use the following code:
fbo.begin(); ofBackground( 255, 255, 255 ); fbo.end();
- Draw fbo on the screen using the fbo.draw( x, y ) or fbo.draw( x, y, w, h ) functions. Here, x and y are the top-left corner, and w and h are the optional width and height of the rendered fbo image on the screen. The drawing should be done in the testApp::draw() function. The example of the corresponding code is the following:
ofSetColor( 255, 255, 255 ); fbo.draw( 0, 0 );
The ofFbo class has drawing behavior similar to the image class ofImage. So, the ofSetColor( 255, 255, 255 ); line is needed here to draw fbo without color modulation.
You can use many FBO objects and even draw one inside another. For example, if you have ofFbo fbo2, you can draw fbo inside fbo2 as follows:
fbo2.begin(); ofSetColor( 255, 255, 255 ); fbo.draw( 0, 0 ); fbo2.end();
Be careful: if you call fbo.begin() , you should always call fbo.end() ; do it before drawing FBO's contents anywhere.
The following tips will be helpful for advanced ofFbo usage:
- fbo has texture of the type ofTexture, which holds its current picture. The texture can be accessed using fbo.getTextureReference().
- The settings of your video card, such as like antialiasing smoothing, does not affect FBO, so it may happen that your smooth drawing on screen becomes aliased when you perform this drawing using fbo. One possible solution for smooth graphics is using fbo that is double the size of the screen and shrinking fbo to screen size during drawing.
- When you perform semi-transparent drawing to fbo (with alpha-blending enabled), most probably you should disable alpha-blending when drawing fbo itself on the screen. In the opposite case, transparent pixels of fbo will be blended in the screen one more time, so the resultant picture will be overblended.
- By default, fbo holds color components of its pixels as unsigned char values. When more accuracy is needed, you can use float-valued fbo by allocating it with the optional last parameter GL_RGB32F_ARB.
fbo.allocate( w, h, GL_RGB32F_ARB );
Let's consider an example of using the ofFbo object for accumulated drawing.
Spirals example
Consider a drawing algorithm consisting of the following steps:
- Set a = 0 and b = 0.
- Set the pos point's position to the screen center.
- Set a += b.
- Set b += 0.5.
- Move the pos point a step of fixed length in the direction defined by the angle a measured in degrees.
- Each 100 steps change the drawing color to a new color, generated randomly.
- Draw a line between the last and current positions of pos.
- Go to step 3.
This algorithm is a kind of generative art algorithm—it is short and can generate interesting and unexpected drawings.
The result of the algorithm will be a picture with the the colored trajectory of pos moving on the screen. The b value grows linearly, hence the a value grows parabolically. The value of a is an angle that defines the step pos will move. It is not easy to predict the behavior of steps when the angle changes parabolically, hence it is hard to imagine how the resultant curve will look. So let's implement the algorithm and see it.
We will use the ofFbo fbo object for holding the generated picture.
This is example 02-2D/06-Spirals.
The example is based on the emptyExample project in openFrameworks. In the testApp class declaration of the testApp.h file, add declarations for a, b, pos, fbo, and some additional variables. Also, we declare the function draw1(), which draws one line segment by performing steps 3 to 7 of the drawing algorithm.
double a, b; //Angle and its increment ofPoint pos, lastPos; //Current and last drawing position ofColor color; //Drawing color int colorStep; //Counter for color changing ofFbo fbo; //Drawing buffer void draw1(); //Draw one line segment
Note that a and b are declared as double. The reason is that a grows fast, so the accuracy of float is not enough for stable computations. However, we will play with the float case too, in the Playing with numerical instability section.
The testApp::setup() function initializes the fbo buffer, fills it with a white color, and sets initial values to all variables.
void testApp::setup(){ ofSetFrameRate( 60 ); //Set screen frame rate //Allocate drawing buffer fbo.allocate( ofGetWidth(), ofGetHeight() ); //Fill buffer with white color fbo.begin(); ofBackground( 255, 255, 255 ); fbo.end(); //Initialize variables a = 0; b = 0; pos = ofPoint( ofGetWidth() / 2, ofGetHeight() / 2 ); //Screen center colorStep = 0; }
The testApp::update() function draws line segments in fbo by calling the draw1() function. Note that we perform 200 drawings at once for obtaining the resultant curve quickly.
void testApp::update(){ fbo.begin(); //Begin draw to buffer for ( int i=0; i<200; i++ ) { draw1(); } fbo.end(); //End draw to buffer }
The testApp::draw() function just draws fbo on the screen.
void testApp::draw(){ ofBackground( 255, 255, 255 ); //Set white background //Draw buffer ofSetColor( 255, 255, 255 ); fbo.draw( 0, 0 ); }
Note that calling ofBackground() is not necessary here because fbo fills the whole screen, but we have done so uniformly with other projects.
Finally, we should add a definition for the draw1() function.
void testApp::draw1(){ //Change a a += b * DEG_TO_RAD; //a holds values in radians, b holds values in degrees, //so when changing a we multiply b to DEG_TO_RAD constant //Change b b = b + 0.5; //Shift pos in direction defined by angle a lastPos = pos; //Store last pos value ofPoint d = ofPoint( cos( a ), sin( a ) ); float len = 20; pos += d * len; //Change color each 100 steps if ( colorStep % 100 == 0 ) { //Generate random color color = ofColor( ofRandom( 0, 255 ), ofRandom( 0, 255 ), ofRandom( 0, 255 ) ); } colorStep++; //Draw line segment ofSetColor( color ); ofLine( lastPos, pos ); }
In the original algorithm, described at the beginning of the section, a and b are measured in degrees. In the openFrameworks implementation, we decide to hold b in degrees and a in radians. The reason for this will be explained later, in the Playing with numerical instability section. So, in the code, we convert degrees to radians using multiplication to the DEG_TO_RAD constant, which is defined in openFrameworks and is equal to π/180 degrees.
a += b * DEG_TO_RAD;
Run the project; you will see a curve with two spiral ends constantly changing their color:
This particular behavior of the curve is determined by the parameter 0.5 in the following line:
b = b + 0.5;
The parameter defines the speed of increasing b. Change this parameter to 5.4 and 5.5 and you will see curves with 4 and 12 spirals, as shown here:
Try your own values of the parameter. If the resultant curve is too large and does not fit the screen, you can control its scale by changing the len value in the following line:
float len = 20;
For example, if you set len to 10, the resultant curve shrinks twice.
Playing with numerical instability
In the openFrameworks code, we declare a and b as double values. The double type has much more accuracy when representing numbers than float, and it is essential in this example because a grows fast.
But what will happen if we declare a and b as float? Do it! Replace the line double a, b; with float a, b; and run the project. You will see that the resultant curve will be equal to the curve from the double case just in the first second of the running time. Then, the centers of the spirals begin to move.
Gradually, the two-spiral structure will be ruined and the curve will demonstrate unexpected behavior, drawing circles of different sizes.
The reason for such instability is that the values of a are computed with numerical inaccuracy.
Note that the exploited instability effect can depend on the floating-point arithmetics of your CPU, so your resultant pictures can differ from the presented screenshots.
In many serious tasks such as physical simulation or optimal planning, we need to have the exact result, so such computing instability is unallowable. But from the creative coding and generative art field point of view, such instability lets you create interesting visual or audio effects. So such instability is often permitted and desirable. For more details on the mathematics of such processes, read about the deterministic chaos theory.
Now change the parameter 0.5 in the line b = b + 0.5; to 17, and you will see a big variety of shapes, including triangles, squares, heptagons, and stars. Then try the values 4, 21, and your own. You will see a large number of similar but different pictures generated by this simple drawing algorithm.
Finally, note that the main computing lines of the algorithm are the following:
a += b * DEG_TO_RAD; //... b = b + 0.5; //... ofPoint d = ofPoint( cos( a ), sin( a ) );
These are very sensitive to any changes. If you change it somehow, the resultant curves will be different (in the float case). In this sense, such creative coding can be considered art because it depends heavily on the smallest code nuances, which often cannot be predicted.
Screen grabbing
Sometimes it is desirable to save the picture drawn by your project in the file. You can do it using tools of your operating system, but it's more comfortable to do it right in your project. So let's see how to save the contents of your project screen to an image file.
For such purposes, we need to use the ofImage class for working with images.
The following code saves the current screen to file on the pressing of the Space bar. It should be added to the testApp::keyPressed() function as follows:
//Grab the screen image to file if ( key == ' ' ) { ofImage image; //Declare image object //Grab contents of the screen image.grabScreen( 0, 0, ofGetWidth(), ofGetHeight() ); image.saveImage( "screen.png" ); //Save image to file }
The parameters of the image.grabScreen() function specify the rectangle of the grabbing. In our case, it is the whole screen of the project.
This code is implemented in the 02-2D/06-Spirals example. Run it and press the Space bar; the contents of the screen will be saved to the bin/data/screen.png file in your project's folder.
The PNG files are small and have high quality, so we often use these for screen grabbing. But, writing to a PNG file takes some time because the image has to be compressed. It takes up to several seconds, depending on the CPU and image size. So if you need to save images fast, use the BMP file format.
image.saveImage( "screen.bmp" );
Additional topics
In this article, we have considered some of the basic topics of 2D drawing. For reading further on openFrameworks 2D capabilities, we suggest the following topics:
- Drawing text using the function ofDrawBitmapString() or the class ofTrueTypeFont. See the openFrameworks example examples/graphics/fontShapesExample.
- Drawing filled shapes using the functions ofBeginShape() , ofVertex() , and ofEndShape() . See the openFrameworks example examples/graphics/polygonExample.
- Creating PDF files with openFrameworks drawings. Such files will contain vector graphics suitable for high-quality printing purposes. See the openFrameworks example examples/graphics/pdfExample.
For deeper exploration of the world of 2D graphics, we suggest the following topics:
- Using Perlin noise for simulating life-like motion of objects.
- Using the algorithmic method of recursion for drawing branched structures like trees.
If you are interested in playing with generative art, explore the huge base of Processing sketches at openprocessing.org. Processing is a free Java-based language and development environment for creative coding. It is very similar to openFrameworks (in a way, openFrameworks was created as the C++ version of Processing). Most of the Processing examples deal with 2D graphics, are generative art projects, and can be easily ported to openFrameworks.
Summary
In this article we learned how to draw geometrical primitives using control points, perform transformations of the coordinate system, and work with colors. Also, we studied how to accumulate drawings in the offscreen buffer and considered the generative art example of using it. Finally, we learned how to save the current screen image to the file.
Resources for Article :
Further resources on this subject:
- OpenGL 4.0: Building a C++ Shader Program Class [Article]
- Kinect in Motion – An Overview [Article]
- A quick start – OpenCV fundamentals [Article]