Tutorial 11: Procedural Drawing
Introduction
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.
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 1 for each bang message it receives. When the output reaches 9, the next bang will reset the output to 0. 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 blue) 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 5 then hit the first button, we see that the output is the number 5 – regardless of what the previous value was. This is how we set the state of the counter on the fly. Clicking on the second (blue) button (which sends the bang message) will reset the value to the minimum: 0 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 drawing patch has a section labeled 1; this 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 0 to 767, giving us 768 steps total. The drawing X/Y location is calculated with the % (modulo) and / (division) objects, breaking up our lcd object into a 24x24 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 0 to 23, one going quickly (from the % object) and the other incrementing each time the first loop has repeated (the / object). The values are multiplied by 10 (to get the pixel location for the top-left location), then have 10 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 2 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 back to the top-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 0 and 100 are mapped to the range of -1.0 to 1.0 – this is determined by the arguments provided to the object. If you change the input number box, you will see that a value of 0 yields an output of -1.0, a value of 50 will give us a value of 0.0, and a value of 100 will give us a result of 1.0. 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, a value of 150 will translate into an output of 2.0, while a value of -100 will give you a value of -3. The arguments are used to determine the mathematical expression used for conversion.
The section of the drawing patch labeled 2 uses scale to draw a shape based on sine wave calculations. A counter object produces output ranging from 0 to the maximum value (set using the two number boxes to the right of the metro object). This is scaled from 0.0 to 6.283185 (2*pi), then sent to a sin object, which calculates the sine of the input value. This produces a sine wave that varies from -1.0 to 1.0 in range. This value is sent to another scale object which maps this range to the integer range 0 to 319 – the size range of the lcd 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, prepended with the lineto message (using prepend) and then sent to the lcd.
Clear the lcd using the message box containing the clear 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 to move to that new value.
Clicking on the first message box (labeled 100 1000) will move the line from the starting value of 0 to the target value (100) over 1000 milliseconds. Clicking on it again will show no change, because the current value (which remains at 100) 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.
Clicking on the second message box (0 5000) will move the result of the line back to 0 over 5000 milliseconds (5 seconds). Clicking on the first box will move us back to 100.
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 message, and will function as if two messages were sent down the same patch cord immediately after one another. In this case, the value 20 is sent as the first message. Since there is no time pairing, the current value out of the line immediately jumps to 20. Immediately afterward, the 50 2500 pair is sent, causing the output to slide from 20 to 50 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 section of the drawing patch labeled 3 is the most complex procedure we’ve yet seen. It depends on the idle mechanism of the lcd object to do some procedural drawing. When the idle message is turned on (with the idle 1 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 back at the top. 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 idle 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 (1000 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 four drawing commands in sequence to the lcd. The first two draw two background circles using the paintoval command at fixed positions (100 100 140 140 and 180 100 220 240). The last three arguments to paintoval set the color (black: 0 0 0). The second 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 paintarc 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.
Conclusion
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.