VOICE Home Page: http://www.os2voice.org
[Newsletter Index]
[Previous Page] [Next Page]
[Features Index]

July 1999
editor@os2voice.org

View From the End (User)

By: Don K. Eitner (freiheit@tstonramp.com) http://www.tstonramp.com/~freiheit/


Learning REXX The Photo>Graphics Way

I Did It, You Can Do It Too

It was only in the past few months that I acquired a copy of TrueSpectra's (sadly defunct) Photo>Graphics Pro for OS/2 vector draw application. Those familiar with PGPro know that it comes with a nice little selection of REXX scripts with full source code showing. Taking this as a sign that I could probably learn REXX scripting if I applied myself, I dove into the sample scripts and was soon modifying lines here, tweaking variables there, and watching how my modifications affected the script's performance in Photo>Graphics Pro.

One script in particular interested me. The regshad.cwx file creates a standard drop-shadow of a text region which looks pretty stylish and could have numerous professional uses. But I decided I wanted something a little different -- something based on the same drop-shadow idea but which would give a more 3D perspective feel to the finished product. My fingers flew across the keys and within days I had produced three workable sets of scripts (version 1.0, 1.1, and 1.2) with some bug fixes and feature enhancements at each step. I say sets of scripts because there was a script for each of four shadowing directions: up and left, up and right, down and right, down and left. Each script performed the same function but moved the shadow "instances" (6 of them created during each running of a script) in different directions. The shadow instances faded from pure white (255,255,255 in RGB values) to pure black (0,0,0) stepping by 51 levels at each instance.

