VOICE Home Page: http://www.os2voice.org |
[Previous Page] [Next Page] [Features Index] |
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.
/* 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 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.