Fun with Macros: Snowflakes

Macros in CC3+ can be used for a variety of tasks, so today I thought I would explore macros by having a little bit of fun. I wanted to write a macro that could decorate my maps with randomly placed tiny snowflakes in spirit of the winter season.

In this case, I made a macro that draws small snowflakes in random location all over the map, each snowflake being generated randomly. The snowflakes themselves are just simple white lines, which can then be enhanced by adding sheet effects to them.

In this article, I’ll just present and explain my macro, if you are new to macros you may want to start with my Getting Started with Macros series first: Part 1, Part 2, Part 3.

Let us just start with the complete macro, which looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
MACRO SNOW
ECOFF
:Initialize Values
GN numFlakes 100
GN numFlakes ^DNumber of Snowflakes to draw [100]:
SAVESETTINGS
SELSAVE
GETDISTFMT origDistFmt
DISTFMT 0
GETLAYERL xy1 MAP BORDER
GETX x1 xy1
GETY y1 xy1
GETLAYERH xy2 MAP BORDER
GETX x2 xy2
GETY y2 xy2
GV mapWidth x2-x1
GV mapHeight y2-y1
GOLAYER SNOWFLAKES
GOSHEET SNOWFLAKES
COLOR 15
LWIDTH 0
LSTYLE Solid
FSTYLE Solid
GV baseLength mapWidth/200
:Draw Snowflakes
GOLAYER TEMPSNOW
GV loops numFlakes
:loop
RANDOM r1
RANDOM r2
RANDOM ac
RANDOM ang
GN innerLoop ac*5+4
GN armCount innerLoop
:inner
GV a innerLoop*360/armcount
LINE r1*mapWidth+x1,r2*mapHeight+y1;<a+(ang*360),baseLength;
GN innerLoop innerLoop-1
IFP innerLoop inner
SELBYL
CHANGEL TEMPSNOW;SNOWFLAKES
SELBYP
GROUP
GV loops loops-1
IFP loops loop
:Restore Settings
DISTFMT origDistFmt
GETSETTINGS
SELREST
ECON
ENDM

 

This macro is divided into 3 main section, the setup section where we save the user’s current settings, figure out the extents of the map we operate on, initialize our variables, and set basic settings. Then we have the main body of the macro which consists of two loops, one outer loop that iterates once for each snowflake, and an inner loop that draws each arm on each individual flake. Finally, we finish the macro by restoring the user’s settings to what they were before starting the macro.

Now, that was the high level explanation, but let us dive down and dissect the macro a bit more.

 

 2

50
ECOFF

ECON

When running a macro that does a lot of commands, like drawing our snowflakes will be, the command line will be flashing commands like crazy. These commands turn off (and back on) the update of the command line while the macro runs unless it actually prompts us for something we need to answer.

 

 4
 5
GN numFlakes 100
GN numFlakes ^DNumber of Snowflakes to draw [100]:

These two lines are used to prompt the user for the number of snowflakes to draw. GN is the macro command to get an integer (whole) number. What we do here is that we first set it to a sensible value (100), and then we ask the user for a number. By defining a valid number first, we ensure that the macro gets a valid number to work with even if the user fails to give a proper number. If the input from the user is invalid, the variable is never changed, so it just keeps the base value of 100.

Remember here that when we set variables, if we instead of providing a value we type ^D followed by some text, it means CC3+ will ask the user to provide the value on the command line.

 

 6
 7
 8

47
48
49
SAVESETTINGS
SELSAVE
GETDISTFMT origDistFmt

DISTFMT origDistFmt
GETSETTINGS
SELREST

Since we need to change various settings during the macro, we use these lines to store the current settings at the beginning of the macro and restore them at the end. SAVESETTING/GETSETTINGS handles the various entity property settings, like line width, line style, color, fill style, layer and so on. SELSAVE/SELREST takes care of preserving the selection method, while GETDISTFMT/DISTFMT is used to store the distance format (metric/imperial, number of decimals) used by various displays.

 

 9
DISTFMT 0

When working with numbers retrieved from map sizes and such, you normally want to work with plain decimal numbers, as other formats, such as the composite feet/inches are very hard to work with. This command sets the number format to standard numeric decimal numbers.

 

10
11
12
13
14
15
16
17
GETLAYERL xy1 MAP BORDER
GETX x1 xy1
GETY y1 xy1
GETLAYERH xy2 MAP BORDER
GETX x2 xy2
GETY y2 xy2
GV mapWidth x2-x1
GV mapHeight y2-y1