Fast forward now through version 1.3 (failed attempt to handle one of PGPro's interestingly unique types of objects known as a group) and version 1.4 (technically full-featured but still stuck in 4 separate scripts) to the release of my PGPro SCripts v1.5. For the first time a single script performs all the tasks needed to handle all Photo>:Graphics object types (including groups). This release also marks a monumental accomplishment in taking input from the user at runtime to vary the number of shadow instances. Since it was these scripts which provided my means to understanding REXX, I would like to now share some not-so-secrets with any of you who may also be interested in learning REXX but find it somewhat daunting to learn a new programming language. I will walk you through PGPro Scripts v1.5 (the filename is 3DShadow.cwx by the way) and hopefully shed some light on what the various parts of the script do and why they work this way.

Beginning A Script


/*
 3DShadow.cwx v1.5 - by Don Eitner, 1999

 Creates a "3D shadow" in the selected direction for the selected object.

 This code is neither supported nor under warranty.  Feel free to
 examine and modify this script for your own purposes.  See the
 included readme.txt for additional information.
*/

Every REXX script must begin with the comment tags /* */. You can place anything you like in between these, as you see above. Usually people put some information about the script's purpose, the author, etc. This is the easy part, and technically this is already a 100% valid REXX script.

Handle.0 = CwGetSelectedObject()

/* no object selected */
if \CwIsHandleValid(Handle.0) then
 do
  call CwMsg "No object selected."
  exit
 end

Okay, I got this from TrueSpectra's own regshad.cwx script -- I told you it had a major influence on my own script. :) What this does is to check what object in the Photo>Graphics project is currently selected. Selected objects have the "marching ants" border around them. If there is no selected object, this script will exit after popping up a dialog box saying so. This is because there must be something selected for the script to work on. We cannot create a 3D shadow of an empty canvas. It would be like asking what is the sound of one hand clapping.

/* Set initial variables */
Output  = CwGetAppHandle("output settings");
Measure = CwGetProperty(Output, "unit");

/* Force project into pixels mode -- we'll change it back later */
if (Measure = "Inches") | (Measure = "Centimeters") | (Measure = "Points") then
     call CwSetProperty Output, unit, "pixels"

/* Set additional variables */
oHeight = CwGetProperty(Output, "output size:Height")
oWidth  = CwGetProperty(Output, "output size:Width")
Height  = CwGetProperty(Handle.0, "Position:Height")
Width   = CwGetProperty(Handle.0, "Position:Width")
Rotate  = CwGetProperty(Handle.0, "Position:Angle")
Sheer   = CwGetProperty(Handle.0, "Position:Skew")

if oHeight <= 200 then
 Move = 1
else
 Move = (oHeight / 200)

In the above section of code, I have set up some initial variables which will be used throughout the script to set other variables. In this case, for instance, the Output variable holds the handle (Photo>Graphics' internal name to keep track of the objects in a given project) to the project's output settings, which include width, height, unit of measurement, and so forth.

I find it's easier, for this script anyway, to work in pixels mode, so I set the project's unit of measurement to "pixels" and keep the original unit in the Measure variable. This way I can set the project back into whatever unit it was in before invoking this script.

I then go on to set some additional variables which will be used later by the shadow instances I create. I didn't like the idea of always moving shadows by the same amount of pixels regardless of the size of the output window, so I used an IF ... THEN ... ELSE statement to determine the project's output size (when rendered to a bitmap file) and set the Move variable accordingly. Move will be used later to shift shadow instances by the given amount of pixels.

/* Prompt user for number of shadow instances */
call Prompt1

/* Wait for user to press a button */
call Wait1

/* Prompt user for direction of shadow */
call Prompt2

/* Wait for user to press a button */
call Wait2

There's a lot to be said about REXX's call statement. It sends the processing of the script to a subroutine, that is to another spot in the script where a certain set of commands are run and then processing returns back to the same spot here. For instance, I have sent the script's processing down to the Prompt1 subroutine (see below). When that routine completes its task, processing of the script comes back here and continues to the next processable line which is a call to the subroutine known as Wait1.

You have surely noticed the numerous blocks of /* */ throughout this script. Just as with the beginning of any REXX script, these comment tags may be used almost anywhere else within the script when the author wishes to make a note about how a section of code works or perhaps how it might be done differently later on. This is a good way to code, as you can easily go back a year or two from now and remember why you coded your script the way you did. Also, if someone else takes your script and wishes to update it, these comment blocks give them a clue to what you were thinking when you first wrote it. Comment blocks like these are not processed and are here solely for the author's benefit, so feel free to be as descriptive as you like.

/* Do the shadowing of the selected object */
do Num=1 to Instances
 Prevnum = Num - 1
 Handle.Num = CwDuplicateObject(Handle.Prevnum)
 Colors = (255 % (Instances - 1))
 ColorVal = 255 - Colors * (Num-1)

 /* move the current shadow instance by the value specified in MOVE */
 Xcenter = CwGetProperty(Handle.Prevnum, "Position:X Center")
 Ycenter = CwGetProperty(Handle.Prevnum, "Position:Y Center")
 Xcenter = Xcenter + (XMod * Move)
 Ycenter = Ycenter + (YMod * Move)

 call CwSetProperty Handle.Num, "Position:Y Center", Ycenter
 call CwSetProperty Handle.Num, "Position:X Center", Xcenter
 call CwMoveObjectBehindObject Handle.Num , Handle.Prevnum
 call CwSetPosition Handle.Num, Xcenter, Ycenter, Width, Height, Rotate, Sheer

     /* Check to see if the object is a group object */
 call Recurse Handle.Num, 'call CwSetTool handle, "Solid Color"'

     /* Do the shading of the current shadow instance */
 call Recurse Handle.Num, 'call CwSetProperty CwGetTool(handle), "Color:HSV Color", "('ColorVal','ColorVal','ColorVal')"'
end

Now here is the fun part of the script! The above section of code performs the actual task of creating shadow instances, moving them within the project to give the 3D effect, and changing their color to fade from solid white to solid black as you move backward.



Image before running the script

Image after running the script

The do statement has many functions, but in this instance it allows loop processing. That is, the code that comes after the line "do Num=1 to Instances" which is also before the "end" statement is processed again and again until the Num variable climbs from 1 to whatever value is held in the Instances variable. We have not set the Instances variable yet -- and yet we have. You see, the script was previously sent down to four subroutines (Prompt1, Wait1, Prompt2, and Wait2) where the Instances variable was set. When processing returned back up here, the variable was retained and now we are using its value. You can think of this as going to find your keys and then returning to open your car door with them. You can't do it in the other order even though you walked out of your house to the car before realizing you'd left the keys in the house. This script, then, is not processed in a linear manner (that is, from top to bottom without jumping to another area at any time). When the call statements were reached, the script jumped down to handle them and then returned to continue in a linear path.

There's a lot to talk about here, despite the rather short section of code we're looking at. For one, the Handle.Num variable takes our old friend Num (from "do Num=1 to Instances") and generates an array or collection of similarly named variables. On the first pass through this section we create Handle.1. On the second pass we create Handle.2. Prevnum always holds a value 1 lower than Num, and this is important. You will recall from the very top of the script that the object selected by the user for shadowing was given the variable Handle.0, so on the first pass through this part of the script, Handle.Prevnum refers back to Handle.0. The function CwDuplicateObject() is being used to take Handle.Prevnum and duplicate it exactly -- positioning, height, width, color, and so forth. We then set up some variables based on the positioning of the previous object and our earlier Move variable and use them to shift the position of Handle.Num, which is the current shadow instance.

The function "call CwSetProperty" takes three bits of data for processing. The first must be a handle (name used by the program to refer to the object in question). In this case, the handle is Handle.Num. The second bit of data is the property of Handle.Num which is being altered. For instance "Position:Y Center" indicates we are moving the object along the up-down axis rather than left-right. The final bit of data needed is the new value for the property being changed. In this example this would be Ycenter (be careful not to use quotation marks around it or P>:G Pro will think it's a literal string and not the variable that it is). The variable YCenter has already been defined by a modification of the previous object's YCenter and the Move variable, as well as a Mod (modifier) variable which will be explained later -- in short, Mod just tells the script whether to move in a positive or a negative direction along the given axis. Note however that we're only changing these values within variables, still. The object itself has not moved.

So now we've got our variables all set for our current shadow instance and now it's time to move it around. First we push it behind the previous instance with the "call CwMoveObjectBehindObject Handle.Num , Handle.Prevnum" line. This does as it says -- moves the object referred to by Handle.Num behind the object referred to by Handle.PrevNum. Now to actually move it along the X/Y axes, we use "call CwSetPosition Handle.Num, Xcenter, Ycenter, Width, Height, Rotate, Sheer". This is a nice string of the variables we previously set up, set down in a specific order in which Photo>Graphics Pro expects to find them. The variable names I've used are pretty self-explanatory. You set the x and y points for the object's center, you set the width and height of the object, and you set the angles or rotation and skew (or sheer). We set up Rotate and Sheer earlier in the script by checking what the originally selected object's angle and skew were set to. This ensures that all of our shadow instances stay with the original object rather than splaying out all around it.

I'll explain the two "call Recurse" lines in a moment. First, let's give the script an official ending point.

/* Set project back to original unit of measurement */
call CwSetProperty Output, unit, measure

exit

I told you we'd set the project back to its original unit of measurement when we were finished. :) Of course, if the original unit was pixels, we didn't have to change anything. When REXX comes across an "exit" instruction, it ends the current script and returns control back to the user. Don't worry about it apparently being in the middle of this script. Remember, the script is not necessarily read in a linear fashion. We skipped down to the stuff before "exit" with the call subroutine commands earlier. If we did not have "exit" here, though, the script would get to this point and then try to go through our subroutines again, which would create quite a mess.

/* If object is a group, recurse into it. Otherwise, perform operation */
Recurse: procedure
parse arg handle, operation

 if translate(CwGetHandleType(handle)) == "GROUP" then
     do
  handle = CwFindFirstObject(handle)
  do while CwIsHandleValid(handle)
   if translate(CwGetHandleType(handle)) == "GROUP" then
            call Recurse handle, operation
   else
    interpret operation
    handle = CwFindNextObject(handle)
               end
  end
 else
  interpret operation

return

Back to the earlier "call Recurse" lines. The code snippet above is designed to walk through a very special object type known as a group. Basically, a group is a container for any other object types and is usually used to keep several objects together so that they move and resize together to maintain a clean and consistent look.

We first check to see if the object referenced by the handle variable is a group of objects. If not, we execute the commands following the outermost else instruction. That is, we send the value of the operation variable to the REXX interpreter to run as if it were hard-coded here in the script. Having it in variable form, however, allows us to send many different instructions to this same subroutine and have them executed in their own special ways.

If the handle object is indeed a group, we start to walk into that group looking for nested groups within it. There may or may not be groups within groups, and if there are not any, we again send operation to the REXX interpreter and go looking for additional objects to search. This continues until there are no further objects within the current group, and then processing returned back to the original calling statement.

/* Prompt user for number of shadow instances */
Prompt1:

 /* Stop drawing until we've setup the display */
 call CwClearSelectionRectangle
 Window=CwGetCurrentView()
 call CwStopRender window

 /* Drop a white box over everything so we can see our prompts */
 boxeffect = CwCreateEffect('Rectangle', 'Solid Color')
 call CwSetPosition boxeffect, oWidth/2, oHeight/2, oWidth, oHeight, 0,0
 call CWSetName boxeffect, "DIALOG"
 whitebox = CwGetTool(boxeffect)
 call CWSetProperty whitebox, "Color", "(255,255,255)"

 /* show the prompts */
 text1effect = CwCreateEffect('Headline Text', 'Solid Color')
 call CwSetPosition text1effect, oWidth/4, oHeight-oHeight/12, oWidth/2, oHeight/6, 0, 0
 call CWSetName text1effect, "UPLEFT"
 textobj = CwGetRegion(text1effect)
 call CWSetProperty textobj, "Caption", "# of Shadows:"
 textobj = CwGetTool(text1effect)
 call CWSetProperty textobj, "Color", "(0,0,0)"

 text2effect = CwCreateEffect('Headline Text', 'Solid Color')
 call CwSetPosition text2effect, oWidth-oWidth/6, oHeight-oHeight/12, oWidth/3, oHeight/6, 0, 0
 call CWSetName text2effect, "UPRIGHT"
 textobj = CwGetRegion(text2effect)
 call CWSetProperty textobj, "Caption", "6"
 textobj = CwGetTool(text2effect)
 call CWSetProperty textobj, "Color", "(255,0,0)"

 btneffect = CwCreateEffect('Ellipse', 'Solid Color')
 call CwSetPosition btneffect, oWidth/2, oHeight/6, oWidth/4, oHeight/3, 0, 0
 call CWSetName btneffect, "OK"
 o = CwGetTool(btneffect)
 call CWSetProperty o, "Color", "(0,255,0)"

     /* Show user instructions */
 instructeffect = CwCreateEffect('Block Text', 'Solid Color')
 call CwSetPosition instructeffect, oWidth/2, oHeight/2, oWidth/1.5, oHeight/3, 0, 0
 call CWSetName instructeffect, "INSTRUCTIONS"
 o = CwGetRegion(instructeffect)
 call CWSetProperty o, "Caption", "Set red value above, then select green button below."
 call CwSetProperty o, "Justification", "Center"
 o = CwGetTool(instructeffect)
 call CWSetProperty o, "Color", "(0,0,255)"

 /* Now render the screen for the user */
 call CwStartRender window

return

The above code comprises the Prompt1 subroutine we called very early in this script. It may look like a lot, but surprisingly all it does is set up a user-input display -- it doesn't even process the input.

You can again read the /* */ comment blocks to get a good idea of what is going on here. For instance, to improve performance on slower systems, we temporarily stop all graphics rendering until the input screen is fully set up. This is accomplished with just two lines: "Window=CwGetCurrentView()" and "call CwStopRender window". The first creates a variable Window which contains a handle to the current view. Current view is defined here as the project window but it could be a Custom Region which acts as a sort of sub-project. The second line tells Photo>Graphics to stop rendering in the current view. Rendering can eat quite a bit of CPU time and throttle a slow video card, so the performance improvement over not stopping rendering may or may not be a big issue for you. For me it was.

Perhaps the most interesting function in this section of code is the CwCreateEffect() which allows us, in one step, to set up a Region with a Tool/Fill. You'll see that I have created both Rectangle regions and Headline Text regions with Solid Color fills. Through a series of commands designed to retrieve specific information about a selected region or the region's tool/fill, I can then set the caption (displayed text) of the Headline Text regions, the color of the Solid Color tools/fills, and so forth. Once the interface is set up, We start rendering again so the user can actually see what we've created for them.

/* Prompt user for shadow direction */
Prompt2:

 /* Stop drawing until we've setup the display */
 call CwClearSelectionRectangle
 Window=CwGetCurrentView()
 call CwStopRender window

 /* Drop a white box over everything so we can see our prompts */
 boxeffect = CwCreateEffect('Rectangle', 'Solid Color')
 call CwSetPosition boxeffect, oWidth/2, oHeight/2, oWidth, oHeight, 0,0
 call CWSetName boxeffect, "DIALOG"
 whitebox = CwGetTool(boxeffect)
 call CWSetProperty whitebox, "Color", "(255,255,255)"

 /* show the prompts */
 text1effect = CwCreateEffect('Headline Text', 'Solid Color')
 call CwSetPosition text1effect, oWidth/6, oHeight-oHeight/12, oWidth/3, oHeight/6, 0, 0
 call CWSetName text1effect, "UPLEFT"
 textobj = CwGetRegion(text1effect)
 call CWSetProperty textobj, "Caption", "Up ~Left"
 textobj = CwGetTool(text1effect)
 call CWSetProperty textobj, "Color", "(255,0,0)"

 text2effect = CwCreateEffect('Headline Text', 'Solid Color')
 call CwSetPosition text2effect, oWidth-oWidth/6, oHeight-oHeight/12, oWidth/3, oHeight/6, 0, 0
 call CWSetName text2effect, "UPRIGHT"
 textobj = CwGetRegion(text2effect)
 call CWSetProperty textobj, "Caption", "Up ~Right"
 textobj = CwGetTool(text2effect)
 call CWSetProperty textobj, "Color", "(255,0,0)"

 text3effect = CwCreateEffect('Headline Text', 'Solid Color')
 call CwSetPosition text3effect, oWidth/6, oHeight/12, oWidth/3, oHeight/6, 0, 0
 call CWSetName text3effect, "DOWNLEFT"
 o = CwGetRegion(text3effect)
 call CWSetProperty o, "Caption", "Down ~Left"
 o = CwGetTool(text3effect)
 call CWSetProperty o, "Color", "(255,0,0)"

 text4effect = CwCreateEffect('Headline Text', 'Solid Color')
 call CwSetPosition text4effect, oWidth-oWidth/6, oHeight/12, oWidth/3, oHeight/6, 0, 0
 call CWSetName text4effect, "DOWNRIGHT"
 o = CwGetRegion(text4effect)
 call CWSetProperty o, "Caption", "Down ~Right"
 o = CwGetTool(text4effect)
 call CWSetProperty o, "Color", "(255,0,0)"

     /* Show user instructions */
 instructeffect = CwCreateEffect('Block Text', 'Solid Color')
 call CwSetPosition instructeffect, oWidth/2, oHeight/2, oWidth/1.5, oHeight/3, 0, 0
 call CWSetName instructeffect, "INSTRUCTIONS"
 o = CwGetRegion(instructeffect)
 call CWSetProperty o, "Caption", "Click a corner to select shadow direction."
 call CwSetProperty o, "Justification", "Center"
 o = CwGetTool(instructeffect)
 call CWSetProperty o, "Color", "(0,0,255)"

 /* Now render the screen for the user */
 call CwStartRender window

return

Almost the same as Prompt1, this code snippet sets up the second user input screen to determine the direction of the shadow instances we will later create (up above in the "do Num=1 to Instances" section)

Wait1:

 /* Begin loop to wait for button selection */
 if RxFuncQuery('SysLoadFuncs') then
  do
   call RxFuncAdd 'SysLoadFuncs', 'REXXUTIL', 'SysLoadFuncs'
   call SysLoadFuncs
  end

 do forever
  /* sleep */
  rc = SysSleep 5

  /* see what (if anything) is selected */
  obj = CWGetSelectedObject()

  /* nothing selected?  Continue */
  if \CwIsHandleValid(obj) then ITERATE

  /* Something selected.  What is it? */
  name = CWGetName(obj)

  /* if it is a button then go on */
          if name = "OK" then
               do
        o = CwGetRegion(text2effect)
        Instances = CWGetProperty(o,"Caption")
                    LEAVE
               end

 end

 /* delete all the prompting stuff before continuing */
     rc = CwDeleteObject(btneffect)
 rc = CwDeleteObject(text1effect)
 rc = CwDeleteObject(text2effect)
 rc = CwDeleteObject(instructeffect)
     rc = CwDeleteObject(boxeffect)
return

This subroutine loads system REXX functions so that we can use the SysSleep function. This tells the script to stop processing for a while to give the user time to make a selection on the user input screen (Prompt1) which is currently being displayed. After 5 seconds, we check to see if anything is selected. If so, we check to see what that something is. In this subroutine, the only thin we care about is the "OK" button. If it is pressed, we use the CwGetProperty function of Photo>Graphics to read the caption (displayed text) of the text2effect handle. This caption determines the value of the Instances variable, which you will recall is used in a loop to create the shadow instances.

This, for the first time in the history of PGPro Scripts, allows the user to define the number of shadows to create and thereby how many steppings to change the grayscale value of each. Originally you were limited to 6 -- no more, no less -- because it made the math easy for me. But when I realized I could set the color values easily in relation to the number of shadow instances, I added this functionality. Specifically, the earlier (but processed later--you know the story) lines "Colors = (255 % (Instances - 1))" and "ColorVal = 255 - Colors * (Num-1)" set the dynamic color fading. The first line there sets a variable Colors as 255 (the maximum allowable for any of the Red, Green, or Blue values) divided by the number of instances minus 1. This modification (subtracting one from the number of instances) helps to ensure the extremes are pure white and pure black.

It's a good thing to be going through my scripts trying to explain them to others, because I just realized an inherent flaw in my Colors equation. If Instances = 1, then you will have an undefined (divide by 0) situation. For your benefit, I have left it intact for this article -- you can try it yourself and try to figure out a solution, or you can just make these small changes:

In the Wait1 subroutine, replace the "if name = "OK" then" section with the following. This now checks the value of Instances and, if needed, corrects it to avoid a divide by zero error.

if name = "OK" then
 do
  o = CwGetRegion(text2effecteffect)
  Instances = CWGetProperty(o,"Caption")
          if Instances = 1 then
           do
            call CwMsg "Shadow instances must be at least 2.  I will correct."
                    Instances = 2
               end
          LEAVE
     end

Up in the original "do Num=1 to Instances" section, change the line "Colors = (255 % (Instances - 1))" to this:

Colors = 255 % Instances

Now we have both justified the use of 3DShadow.cwx rather than regshad.cwx (by forcing the use of at least two shadow instances rather than just one which was already possible with much less code) but we have also avoided a potential disaster in an automated task. My own future releases of PGPro Scripts will be wise to this sort of problem.

Before moving onto the final section of code, I would like to explain all the CwDeleteObject() lines above. After we create the user input screen, laying a full-size white "board" over any previous work, we need to get rid of everything from that input screen so it doesn't get in the way of additional functions and doesn't ruin your otherwise pretty picture.

Wait2:

 /* Begin loop to wait for button selection */
 if RxFuncQuery('SysLoadFuncs') then
  do
   call RxFuncAdd 'SysLoadFuncs', 'REXXUTIL', 'SysLoadFuncs'
   call SysLoadFuncs
  end

 do forever
  /* sleep */
  rc = SysSleep 5

  /* see what (if anything) is selected */
  obj = CWGetSelectedObject()

  /* nothing selected?  Continue */
  if \CwIsHandleValid(obj) then ITERATE

  /* Something selected.  What is it? */
  name = CWGetName(obj)

  /* if it is a button then go on */
          if name = "OK" then
               do
        o = CwGetRegion(text2effecteffect)
        Instances = CWGetProperty(o,"Caption")
                    LEAVE
               end

  if name = "UPLEFT" then
   do
                    XMod = -1
                    YMod = 1
            LEAVE
   end

  if name = "UPRIGHT" then
   do
                    XMod = 1
                    YMod = 1
            LEAVE
   end

  if name = "DOWNLEFT" then
   do
                    XMod = -1
                    YMod = -1
            LEAVE
   end

  if name = "DOWNRIGHT" then
   do
                    XMod = 1
                    YMod = -1
            LEAVE
   end
 end

 /* delete all the prompting stuff before continuing */
 rc = CwDeleteObject(text1effect)
 rc = CwDeleteObject(text2effect)
 rc = CwDeleteObject(text3effect)
 rc = CwDeleteObject(text4effect)
 rc = CwDeleteObject(instructeffect)
     rc = CwDeleteObject(boxeffect)
return

At last we have reached the end of the script! It really isn't all that lengthy, but my descriptions may have gone on a bit too long for some of your tastes. Anyway, you can see that Wait2 is almost identical to Wait1 with the exception of handling a different series of input buttons and then deleting them after the user makes a selection for the shadowing direction. Depending on the 'button' selected, We have to set the YMod and XMod variables for use in the "do Num=1 to Instances" routine.

You may remember "Xcenter = Xcenter + (XMod * Move)" and "Ycenter = Ycenter + (YMod * Move)" from that other routine. Since the value of Move is always a positive value (based on the size of the project in pixels) we must use YMod and XMod as either +1 or -1 to shift each shadow instance either up or down, left or right. It would also be possible to move in only one direction, for instance YMod = 1 and XMod = 0 would allow shadowing straight up with no left/right movement.

The Closing Bell

The best way I've found to see how any particular part of a REXX script works is to start modifying it a piece at a time. Take something small and trivial like the value of Move or of XMod and YMod and change them. Notice how it changes other variables which call upon those seemingly insignificant variables. I would never claim to be a master at REXX programming, and in fact REXX still intimidates me, but scripting for Photo>Graphics is pretty easy. There are a number of sample scripts provided on the CD and you now have another to work with. I highly recommend reading through the REXX scripting reference included with Photo>Graphics. It's neatly organized and contains almost all the information you need for basic to intermediate scripting of that program. The REXX reference included with OS/2 Warp is also full of good information, but some of that gets pretty advanced -- far more so than you are likely to need in P>G Pro, but you never really know until you start writing your own scripts.

I would like to thank Glassman Glassman (glassman_ru@geocities.com) for showing me how to properly recurse through group objects without crashing the script. :) You can find his website of Photo>Graphics scripts and objects at http://www.geocities.com/SiliconValley/Vista/7567/graphics/english/index.html. The latest release of my own PGPro Scripts can be found on my "The 13th Floor" website at http://www.tstonramp.com/~freiheit/pgpro.shtml. At the time of this writing, version 2.0 was the latest, containing a major update to 3DShadow.cwx and an entirely new script for easily creating basic foldable greeting cards using some user input. However, having found that divide by zero bug while writing this article, you can expect to see version 2.1 or maybe version 3.0 (with a new third script) on my site. TrueSpectra created a wonderful product. It's a shame they pulled it from the market in favor of a very expensive (about US$995 I believe) server-side variation which runs on something other than OS/2.


Features
editor@os2voice.org
[Previous Page ] [ Index] [Next Page
]
VOICE Home Page: http://www.os2voice.org