Drawing in 2D

0
127
15 min read

(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:

  1. Store the current transformation matrix using ofPushMatrix().
  2. Change the coordinate system by calling any of these functions: ofTranslate(), ofScale(), or ofRotate().
  3. Draw something.
  4. 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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here