Next up is figuring out the extents of our map. We need these values so we can ensure that all our snowflakes are drawn inside the map area. GETLAYERL and GETLAYERH retrieves the lower left point and the upper right point of the combined extents of all entites on the specified layer (here MAP BORDER). I then use GETX/GETY to split the point into X and Y values for each. Note that I could assume that the lower left corner was 0,0, this would be true for most maps, but it only cost me a few extra lines to grab the values ensuring this works on maps that are not set up with 0,0 in the bottom left.

I then take the X and Y values and calculate the map width (GV is the macro command for getting a decimal value) and store it in appropriate variables. Note that there are commands like GETLAYERX that could get the width directly, but I needed the coordinate anyway, so I could just as well calculate it from them.

 

18
19
GOLAYER SNOWFLAKES
GOSHEET SNOWFLAKES

It is best to put these entities on their own sheet and layer. I use the GOLAYER/GOSHEET command for this, as it will create the sheet and layer if they don’t already exist, and set them as the active one. I used a name that was unlikely to be found in any existing map to avoid conflicts, and I also avoided naming them the same as the name of the macro, as this can cause issues with variable expansion later in the macro.

 

20
21
22
23
COLOR 15
LWIDTH 0
LSTYLE Solid
FSTYLE Solid

Setting the properties we will use to draw the flakes, Color, Line Width, Line Style and Fill Style. I assume that color 15 is white, it should really be in all maps (If you edit the palette, you shouldn’t ever change the first 32 colors). Since we saved the current settings earlier, it is safe to change these here, since when we restore settings at the end of the macro, CC3+ will be back to whatever settings that were active then.

 

24
GV baseLength mapWidth/200

This line is used to set the size of the snowflakes. The baseLength variable will be used for each “arm” of the snowflake, so the snowflake diameter is actually 2 times this value. This means our formula results in the snowflake diameter being 1/100th the map width, ensuring it has a visible size no matter the size of the map the macro is used on. Of course, this is intended to be visible when the map is zoomed all the way out, if you use it on a highly detailed very large map, the flakes will be gigantic when zoomed in.

 

26
27
28
29
30
31
32

40
41
42
43
44
45
GOLAYER TEMPSNOW
GV loops numFlakes
:loop
RANDOM r1
RANDOM r2
RANDOM ac
RANDOM ang

SELBYL
CHANGEL TEMPSNOW;SNOWFLAKES
SELBYP
GROUP
GV loops loops-1
IFP loops loop

Here’s the outer loop. The actual drawing happens in the inner loop below, but the outer loop sets up the values needed for the inner loop. Each run of the outer loop represents one complete snowflake

We start by changing to a temporary layer TEMPSNOW. This is done to make it easier to select the entities we just drew. By drawing them on a temporary layer with nothing else on it, we can simply select everything on that layer.

I start the loop by declaring a loop variable called loops based on the number of flakes input by the user earlier, and then I add a label :loop that we can jump to at the end of the loop to go to the next iteration.

The four RANDOM lines generate random numbers between 0 and 1. I will be using these in the inner loop for the X-coordinate, Y-coordinate, number of arms, and angle of rotation of the flake.

After the inner loop has run, drawing each arm as a separate line, the outer loop needs to finalize each flake. First, we set the selection method to Select by Layer (SELBYL). This means that I now select entities based on which layer they are on instead of having to individually identity them. This is used in the next line, where I use CHANGEL to change the layer of the newly drawn lines from TEMPSNOW to SNOWFLAKES. I then change the selection method again, this time to Select by Prior (SELBYP) which means my selection will automatically be whatever I had selected last, which at this point will be the lines I just moved to the SNOWFLAKES layer. So when I issue the GROUP command to group these lines, the last set of lines will be automatically selected ensuring only the most recent lines are added to the group.

Finally, I decrement the loop variable by one, and then I check if the loop variable is still positive (IFP) and if it is, it jumps to my loop label, repeating for the next snowflake. Once the macro have drawn as many snowflakes as requested, the loop variable should reach zero, causing IFP to fail, and let the macro continue on instead of repeating the loop.

 

33
34
35
36
37
38
39
GN innerLoop ac*5+4
GN armCount innerLoop
:inner
GV a innerLoop*360/armcount
LINE r1*mapWidth+x1,r2*mapHeight+y1;<a+(ang*360),baseLength;
GN innerLoop innerLoop-1
IFP innerLoop inner

The inner loop is where the drawing happens. We start off by declaring a loop variable. I decided I wanted between 4 and 8 arms on my flakes. The random function creates a number between 0 and 1 (not inclusive). So the way to generate any integer random number is to multiply by the max, and adding 1. So if I wanted random numbers between 1 and 10, I take the output from RANDOM, multiply it by 10, and add 1. However, here I wanted a minimum of 4, so I instead multiply the value by 5 (This results in numbers between 0 and 4 inclusive) and add 4, ending up with the values I want, between 4 and 8 inclusive. Since I need to access the original number in the loop I also assign it to a separate variable, armCount.

