Tutorial 11: Procedural Drawing
In this tutorial, we will be using several new objects to perform procedural drawing with the lcd object. The counter object will provide enumerated step processing, the line object will allow us to create smoothed output, and the scale object will allow us to change the range of output. Finally, we will introduce a few mathematical objects that will help us create some common shapes that require simple trigonometry.
We often use programmatic and procedural constructs to perform tasks in our programs; these can present new opportunities for graphic display, but can also be used any time we need to count through a set of numbers, create a numerical trajectory, or scale values. Whether used for audio visualization, controller feedback or just an interesting bit of art, procedural code can serve as both a useful tool and an inspiration.
To open the tutorial patch, click on the green Open Tutorial button in the upper right-hand corner of the documentation window.
Using counter to draw
Take a look at the tutorial patcher. This patch has a few areas that help us understand some new objects, and another large drawing patch. At the top-left is a small patch that demonstrates the counter object. When we click on the left button, the object produces numeric output that increments by for each message it receives. When the output reaches , the next will reset the output to . The arguments we've provided here to the counter provide the minimum and maximum numbers for the output. When the object hits its maximum, it will loop back again to the minimum number. This allows us to use counter to create loops of values.
The counter object is somewhat unusual in that it can accept a variable number of arguments:
- If you have one argument, it sets the maximum value of the counter.
- If you have two arguments, they set the minimum and maximum values of the counter.
- If you have three arguments, they set the direction, minimum and maximum values of the counter.
Our test patch for counter includes an additional button (colored red) and two number boxes to set values of the object. This is a great opportunity to use the assistance on inlets to find out what they do. If we unlock the patch and hover over the third inlet (which has both a button and number box connected), the assistance says “Resets Counter to Number on Next Clock”. This lets us change the current value of the counter while it is running.
If we change the number box to then hit the first button, we see that the output is the number – regardless of what the previous value was. This is how we set the state of the counter on the fly. Clicking on the second (red) button (which sends the message) will reset the value to the minimum: in this case.
The number box on the right allows us to change the maximum value on the fly. This would be useful for drawing situations where you might need to change the size of the drawing area, or when you are sequencing an effect and need to change the number of steps. When we change this value and repeatedly hit the button, we see that the new maximum value is honored.
The patcher object labeled example 1 is where we use the counter object to procedurally draw in the lcd object. When we start the metro (using the connected toggle object), we see that the patch begins drawing small circles in the lcd, with the color gradually changing as it progresses. When the drawing reaches the bottom of the lcd, it restarts drawing at the top.
Four different counter objects are used in this patch; one to set the drawing location and size, and three others to change the color. Understanding the leftmost counter is the most important: it counts from to , giving us steps total. The drawing X/Y location is calculated with the % (modulo) and / (division) objects, breaking up our lcd object into a x grid. If we look at the output of the counter and the % and / objects, we can see that the system works by creating two loops from to , one going quickly (from the % object) and the other incrementing each time the first loop has repeated (the / object). The values are multiplied by (to get the pixel location for the top-left corner of the shape), then have added (to get the pixel location for the bottom-right). This sets up a lcd-filling grid of circles for our program to draw.
The three other counter objects cycle through three different number ranges to give us an ever-changing color palette. These counter objects use the three-argument initialization, where the initial forces the counter to move upward to the maximum, then downward to the minimum. This is what causes the drawing to slowly change colors as it draws. Clicking the button labeled "reset" will restart the simulation from the beginning of the loop.
Using scale to draw
If we return to the left of the patcher, the second small example shows the use of the scale object. This object allows you to set a range of input values and a range of output values; the object will then correctly scale the values so that they are mapped into the new number range. In our example, input values between and are mapped to the range of to – this is determined by the arguments provided to the object. If you change the input number box, you will see that a value of yields an output of , a value of will give us a value of , and a value of will give us a result of . These numbers are mapped in a linear fashion across the range we provide.
If you choose to exceed the input range, you will find that the output continues to use the calculated range effect, thereby allowing you to exceed the expected range without failing. Hence, an input value ofwill translate into an output of , while a value of will give you a value of .
The patcher object labeled example 2 uses scale to draw a shape based on sine wave calculations. A counter object produces output ranging from to the maximum value (set using the two number boxes to the right of the metro object). This is scaled from to (2*pi), then sent to a sin object, which calculates the sine of the input value. This produces a sine wave that varies from to in range. This value is sent to another scale object which maps this range to the integer range to – the size range of the object. This is used to provide the X locations of the line. A similar set of functions are used to set the Y location. These numbers are then inserted into a list using pack, attached to a message (using prepend) and then sent to the lcd.
Clear the lcd using the message box containing the message. Turn on the metro using the toggle, and watch the result. The result is a sine-based drawing over the entire lcd face. You can set the maximum values of the procedure by altering the number boxes that are attached to the counter objects. Note that the number boxes change not only the parameters of the counter objects, but also the maximum input range expected by the scale objects immediately below (the third inlet for the scale object determines the maximum input range expected).
The two counter objects, when set to different maximum points, create different interlocking sine waves that draw across the lcd. These curves are called lissajous patterns and simulate what happens when out-of-phase oscillators are fed into the X and Y inputs of an oscilloscope. This is a great example of using simple math functions to create complex drawings.
Using line to draw
Going back to the top-left of the patch, the third example gives us a chance to see the line object in action. The line object takes a pair of incoming values (configured as a value/time pairing) and slowly moves from one value to the next. If a single value is received, it jumps immediately to that value. However, when a second value is provided, it is treated as the number of milliseconds used to move to that new value.
Clicking on the first message box (labeled ) will move the line from the starting value of to the target value ( ) over milliseconds. Clicking on it again will show no change, because the current value (which remains at ) is the same as the target value (it has nowhere to go). The line object is “state aware”, meaning that it understands its current value, and will adjust its action based on the state of that value.
The third message box is a little more interesting. It uses a special function of the message box to create multiple messages; when a comma separates values, they are treated as separate messages, and will function as if two messages were sent down the same patch cord immediately after one another. In this case, the value is sent as the first message. Since there is no time pairing, the current value out of the line immediately jumps to . Immediately afterward, the pair is sent, causing the output to slide from to over 2.5 seconds. We can also use the number box to immediately jump the line to a new value; since no timing value is provided, it too will cause the current state to jump to the entered value.
The patcher labeled example 3 is the most complex procedure we’ve yet seen. It depends on the mechanism of the lcd object to do some procedural drawing. When the message is turned on (with the message), the lcd object outputs the current mouse location relative to the lcd object's canvas when you move your mouse over it. These X/Y coordinates are sent as a list out the second outlet of the lcd.
The light blue patchcord out of the lcd connects our mouse coordinates to an unpack object at the top of patch 3. These values are sent into our drawing code, which creates a set of values used to draw a pair of round shapes that follow the cursor. The line object is used to slow the changes in the shapes, and causes them to slowly "zero in" on the current cursor location. Turn on the message with the toggle box, then mouse around the lcd object to see how it reacts.
There are two very important things in this procedure. First, the use of the line object to slew the output values over a one second ( ms) timeframe. This gives the results a more organic feel by causing them to interpolate over time. Secondly, we use the cartopol (cartesian to polar) object to change the X/Y ("Cartesian") position of the cursor into a Distance/Angle ("Polar") pair of coordinates, useful for drawing contents in a circular domain. Once the polar coordinates are calculated, they are scaled to match the size of the circular drawing area, then used as part of a complex, multi-message message box to create the two “eyes” found in the middle of the lcd object.
The message box at the bottom of the code sends five drawing commands in sequence to the lcd. The beginning removes the last drawing to provide continuous animation. The next two draw two background circles using the command at fixed positions ( and ). The last three arguments to set the color (black: ). The final two messages use the values from the pack object to draw green "pupils" on the eyes. The four values provide the starting and ending angle for the pupils using the message.
Understanding all of the functionality of this procedure may be difficult, but it is worth it – this is another example of using some mathematical programming to create a complex and life-like end result. Simple realtime graphics are made out of building blocks of procedural code just like this; taking a set of input values and scaling them to your needs is critical for many applications in Max.
We’ve seen some fairly complex programming that provides controlled drawing functionality within an lcd object by creating loops (counter), scaling input values to output values (scale), and interpolating numbers over time (line). We’ve also worked with a few objects that compute trigonometry. By using these new objects in combination, we can assert a lot of procedural control over what we want to accomplish in our programs.