Now, on line 36 I need to figure out the angle of the arm I am working on. The angle between arms are simply calculated by dividing 360 degrees by the number of arms, and I then multiply this by the loop variable to get the angle of the current arm being drawn.

Each arm is drawn by the LINE command, which draws a line from the center of the snowflake. You may remember from the outer loop, I generated the random values r1 and r2 for coordinates. As mentioned above, these random values are between 0 and one, all I have to do is to multiply them by the width/height of the map to get a coordinates needed. But, since I also made sure we could handle maps that did not have 0,0 as the bottom left, I also add the x/y values from the map bottom left to the final value to ensure the snowflake is positioned properly within the map. This gives me the following calculation for the center point of the snowflake, which is used as the first point for each arm line: r1*mapWidth+x1,r2*mapHeight+y1

For the second coordinate of each arm, I turn to polar coordinates. Polar coordinates are great, because instead of doing several complicated formulas to find the coordinates I need, I can just specify the angle and the distance. The angle is easy, I calculated that above and assigned it to the variable a. And the length is equally easy, I already have that in the baseLength varible. So, in the simple form, the second coordinate of the line is simply <a,baselength. (Keep in mind that in this context, the symbol < means we are providing a polar coordinate, it is not used in the meaning of a less-than symbol.). Now, I did want to have random rotation for my flakes, which is why I already generated a random number for this in the outer loop. And since I need a random number between 0 and 360, it is just taking this random number, multiplying it by 360 and adding it to my angle variable, so the final form of the second coordinate becomes <a+(ang*360),baseLength.

Do note that at the end of line 37 (the LINE command), there is a ;-character. While common in many programming languages, normally, you don’t put ;-characters at the end of lines in CC3+ macros. But it is needed here. This is because the LINE command doesn’t end untill you give it an empty input. Otherwise, it just continues on (Go ahead, just try it manually in CC3+). So the way to provide a blank input is to simply put a ;-sign, which is a separator character, immediately followed by newline, which is another separator character. This will cause CC3+ to get a blank input, the LINE command ends, and we move cleanly on to the next line. If you forget this, you will get errors when running the macro. Also note, that if you put a ; at the end of lines where it doesn’t belong, you cause other problems. This is because if CC3+ receives a blank input when NOT in the middle of a command, it will try to repeat the previous command. This is very helpful when you are drawing normally, interacting with the program, but quite problematic in most macros.

Finally, the inner loop is checked (and terminated) the same way we did our outer loop.

 

Effects

You’ll probably want to add some effects to your SNOWFLAKES sheet. For my example map here, I added two effects. Outer Glow and Bevel, Lighted, using the following settings. Note that my settings are configured relative to map size, so the values should work no matter the actual size of the map, but you may want a different look than I have.

Troubleshooting

First of all, the sheet used for the snowflakes likely doesn’t exist in your map before running the macro. This means that it will be created and placed at the bottom of the list, meaning entities on it will overlap everything else. I recommend moving it a bit upwards, at least above the SCREEN and MAP BORDER sheets in the list, but if you have a title box like in my example map, you may wish to even move it above that as well ensuring the snow doesn’t overlap it.

As for the macro itself, make sure everything is typed exactly right. CC3+ treats spaces like separator characters, so typing GN myVar 3 + 3 is NOT the same as typing GN myVar 3+3. And when using ;’s, make sure you understand their meaning in the context you are using them.

This macro also sets several settings, most noticeable of these is the command line echo and the selection method. If the macro crashes half-way through before it can clean up and restore the values, you may find things a bit broken. To restore echo to the command line, simply type the command ECON on the command line (and hit enter), and to restore your selection method to normal, simply type SELBYD and hit enter. (SELBYD means Select by Dialog and is the normal way you select entities in CC3+ where you can click on entity edges, drag selection windows, and confirm the selection with Do It. If it is left as Select by Layer, you’ll notice that instead of asking you to picking entities, the command line will ask you for a layer when you try to run a command that requires selection, like change properties. And if it is left as Select by Prior, it won’t ask you at all, it will simply execute the command on whatever your prior selection or latest added entity was)

 

If you have questions regarding the content of this article, please use the ProFantasy forums. It can take a long time before comments on the blog gets noticed, especially for older articles. The forums on the other hand, I frequent daily.

 

 

Leave a Reply