The amazing Toon-o-Matic, take 2

Yes, this is the home page of that astounding drawing software, the amazing Toon-o-matic. Largely vaporware, the Toon-o-Matic takes a description of a cartoon, style information, character descriptions and plans, chops, dices, puts it all into the microwave for five minutes (stirring occasionally) and then -- draws a cartoon.

This is the second run from whole cloth. This is the Toon-o-Matic Take 2, and the cartoon series it produces shall heretofore be known as Toonbots 2.0. The old Toon-o-Matic is here. Development on that ran from October 21, 2000 until April 28, 2001, at which point development stalled due to, well, a lot of things, ranging from my son's illness to my business's failure to my country's insanity. Toonbots staggered along until July of 2005, with a sort of fractal hiatus pattern, but essentially it was going nowhere, and that was largely because my beloved baroque script was already completely unmanageable and thus less fun to work with. Impossible, really, in the time I had at my disposal.

I'm not sure why I've suddenly got the urge to revisit the Toon-o-Matic, but it appears that the Muse is back. I ain't complainin'.

With Take 2, as with the original effort, my objective is not to produce quality software. Instead, my objective is to take as little time from my family as possible and still support a little technical self-development and creative self-expression, in the form of both weird-ass software and weird-ass cartoon humor. To reduce the overall complexity, however, it appears that it will be necessary to produce at least a minimal level of software quality. So even though quality is not my objective, it appears that it is once again raising its ugly head no matter what my opinion may be.

Roughly, the timeline of development of the Toon-o-matic has been:
October 21, 2000Panel layout
October 28, 2000Caption layout
up to Nov 10 or soVarious panel and caption features
November 12, 2000Rudimentary character sizing and placement
December 2000A first stab at variant structures for characters
January 1, 2001The first drawing commands, definition of named points for reference, etc.
April 2001Conversion of the original much more baroque program into a single script which outputs SVG code for ImageMagick to convert all in one fell swoop. You don't want to know how it used to be.
April 28, 2001Rework of text handling to use XML font summaries generated by my new tool ttfx. Captions again!
 
October, 2006 - January, 2007Take 2: total rewrite (nearly) from scratch.

The new structure is a different beast. Each comic will be built using a Makefile, and the tools it uses in its self-construction will be diverse and as small and understandable as possible. This should allow me to express some cartoons as templates to be randomized, to include cartoons in other cartoons, and to generalize the very concept of what a cartoon really is. There's a lot of ground I'd like to cover, but the steps to get there have to be baby steps.

At any rate, the top-level script in play here is "toon_setup.pl". All this does is to read the original cartoon definition file (by default in cartoon.xml) and set up the build directory, including writing the Makefile and initial setup files. It then invokes make to perform the actual build. The tools used by the build process reside in the Toon-o-Matic home directory, and there will probably also be a module of some description. The first run will build dynamic Makefiles to take care of other stuff.... It could get ugly!

Reverse compatibility is not an issue. All previous scripts will probably break. I don't care; I made some weird decisions during that coding which, in retrospect, I didn't much care for. Now I will make other, but at least different, mistakes. And hopefully the new start will result in some fresh insights (that's the way everything else works.)

Whereas the original Toon-o-Matic ran as a single monolithic script, Take 2 will run in itty-bitty pieces. Our original setup is the first step; it takes the original XML cartoon specification and sets up a build directory for the cartoon, then runs "make" in that directory. Simple as pie. The actual work is then done using separate small tools which will be defined in later steps. Let's first document that setup, then we'll go from there.

Definition of toon_setup.pl:
 
use Workflow::wftk::XML;

$toon = $ARGV[0];
$toon = 'cartoon.xml' unless $toon;


open IN, $toon or die "Can't open $toon for reading";
$input = xml_read (*IN);
close IN;

$tag = xml_attrval ($input, "tag");
die "No tag specified in cartoon definition" unless $tag;
$dir = $tag; # TODO: perhaps something more flexible.

chomp($date = `date`);
chomp($cwd = `pwd`);

system "rm -rf $dir";
mkdir $dir or die "Can't make directory $dir";
system "cp $toon $dir/cartoon.xml";

$background = xml_attrval ($input, "background");
if ($background == '') {
   $panel_steps = <<EOF;
panels.xml: instance.xml
	perl $cwd/build_panels.pl instance.xml > new-panels.xml
	perl $cwd/synch_up.pl panels.xml new-panels.xml

EOF
} else {
   system "cp $background $dir/background.gif";
   $panel_steps = <<EOF;
panels.xml: instance.xml background.gif.info
	perl $cwd/build_panels.pl instance.xml > new-panels.xml
	perl $cwd/synch_up.pl panels.xml new-panels.xml

background.gif: cartoon.xml
	perl retrieve_background.pl instance.xml

background.gif.info: background.gif
	identify -format \@$cwd/identify_format.txt background.gif > background.gif.info

EOF
}

open MAKEFILE, ">$dir/Makefile";
print MAKEFILE <<"END_MAKEFILE";
# Makefile generated $date by Toon-o-Matic t2
# Contains no serviceable parts.  Void where prohibited.
TAG=$tag;
TOMDIR=$cwd;

all: $dir.gif

cartoon.svg: panels.svg
	perl $cwd/merge_svg.pl panels.svg panel-*.svg > new-cartoon.svg
	perl $cwd/synch_up.pl cartoon.svg new-cartoon.svg

$dir.gif: cartoon.svg
	convert cartoon.svg $dir.gif
	cp $dir.gif ../../vivtek/pages/toonbots/current_test.gif

panels.svg: panels.xml Makefile.panels
	perl $cwd/draw_panels.pl panels.xml > new-panels.svg
	perl $cwd/synch_up.pl panels.svg new-panels.svg

$panel_steps
instance.xml: cartoon.xml
	perl $cwd/instantiate.pl cartoon.xml > new-instance.xml
	perl $cwd/synch_up.pl instance.xml new-instance.xml

Makefile.panels: panels.xml
	perl $cwd/build_panel_make.pl $cwd panels.xml > Makefile.panels
	make -f Makefile.panels

END_MAKEFILE
close MAKEFILE;

chdir $dir;
system "make";

exit;
That script runs first and sets up the toon's working directory. That directory initially contains the definition XML, and the master Makefile for the toon. Now, let's review what the Makefile will actually do. That is, what are the precise steps in generating a Toon-o-Matic cartoon? Note that some steps will actually vary from build to build, if there are variable definition points. For instance, we can envision a Perl script to take a generalized caption specification and generate a specific caption from it; in that case, that caption might be different on each run. Similarly, we can well imagine a Perl script to instantiate an entire toon from a template of some sort. (We can't well imagine the precise details, alas.)

So in general, the Toon-o-Matic does the following: (I think):
  • If there is any preprocessing of the cartoon, do it. We want a final instantiation before starting to draw. Preprocessing may involve randomization, retrieval, inclusion of other files, or any arbitrary processing we might think of later. Preprocessing is done by the instantiate.pl script defined here.
  • Scan the cartoon instantiation and generate a panel structure from it. If there's a background for the entire toon, add a retrieval step.
    • Generate the drawing commands for the panel boxes and backgrounds (build_panels.pl).
    • Generate a Makefile which will build the elements to be placed on each individual panel (build_panel_make.pl)
  • Scan the cartoon instantiation for a list of all characters, and a list of aspects for each character.
    • Retrieve any graphical elements to be used to build the characters (e.g. the image files for iconic characters)
    • Generate a Makefile which will build each aspect used in the toon. Each goes into a separate SVG stub.
  • Scan the cartoon instantiation again for a list of all text pieces (captions, speech balloons, and sound-effect-like texts).
    • Place each into a text file.
    • Generate a Makefile which will build each text piece used in the toon; each goes into a separate SVG stub.
  • Finally, the overall Makefile will regain control and bind all the SVG built into one SVG file (cartoon.svg), which is then converted into a graphic by calling ImageMagick's convert command. A generalization of this will produce HTML and an arbitrary number of graphics; those smaller graphical components could then, for instance, have click maps or be animations.
  • Perform any publishing step required by the superordinate system.
And then we get down and do our stuff. The panel module does panel positioning, and the layout module does the rest of the cartoon (character placement, drawing, etc.) Hmm. A third module I will call the drawing module; that takes the decorated cartoon XML and writes an SVG script for ImageMagick to work from.
 
See The layout module
See Drawing (by writing SVG)


Instantiating a toon.
Instantiation of a cartoon template is something that could arguably be called the entire purpose of a "Toon-o-Matic". But since I haven't done anything at all with it besides to acknowledge the bare fact of the existence of the notion, I'm afraid this section is pretty bleak.
Definition of instantiate.pl:
 
use Workflow::wftk::XML;

$toon = $ARGV[0];
$toon = 'cartoon.xml' unless $toon;

open IN, $toon or die "Can't open $toon for reading";
$input = xml_read (*IN);
close IN;

## TODO: processing to, y'know, instantiate.  This will massage the input XML in place.

print xml_string ($input);
print "\n";


Deciding whether a file has changed or not after processing
There are a number of cases where a script is invoked to process some file or another. If we blindly let its output overwrite the already existing version of the output, there's no way for make to know if the actual contents of that file were actually changed. The result is that everything will always run, even if it's not needed.

The solution is simple: let the processing step write the file to a buffer, then compare the buffer to the existing file. If the "diff" program then shows that the two are identical, delete the new one. Otherwise, use it to update the existing file. Then make can go on its merry way without doing extra work.
Definition of synch_up.pl:
 
$existing = $ARGV[0];
$new = $ARGV[1];

unless (-e $existing) {
  system "mv $new $existing";
  exit;
}

$diff = `diff $existing $new`;
if ($diff eq '') {
   print "No change to $existing.\n";
   unlink $new;
} else {
   system "mv $new existing";
}


Merging separate SVG command files into a single one.
Merging SVG, in our world, is easy: the first file is taken as the top-level element, and all other files supplied are simply appended to it, the assumption being that they must be group "g" elements. So this is a very simple little utility.
Definition of merge_svg.pl:
 
use Workflow::wftk::XML;

$top = shift @ARGV;
open IN, $top or die "Cannot open $top for reading";
$svg = xml_read (*IN);
close IN;

foreach $file (@ARGV) {
   open IN, $file or die "Cannot open $file for reading";
   $f = xml_read (*IN);
   close IN;

   xml_append ($svg, $f);
   xml_append ($svg, xml_createtext ("\n"));
}

print xml_string ($svg) . "\n";


The panel module
So here's how we lay out and draw our panels. In the original Toon-o-Matic, this was a function called during monolithic processing; in Take 2, it is a standalone script. Actually, there probably won't be much of a difference.

As I've explained, this module is intended to be baroque and interesting -- that may mean it will be cantankerous and hard to maintain, but as long as it draws cartoons when I tell it to, that's good enough for me. I'm showing you the code just for kicks and in case you find it interesting.

Here are a few panel layouts and the graphics they produce when laid out on a small background. Note that some of them look bluish; that's because the little background is a shrunken Boxjam-blue bitmap but in conversion to GIF the blue turned into the background color and I don't feel like post-processing. Sue me. They look good enough for illustrations.
<cartoon background="littlebkgd.bmp">
<panel/>
<panel>
 <panel/>
 <panel/>
</panel>
<panel/>
<panel/>
</cartoon>
<cartoon background="littlebkgd.bmp" rowformat="2-2">
<panel/>
<panel/>
<panel/>
<panel/>
</cartoon>
<cartoon background="littlebkgd.bmp" rowformat="2-1">
<panel>
 <panel>
  <panel/>
  <panel/>
 </panel>
 <panel/>
</panel>
<panel/>
<panel/>
<panel/>
<panel/>
</cartoon>
In case you're wondering, this presentation is a simple form of literate programming, whereby code and documentation are generated from the same source document. This version is rather straightforward, as I'm not inserting code blocks from other pages, just running down the list. If you find this easy to deal with, you might want to
read more. My open-source work is all done in a literate style. (Helps me find problems when I need to fix them.)

The first thing we do is load our cartoon. It's assumed to be in cartoon.xml in the main cartooning directory. This all is supposed to run in one directory, by the way, for ease of planning.
Definition of build_panels.pl:
 
use Workflow::wftk::XML;

$toon = $ARGV[0];
$toon = 'instance.xml' unless $toon;
open IN, $toon or die "Can't open $toon for reading";
$cartoon = xml_read (*IN);
close IN;
$panel_structure = xml_create ("cartoon");
The cartoon XML notes the background image we'll be using, so let's make sure it exists. (October 31, 2000) - but if we're not using a background, but instead we're using a solid-color generated background, then do that now.
 
if (xml_attrval ($cartoon, 'background') ne '') {
   $background = 'background.gif';
   die "Can't find background $background" if (!-e $background);
} else {
   $background = "null:";
}
xml_set ($panel_structure, 'background', $background);
(November 4, 2000)
If we specify a gradient for the background, this is the place to take care of it. Otherwise, if the background ended up as null: let's default to a white background.
 
if (xml_attrval ($cartoon, 'gradient') ne '') {
   $background = "gradient:" . xml_attrval ($cartoon, 'gradient');
   xml_set ($panel_structure, 'background', $background);
} elsif ($background eq 'null:') {
   xml_set ($panel_structure, 'color', xml_attrval ($cartoon, 'color') eq '' ? 'white' : xml_attrval ($cartoon, 'color'));
}
This phase used to modify cartoon.xml in place. Now it copies panels into the (separate) panel structure. What it does in either case is to decorate the cartoon structure with explicit specifications for the cartoon. Later some of these will come from the style library; for the time being, they're hard-coded. Note that any of these values may be overridden in the original cartoon.xml. After this phase of processing, we'll write the resulting XML to panels.xml. (Or actually, just to stdout; the makefile will direct it to the right place.)
 
xml_set ($panel_structure, 'linestyle', xml_attrval ($cartoon, 'linestyle') eq '' ? 'simple' : xml_attrval ($cartoon, 'linestyle'));
xml_set ($panel_structure, 'rowdir',    xml_attrval ($cartoon, 'rowdir')    eq '' ? 'horiz'  : xml_attrval ($cartoon, 'rowdir'));
xml_set ($panel_structure, 'rowformat', xml_attrval ($cartoon, 'rowformat') eq '' ? '1'      : xml_attrval ($cartoon, 'rowformat'));
xml_set ($panel_structure, 'border',    xml_attrval ($cartoon, 'border')    eq '' ? '1'      : xml_attrval ($cartoon, 'border'));
xml_set ($panel_structure, 'gutter',    xml_attrval ($cartoon, 'gutter')    eq '' ? '7'      : xml_attrval ($cartoon, 'gutter'));
Next step is to use the Image Magick identify program to glean the size of the background (if the background is an image). I do that in a function far below, but the results get used here.
 
xml_set ($panel_structure, 'panel-x', '0');
xml_set ($panel_structure, 'panel-y', '0');
if (xml_attrval ($cartoon, 'background') ne '') {
   open IN, 'background.gif.info';
   $info = xml_read (*IN);
   close IN;
   xml_set ($panel_structure, 'panel-w', xmlobj_get ($info, '', 'something'));  # TODO: fix this -- haven't used it in six years.
   xml_set ($panel_structure, 'panel-h', xmlobj_get ($info, '', 'something'));
}
(October 31, 2000)
If the height and width are specified explicitly, then we'll change panel-w and panel-h to match, and we'll also include a -size directive in the background specifier for convert to work with.
 
if (xml_attrval ($cartoon, 'height') ne '') {
   $height = xml_attrval ($cartoon, 'height');
   xml_set ($panel_structure, 'panel-h', $height);
} else {
   $height = 0;
   xml_set ($panel_structure, 'panel-h', '?');
}
if (xml_attrval ($cartoon, 'width') ne '') {
   $width = xml_attrval ($cartoon, 'width');
   xml_set ($panel_structure, 'panel-w', $width);
} else {
   $width=0;
   xml_set ($panel_structure, 'panel-w', '?');
}
Now, we scan the panel structure of the XML. As we do, we also mark each and every panel with its location and size. The main cartoon wants to lay out its panels horizontally, and subpanels of panels alternate default layout directions (so that unless I tell the code otherwise, a subpanels of a top-level panel will be arranged vertically.) I can use the "rowformat" attribute to tell the code how many panels should go in each row.

There's a lot to this, and I'm not going to document each piece. Live with it.

Take 2: this is building a separate panel structure, and determining a unique identifier for each panel. Then it extracts the non-panel elements for each panel into the panel structure, and writes the definition of each panel out to a separate panel definition XML file. I'm not 100% sure I need to do that, but it sounds cool. It's a discombobulator. This is the panel discombobulator. See how cool that sounds?

November 5, 2006:
It's always been a little too rigid that I have to declare the final size of the whole cartoon. So now, if I don't know that in advance, I can specify the panel height or width instead, which will apply to the top-level panels.
 
$panel_number = 0;
@panel_list = ();

# OK, scan for panels.  Just to make the whole thing more baroque, I'm putting the recursive subroutine
# right in the middle of our script; more top-level processing goes on below this.  Isn't that cool?
panel_scan ($cartoon, $panel_structure);
sub panel_scan {
   my $parent = shift;
   my $outparent = shift;
   my @panels = ();

   foreach (xml_elements($parent)) {
      next if $$_{name} ne 'panel';
      push @panels, $_;
      push @panel_list, $_;
   }

   if (!@panels) { # There is no panel structure in this panel -- thus it is a content panel.  Write it out.
                   # (Or rather: promise to write it out later.)
      $panel_xml{'panel-' . xml_attrval ($outparent, 'tag') . ".xml"} = $parent;

      return;
   }

   # Find actual row structure.
   my @rowformat = split /-/, xml_attrval ($parent, 'rowformat');
   my @actual = ($#panels + 1);
   if (xml_attrval ($parent, 'rowformat')) {
      my $rowoffset = 0;
      my $actual_offset = 0;
      $rowformat[$rowoffset] = 1 if !$rowformat[$rowoffset];
      while ($actual[$actual_offset] > $rowformat[$rowoffset]) {
         push @actual, $actual[$actual_offset] - $rowformat[$rowoffset];
         $actual[$actual_offset] = $rowformat[$rowoffset];
         $actual_offset++;
         $rowoffset++;
         $rowoffset = 0 if $rowoffset > $#rowformat;
         $rowformat[$rowoffset] = 1 if !$rowformat[$rowoffset];
      }
   }

   # Stash it for debugging and all-around baroqueness.
   xml_set ($outparent, 'actual-rowformat', join ('-', @actual));

   # Now parcel out horizontal and vertical space based on the actual row structure.
   my ($row_coord, $row_width, $col_coord, $col_width);
   if (xml_attrval ($parent, 'rowdir') =~ /^v/) {
      $row_coord = 'panel-x';
      $row_width = 'panel-w';
      $col_coord = 'panel-y';
      $col_width = 'panel-h';
   } else {
      $row_coord = 'panel-y';
      $row_width = 'panel-h';
      $col_coord = 'panel-x';
      $col_width = 'panel-w';
   }

   my $rowpos = xml_attrval ($outparent, $row_coord) + xml_attrval ($outparent, 'border');

   if (xml_attrval ($outparent, $row_width) eq '?') {
      my $total = xml_attrval ($parent, $row_width) * @actual;
      $total += 2 * xml_attrval ($outparent, 'border');
      $total += xml_attrval ($outparent, 'gutter') * (@actual - 1);
      xml_set ($outparent, $row_width, $total);
   }
   my $rowtotal = xml_attrval ($outparent, $row_width) - 2 * xml_attrval ($outparent, 'border') - 1
                                                    - (xml_attrval ($outparent, 'gutter') * (@actual - 1));
   my $rowportion = $rowtotal / @actual;
   my $row_len;
   foreach $row_len (@actual) {
      next if !$row_len;
      my $colpos = xml_attrval ($outparent, $col_coord) + xml_attrval ($outparent, 'border');
      my $coltotal = xml_attrval ($outparent, $col_width) - 2 * xml_attrval ($outparent, 'border') - 1
                                                       - (xml_attrval ($outparent, 'gutter') * ($row_len - 1));
      my $colportion = $coltotal / $row_len;
      for (my $i=0; $i < $row_len; $i++) { # Step along the row...
         $r = $rowpos;
         $c = $colpos;
         $rowpos =~ s/\..*//; # Integer portion only -- IM doesn't render lines well if they span pixel boundaries.
         $colpos =~ s/\..*//;

         $r -= $rowpos;
         $c -= $colpos;

         my $panel = shift @panels;
         my $outpanel = xml_create ("panel");
         $rwidth = $rowportion + 1;
         $cwidth = $colportion + 1;
         $rwidth =~ s/\..*//;
         $cwidth =~ s/\..*//;

         if (xml_attrval ($outparent, 'rowdir') eq 'horiz') {
            $max = xml_attrval ($outparent, $row_coord) + xml_attrval ($outparent, $row_width);
            if ($rowpos + $rwidth > $max) { $rwidth = $max - $rowpos; }
            $max = xml_attrval ($outparent, $col_coord) + xml_attrval ($outparent, $col_width);
            if ($colpos + $cwidth > $max) { $cwidth = $max - $colpos; }
         }
         
         xml_set ($outpanel, $row_coord, $rowpos);
         xml_set ($outpanel, $col_coord, $colpos);
         xml_set ($outpanel, $row_width, $rwidth);
         xml_set ($outpanel, $col_width, $cwidth);
         xml_set ($outpanel, 'linestyle', xml_attrval ($panel, 'linestyle'));
         xml_set ($outpanel, 'linestyle', xml_attrval ($outparent, 'linestyle')) if xml_attrval ($outpanel, 'linestyle') eq '';
         xml_set ($outpanel, 'arrow', xml_attrval ($panel, 'arrow'));  # Added 2006-10-25.
         xml_set ($outpanel, 'fill',  xml_attrval ($panel, 'fill'));   # Added 2006-10-25.
         xml_set ($outpanel, 'svg-transform', xml_attrval ($panel, 'svg-transform'));   # Added 2006-10-29.

         $panel_number++;
         if (xml_attrval ($outpanel, 'name') eq '') {
            xml_set ($outpanel, 'name', "panel$panel_number");
         }
         xml_set ($outpanel, 'tag', $panel_number);

         xml_set ($outpanel, 'rowdir', xml_attrval ($panel, 'rowdir'));
         if (!xml_attrval ($outpanel, 'rowdir')) {
            if (xml_attrval ($outparent, 'rowdir') =~ /^v/) {
               xml_set ($outpanel, 'rowdir', 'horiz');
            } else {
               xml_set ($outpanel, 'rowdir', 'vert');
            }
         }
         xml_set ($outpanel, 'gutter', xml_attrval ($panel, 'gutter'));
         xml_set ($outpanel, 'gutter', xml_attrval ($outparent, 'gutter')) if !xml_attrval ($outpanel, 'gutter');

         xml_append_pretty ($outparent, $outpanel);

         my $panel_list_length = $#panel_list;
         panel_scan ($panel, $outpanel);
         if ($panel_list_length != $#panel_list) {
            # Using a side effect is baroque, isn't it?
            xml_set ($outpanel, 'linestyle', 'none');
         }

         $colpos += $c + $colportion + xml_attrval ($outparent, 'gutter');
      }
      $rowpos += $r + $rowportion + xml_attrval ($outparent, 'gutter');
   }
}

Now let's write our panels out (after correcting any sizes which need to be corrected).
 
foreach $file (keys (%panel_xml)) {
   # TODO: correct panel XML before writing.
   open OUT, ">$file";
   print OUT xml_string ($panel_xml{$file}) . "\n";
   close OUT;  # Sale on aisle 7.
}
(November 4, 2000) If any panel is decorated with a gradient, then we take care of that now. We treat a color specification for the panel as a gradient for the purposes of drawing. (A blue panel is thus a gradient from blue to blue.) Gradients in ImageMagick are always top to bottom; an interesting extension would be to be able to specify gradients in any direction, but that would require rotating, cropping, and pasting. I won't get into it today.

This first step prepares the background image for the panel -- note that this means that technically we're going to be able to use external image for this at some point. Again, since we're not addressing sizing, I won't get into it today. But this would be the place to do it.

TODO: this is broken in Take 2. I never used it anyway, not since 2000 or so.
 
foreach $panel (@panel_list) {
   # Background color or gradient.
   $gradient = '';
   if (xml_attrval ($panel, 'color') ne '') {
      $gradient = xml_attrval ($panel, 'color') . '-' . xml_attrval ($panel, 'color');
   }
   if (xml_attrval ($panel, 'gradient') ne '') {
      $gradient = xml_attrval ($panel, 'gradient');
   }

   next if $gradient eq '';

   print "Creating background image for " . xml_attrval ($panel, 'name') . "\n";
   xml_set ($panel, 'background', xml_attrval ($panel, 'name') . "-bg.gif");
   system "convert -size " . xml_attrval ($panel, 'panel-w') .
                       "x" . xml_attrval ($panel, 'panel-h') .
    " gradient:$gradient " . xml_attrval ($panel, 'background');
}
And finally, we write our panel structure to stdout.
 
print xml_string ($panel_structure);
print "\n";
Cool, eh? I can't wait to see what I do next.

Building the Makefile to build panels
Oh, what a tangled web we weave.

Each individual panel generates its own set of SVG commands to draw things on it. To get these steps, we want to read the list of panels and write a Makefile for it.
Definition of build_panel_make.pl:
 
use Workflow::wftk::XML;

$cwd = $ARGV[0];
$toon = $ARGV[1];
$toon = 'instance.xml' unless $toon;
open IN, $toon or die "Can't open $toon for reading";
$panels = xml_read (*IN);
close IN;

chomp($date = `date`);
OK, now that we have the panel structure, let's scan it recursively just to build a list of panels to instantiate. For each panel, we're also collecting its coordinates and dimensions now, just in case. The SVG commands for each panel will be drawn relative to the panel (an SVG transformation will move it into place during the merge step), but then enclosed in a group for translation to the panel's position. If there are other transformations for the panel, such as a rotation or something, those could also go onto the group.
 
scan_panels ($panels);

sub scan_panels {
   my $panel = shift;
   foreach $elem (xml_elements ($panel)) {
      next unless ($$elem{name} eq 'panel');

      $x{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-x');
      $y{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-y');
      $w{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-w');
      $h{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-h');

      scan_panels ($elem);
   }
}

@panel_list = sort { $a <=> $b } keys (%x);
So. Now we have a list of panels, and for each we know the position and size. The position will largely be ignored, but the size is important -- we'll need it to determine a lot of relative positions of things on the panel.

Now what we'll do is to run down the list of panels and read each, compiling a list of things to place on the panel. Those things will include: text (captions and word balloons), characters (arbitrary drawing object), effects, etc. The sky's the limit, really. In Take 1, we had only captions and characters, the characters largely confined to the use of icons. In Take 2, it would in principle be possible to do a lot more. We'll see how far I actually get.

At any rate, each placeable item in the entire cartoon is identified in this step. From that list of items, we build a series of Makefile steps to build them. This area will doubtlessly be very difficult until I figure out what the hell I'm doing, at which point I will surely need to rewrite it entirely a few times.

November 1, 2006:
So it turns out that the ImageMagick SVG renderer still has problems with rotations. I ran into this a few years back when experimenting with rotations, and honestly, so much else has been vastly improved with ImageMagick since then that I really expected this to be fixed, too. But it's not. So our "plain-vanilla" approach to rotated text (which includes text going vertically, but also text with arbitrary other orientations as well) is not going to work. That approach would be this: first generate a simple SVG and render it, containing only the text. Trim that bitmap, and measure its actual extent. This allows us to get the size of the text back into Perl. In a second step, use that same text SVG and a rectangle, with suitable offsets, and rotate it with a transform property on an enclosing 'g' element. That should work, but it's buggy -- some weird translation offsets come into it, and it's not even consistent, so that the position of an element may change if there are changes elsewhere in the SVG. Bad juju.

So instead I'm choosing a different (and grungier) route -- I'm going to do the same first step and get the proper extent of the text, but then if rotation is required (if there is a "rotate" or "direction" tag on the caption) then an intermediate step is needed -- that step will render the full, boxed caption, rotate it with a separate convert call, trim and measure that graphic, and then finally generate SVG to pull that graphic in explicitly, with a clipping path, to place it on the final drawing.

There's an advantage to this process -- it means that it will be easy to substitute other tools to generate text (say, Postscript) when and if that feature becomes my current obsession.

Februember 7, 2007:
A note on scenes: Each panel belongs to a scene, and each scene defines the things present and where they are, etc. A scene has a timeline, so that as characters join the scene or leave it, new steps arise in the timeline. Each panel maps to a step, but a step may map to several panels, if the composition of the scene doesn't change.
 
$makes = '';
$panels_list = '';
$cur_scene = 'default';

@characters_leaving = ();

foreach $panel (@panel_list) {
   next unless -e "panel-$panel.xml";
   open IN, "panel-$panel.xml";
   $pxml = xml_read (*IN);
   close IN;

   @svgs = ();
   $caption_count = 0;
   $draw_count = 0;

   # Track the current scene, to determine groups of characters, later background etc.
   $cur_scene = xml_attrval ($panel, 'scene') if xml_attrval ($panel, 'scene');
   if (not xml_is_element ($scenes{$cur_scene})) {
      $scenes{$cur_scene} = xml_parse ("<scene id=\"$cur_scene\"/>");
      $scene_step{$cur_scene} = 0;
   }
   $scene = $scenes{$cur_scene};
   $scene_tag = "$cur_scene-$scene_step{$cur_scene}";
   $scene_step = $scene_steps{$scene_tag};
   if (xml_attrval ($scene, 'panels')) {
      xml_set ($scene, 'panels', xml_attrval ($scene, 'panels') . "-$panel");
   } else {
      xml_set ($scene, 'panels', $panel);
   }

   if (scalar @characters_leaving) {
      $old_scene_step = $scene_step;
      $scene_step{$cur_scene} += 1;
      $scene_tag = "$cur_scene-$scene_step{$cur_scene}";
      $scene_steps{$scene_tag} = xml_parse ("<frame id=\"$scene_step{$cur_scene}\" tag=\"$scene_tag\"/>");
      $scene_step = $scene_steps{$scene_tag};
      xml_append_pretty ($scene, $scene_step);
      $last_scene_step = $panel;

      # Remove all characters from current scene step, or rather: copy all characters except those leaving.
      foreach $elem (xml_elements($old_scene_step)) {
         unless (grep {print "$_ =?\n"; $_ eq xml_attrval($elem, "name")} @characters_leaving) {
            xml_append_pretty ($scene_step, xml_copy ($elem));
         }
      }
      @characters_leaving = ();
   }

   foreach $elem (xml_elements ($pxml)) {
      if (xml_name ($elem) eq 'character') {
         # 1. New scene step any time a character is referenced, but only once a panel.
         #    The scene has a timeline; each panel is mapped to a subscene.
         if ($last_scene_step ne $panel) {
            $old_scene_step = $scene_step;
            $scene_step{$cur_scene} += 1;
            $scene_tag = "$cur_scene-$scene_step{$cur_scene}";
            $scene_steps{$scene_tag} = xml_parse ("<frame id=\"$scene_step{$cur_scene}\" tag=\"$scene_tag\"/>");
            $scene_step = $scene_steps{$scene_tag};
            xml_append_pretty ($scene, $scene_step);
            $last_scene_step = $panel;

            foreach $elem (xml_elements($old_scene_step)) {
               xml_append_pretty ($scene_step, xml_copy($elem));
            }
         }

         # 2. Add character to current scene step.  (Note: we may need to merge an existing char/panel defn and the current defn.)
         $character = xml_copy ($elem);
         xml_set ($character, 'tag', $scene_tag);
         xml_append_pretty ($scene_step, $character);

         $tag = "character-" . xml_attrval ($character, 'name') . "-$scene_tag";
         open C, ">$tag.xml";
         xml_write (*C, $character); print C "\n";
         close C;

         $make{$tag} = <<"END_MAKE";
$tag.svg: $tag.xml
	perl $cwd/build_character.pl $cwd $w{$panel} $h{$panel} $tag.xml > draw-$tag.xml
	perl $cwd/draw.pl draw-$tag.xml $w{$panel} $h{$panel} > new-$tag.svg
	perl $cwd/synch_up.pl $tag.svg new-$tag.svg

END_MAKE

         push @svgs, "$tag.svg";

         # 3. Remove character from scene if action specifies it.
         if (xml_attrval ($elem, "action") eq "leaves") {
            push @characters_leaving, xml_attrval ($elem, "name");
         }
      } elsif (xml_name ($elem) eq 'scene') {
         # Scene-specific commands.
      } elsif (xml_name ($elem) eq 'caption') {
         $caption_count += 1;
         $tag = "caption-$panel-$caption_count";
         if (xml_attrval ($elem, 'id')) { $elem_id{xml_attrval ($elem, 'id')} = $tag; }

         $rotate = 0;
         $direction = xml_attrval ($elem, "direction");
         if ($direction eq "up") {
            $rotate = 270;
         } elsif ($direction eq "down") {
            $rotate = 90;
         } elsif ($direction eq "inverted") {
            $rotate = 180;
         } else {
            $rotate = xml_attrval ($elem, "rotate");
         }

         if ($rotate == 0) {
            $make{$tag} = <<"END_MAKE";
$tag.info: $tag.xml
	perl $cwd/draw_caption.pl $tag.xml no-info $w{$panel} $h{$panel} 0 > $tag-raw.svg
	convert -trim $tag-raw.svg $tag.gif
	identify -format \@$cwd/identify_format.txt $tag.gif > $tag.info

$tag.svg: $tag.info
	perl $cwd/draw_caption.pl $tag.xml $tag.info $w{$panel} $h{$panel} 0 > $tag.svg

END_MAKE
         } else {
            $make{$tag} = <<"END_MAKE";
$tag.info: $tag.xml
	perl $cwd/draw_caption.pl $tag.xml no-info $w{$panel} $h{$panel} 0 > $tag-raw.svg
	convert -trim $tag-raw.svg $tag-raw.gif
	identify -format \@$cwd/identify_format.txt $tag-raw.gif > $tag.info

$tag-r$rotate.info: $tag.xml $tag.info
	perl $cwd/draw_caption.pl $tag.xml $tag.info 0 0 0 > $tag-boxed.svg
	convert -trim $tag-boxed.svg $tag-boxed.gif
	convert -rotate $rotate $tag-boxed.gif $tag-r$rotate.gif
	identify -format \@$cwd/identify_format.txt $tag-r$rotate.gif > $tag-r$rotate.info

$tag.svg: $tag-r$rotate.info
	perl $cwd/draw_caption.pl $tag.xml $tag-r$rotate.info $w{$panel} $h{$panel} $rotate > $tag.svg

END_MAKE
         }

         push @svgs, "$tag.svg";

         open OUT, ">$tag.xml";
         print OUT xml_string ($elem) . "\n";
         close OUT;
      } elsif (xml_name($elem) eq 'dialog') { # Balloon!
         $dialog_count += 1;
         $tag = "dialog-$panel-$dialog_count";
         if (xml_attrval ($elem, 'id')) { $elem_id{xml_attrval ($elem, 'id')} = $tag; }

         $needs = '';
         $who = xml_attrval ($elem, 'who');
         # What drawing is the instantiation of this character in this panel?
         $whopan = "$who-$panel";
         $needs{$tag} = $character{$whopan};
         $needs .= " $character{$whopan}.svg";
         xml_set ($elem, "ref-who", "$character{$whopan}.svg");

         $make{$tag} = <<"END_MAKE";
$tag.info: $tag.xml $needs
	perl $cwd/draw_caption.pl $tag.xml no-info $w{$panel} $h{$panel} 0 > $tag-raw.svg
	convert -trim $tag-raw.svg $tag.gif
	identify -format \@$cwd/identify_format.txt $tag.gif > $tag.info

$tag.svg: $tag.info
	perl $cwd/draw_caption.pl $tag.xml $tag.info $w{$panel} $h{$panel} 0 > $tag.svg

END_MAKE

         push @svgs, "$tag.svg";
         open OUT, ">$tag.xml";
         print OUT xml_string ($elem) . "\n";
         close OUT;
      } elsif (xml_name($elem) eq 'draw') { # Raw drawing command of various description
         $draw_count += 1;
         $tag = "draw-$panel-$draw_count";
         if (xml_attrval ($elem, 'id')) { $elem_id{xml_attrval ($elem, 'id')} = $tag; }
         $character{xml_attrval ($elem, 'character') . "-$panel"} = $tag;

         $needs = '';
         $area = xml_attrval ($elem, 'area');

         if ($area =~ /^!/) { #Dependency
            ($ref, $spec) = split /: */, $area;
            $ref =~ s/^!//;
            $ref =~ s/\[.*//;
            $needs{$tag} = $elem_id{$ref};
            $needs .= " $elem_id{$ref}.svg";
            xml_set ($elem, "ref-$ref", "$elem_id{$ref}.svg");
         }

         $make{$tag} = <<"END_MAKE";
$tag.svg: $tag.xml $needs
	perl $cwd/draw.pl $tag.xml $w{$panel} $h{$panel} > $tag.svg

END_MAKE
         push @svgs, "$tag.svg";
         open OUT, ">$tag.xml";
         print OUT xml_string ($elem) . "\n";
         close OUT;
      }
   }

   # At this point, pxml contains the panel definition and scene_step contains the list of characters in this panel.
   # Make sure each character is shown correctly in the panel, in its current aspect.
   $panel_changed = 0;
   foreach $character (xml_elements ($scene_step)) {
      unless (xml_search ($pxml, 'character', 'name', xml_attrval ($character, 'name'))) {
         xml_append_pretty ($pxml, xml_copy ($character));
         $panel_changed = 1;
      }
   }

   if ($panel_changed) {
      open P, ">panel-$panel.xml";
      xml_write (*P, $pxml);
      close P;
   }

   if (xml_attrval ($scene_step, 'panels')) {
      xml_set ($scene_step, 'panels', xml_attrval ($scene_step, 'panels') . "-$panel");
   } else {
      xml_set ($scene_step, 'panels', $panel);
   }

   next unless @svgs;

   $panels_list .= " panel-$panel.svg";

   $svg_makes = '';
   foreach $s (@svgs) { $svg_makes .= " $s"; }
   $makes .= <<"END_MAKE";
panel-$panel.svg: $svg_makes
	perl $cwd/build_panel_g.pl panel-$panel.xml $x{$panel} $y{$panel} > panel-$panel-g.svg
	perl $cwd/merge_svg.pl panel-$panel-g.svg $svg_makes > new-panel-$panel.svg
	perl $cwd/synch_up.pl panel-$panel.svg new-panel-$panel.svg

END_MAKE
}

# Write out the scene structures in case we need them for something else.
foreach $scene (keys (%scenes)) {
   open OUT, ">scene-$scene.xml";
   print OUT xml_string ($scenes{$scene}) . "\n";
   close OUT;
}
 
foreach $make (keys (%make)) {
   $makes .= $make{$make};
}

print <<"END_MAKEFILE";
# Panel Makefile generated $date by Toon-o-Matic t2
# Contains no serviceable parts.  Batteries not included.
# Void in NH, VT, and U.S. Minor Outlying Islands.
TOMDIR=$cwd;

all: $panels_list

$makes
END_MAKEFILE



Building top-level group element for each panel which needs it
This is not a challenge: the 'g' element simply needs to hold a translation command to move the panel drawing elements over the panel itself. If other tranformations pertain for that panel, they'll also need to be applied to this group as well (especially rotations and the like -- but there should really be a central repository for this kind of shared function. TODO: said package.)
Definition of build_panel_g.pl:
 
$panel = $ARGV[0];
print "<g transform=\"translate($ARGV[1],$ARGV[2])\"/>\n";


Converting panels.xml into panels.svg for drawing
This is actually pretty brainless. Later things are going to get much more complicated. First, we load the panel structure already built earlier.
Definition of draw_panels.pl:
 
use Workflow::wftk::XML;

$in = $ARGV[0];
$in = 'panels.xml' unless $in;
open IN, $in or die "Can't open $in for reading";
$panels = xml_read (*IN);
close IN;

$svg = xml_create ("svg");
xml_append ($svg, xml_createtext("\n"));
xml_set ($svg, "height", xml_attrval ($panels, "panel-h"));
xml_set ($svg, "width", xml_attrval ($panels, "panel-w") + 2); # Note: adding 2 because of odd clipping behavior.

if (xml_attrval ($panels, "image") ne '') {
   $cmd = xml_create ("image");
   xml_set ($cmd, "xlink:href", xml_attrval ($panels, "image"));
   xml_set ($cmd, "x", "0");
   xml_set ($cmd, "y", "0");
   xml_append ($svg, $cmd);
   xml_append ($svg, xml_createtext("\n"));
} elsif (xml_attrval ($panels, "color") ne '') {
   $cmd = xml_create ("rect");
   xml_set ($cmd, "height", xml_attrval ($panels, "panel-h"));
   xml_set ($cmd, "width", xml_attrval ($panels, "panel-w"));
   xml_set ($cmd, "style", "fill: " . xml_attrval ($panels, 'color'));
   xml_append ($svg, $cmd);
   xml_append ($svg, xml_createtext("\n"));
}
Next, we traverse the XML tree of the cartoon and draw each panel and the things on it. Simple. Except it's not. For instance, if an arrow goes from one panel onto another, the first panel has to be drawn last. So we have to sort the little buggers. That kind of stuff. So before drawing them, we first scan the structure and collect some information about the individual panels.
 
$p{'dummy'} = '';
$s{'dummy'} = '';
$x{'dummy'} = 0;
$y{'dummy'} = 0;
$w{'dummy'} = 0;
$h{'dummy'} = 0;
$on_top_of{'dummy'} = 0;
scan_panels ($panels);

sub scan_panels {
   my $panel = shift;
   foreach $elem (xml_elements ($panel)) {
      next unless ($$elem{name} eq 'panel');

      $p{xml_attrval ($elem, 'tag')} = $elem;
      $x{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-x');
      $y{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-y');
      $w{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-w');
      $h{xml_attrval ($elem, 'tag')} = xml_attrval ($elem, 'panel-h');
      $s{xml_attrval ($elem, 'tag')} = '';
      $on_top_of{xml_attrval ($elem, 'tag')} = '';

      scan_panels ($elem);
   }
}
Now we have enough information to calculate the effects of arrows from one panel to the next. So let's draw some panels. Then we'll sort them, and write the whole thing out.
 
draw_panels ($panels);

sub draw_panels {
   my $panel = shift;
   foreach $elem (xml_elements ($panel)) {
      next unless ($$elem{name} eq 'panel');

      my $tag = xml_attrval ($elem, 'tag');

      # Background image, if any.
      if (xml_attrval ($elem, 'background') ne '') {
         $cmd = xml_create ("image");
         xml_set ($cmd, "xlink:href", xml_attrval ($elem, "background"));
         xml_set ($cmd, "x", xml_attrval ($elem, 'panel-x'));
         xml_set ($cmd, "y", xml_attrval ($elem, 'panel-y'));
         xml_set ($cmd, "width", xml_attrval ($elem, 'panel-w'));
         xml_set ($cmd, "height", xml_attrval ($elem, 'panel-h'));
         xml_append ($svg, $cmd);
         xml_append ($svg, xml_createtext("\n"));
      }

      # Now the panel's polyline, if any.
      $line = ''; $fill = '';

      $style = xml_attrval ($elem, 'color');
      $fill = xml_attrval ($elem, 'fill');
      $style = "fill:$fill" if $fill ne '';
      $style = 'fill:none' if $fill eq 'none' || $fill eq '';

      if (xml_attrval ($elem, 'linestyle') ne 'none') {
         $style .= '; ' if $style ne '';

         $color = xml_attrval ($elem, 'color');
         $color = xml_attrval ($elem, 'stroke') if $color;
         $color = 'black' if $color eq '';
         $line  = xml_attrval ($elem, 'line');
         $line  = xml_attrval ($elem, 'stroke-width') if $line eq '';
         $line  = '1' if $line eq '';
         $style .= "stroke:$color; stroke-width:$line";
      }

      if ($style ne '') {
         my $arrow = xml_attrval ($elem, "arrow");
         my $x1 = xml_attrval ($elem, 'panel-x');
         my $y1 = xml_attrval ($elem, 'panel-y');
         my $w  = xml_attrval ($elem, 'panel-w');
         my $h  = xml_attrval ($elem, 'panel-h');
         my $x2 = $x1 + $w;
         my $y2 = $y1 + $h;
         if ($arrow eq 'next') {
            if (!xml_is_element ($p{$tag+1})) {
               $arrow = '';
            } else {
               $xdelta = $x{$tag+1} - $x{$tag};
               $ydelta = $y{$tag+1} - $y{$tag};
               if ($xdelta > 0 && $xdelta > $ydelta) {
                  $arrow = 'right';
               } elsif ($xdelta < 0 && $xdelta < $ydelta) {
                  $arrow = 'left';
               } elsif ($ydelta > 0) {
                  $arrow = 'bottom';
               } elsif ($ydelta < 0) {
                  $arrow = 'top';
               } else {
                  $arrow = '';
               }
            }
         }
         $cmd = xml_create ("polyline");
         $s{$tag} = $cmd;
         xml_set ($cmd, "transform", xml_attrval ($elem, 'svg-transform'));

         xml_set ($cmd, 'style', $style);
         xml_set ($cmd, 'arrow', $arrow);
         $awidth = 50;
         $alength = 50;
         if ($arrow eq 'top') {
            $midpoint = $x1 + $w/2;
            $abase1 = $midpoint - $awidth/4;
            $abase2 = $midpoint + $awidth/4;
            $atip   = $y1 - $alength;
            $aflare = $y1 - $alength/2;
            $aflank1 = $midpoint - $awidth/2;
            $aflank2 = $midpoint + $awidth/2;

            $atip_x = $midpoint;
            $atip_y = $atip;

            $points  = "$x1,$y1 $abase1,$y1 $abase1,$aflare $aflank1,$aflare $midpoint,$atip ";
            $points .= "$aflank2,$aflare $abase2,$aflare $abase2,$y1 $x2,$y1 $x2,$y2 $x1,$y2 $x1,$y1";
         } elsif ($arrow eq 'bottom') {
            $midpoint = $x1 + $w/2;
            $abase1 = $midpoint - $awidth/4;
            $abase2 = $midpoint + $awidth/4;
            $atip   = $y2 + $alength;
            $aflare = $y2 + $alength/2;
            $aflank1 = $midpoint - $awidth/2;
            $aflank2 = $midpoint + $awidth/2;

            $atip_x = $midpoint;
            $atip_y = $atip;

            $points  = "$x1,$y1 $x2,$y1 $x2,$y2 $abase2,$y2 $abase2,$aflare $aflank2,$aflare ";
            $points .= "$midpoint,$atip $aflank1,$aflare $abase1,$aflare $abase1,$y2 $x1,$y2 $x1,$y1";
         } elsif ($arrow eq 'left') {
            $midpoint = $y1 + $h/2;
            $abase1 = $midpoint - $awidth/4;
            $abase2 = $midpoint + $awidth/4;
            $atip   = $x1 - $alength;
            $aflare = $x1 - $alength/2;
            $aflank1 = $midpoint - $awidth/2;
            $aflank2 = $midpoint + $awidth/2;

            $atip_x = $atip;
            $atip_y = $midpoint;

            $points  = "$x1,$y2 $x2,$y1 $x2,$y2 $x1,$y2 $x1,$abase2 $aflare,$abase2 $aflare,$aflank2 ";
            $points .= "$atip,$midpoint $aflare,$aflank1 $aflare,$abase1 $x1,$abase1 $x1,$y1";
         } elsif ($arrow eq 'right') {
            $midpoint = $y1 + $h/2;
            $abase1 = $midpoint - $awidth/4;
            $abase2 = $midpoint + $awidth/4;
            $atip   = $x2 + $alength;
            $aflare = $x2 + $alength/2;
            $aflank1 = $midpoint - $awidth/2;
            $aflank2 = $midpoint + $awidth/2;

            $atip_x = $atip;
            $atip_y = $midpoint;

            $points  = "$x1,$y1 $x2,$y1 $x2,$abase1 $aflare,$abase1 $aflare,$aflank1 $atip,$midpoint ";
            $points .= "$aflare,$aflank2 $aflare,$abase2 $x2,$abase2 $x2,$y2 $x1,$y2 $x1,$y1";
         } else {
            $points = "$x1,$y1 $x2,$y1 $x2,$y2 $x1,$y2 $x1,$y1";

            $atip_x = 0;
            $atip_y = 0;
         }
         xml_set ($cmd, "points", $points);

         if ($atip_x != 0 && $atip_y != 0) {
            foreach $otag (keys(%p)) {
               next if $tag eq $otag;
               if ($atip_x > $x{$otag}             &&
                   $atip_x < $x{$otag} + $w{$otag} &&
                   $atip_y > $y{$otag}             &&
                   $atip_y < $y{$otag} + $h{$otag}) {
                   $on_top_of{$tag} .= " $otag ";
               }
            }
         }
      }

      draw_panels ($elem);  # Now we draw the panel's contents onto the panel.
  }
}
Now sort them and write them all to the SVG file.
 
sub on_top_of {
   return 1 if $on_top_of{$a} =~ / $b /;
   return -1 if $on_top_of{$b} =~ / $a /;
   return 0;
}
foreach $tag (sort on_top_of keys(%s)) {
   xml_append_pretty ($svg, $s{$tag}) if xml_is_element ($s{$tag});
}

print xml_string ($svg) . "\n";


Building character
To draw a character, we first take its description from the scene frame, retrieve anything needed from external references, and "build character". Then we pass the built character (a fully specified set of drawing commands) to the regular draw script.
Definition of build_character.pl:
 
use Workflow::wftk::XML;
$cwd = $ARGV[0];
$panel_width = $ARGV[1];
$panel_height = $ARGV[2];
$in = $ARGV[3];

open IN, $in or die "Can't open $in for reading";
$character = xml_read (*IN);
close IN;

if (xml_attrval ($character, "action") eq "modify") {
   $mod = $character;
   $character = xml_parse ("");
   xml_set ($character, 'name', xml_attrval ($mod, 'name'));
} else {
   $mod = xml_parse ("");
}
First, check whether the definition is present; if so, we don't need to do anything, otherwise, we retrieve it.
 
$name = xml_attrval ($character, 'name');
unless (-e "definition-$name.xml") {
   if (-e "$cwd/characters/$name.xml") {
      open IN, "$cwd/characters/$name.xml" or die "Problem opening character definition for $name";
      $defn = xml_read (*IN);
      close IN;

      $img_ct = 0;
      foreach $image (xml_search ($defn, 'draw', 'type', 'image')) {
         $ifile = xml_attrval ($image, "file");
         $ifile =~ s/;.*//;
         $ext = $ifile;
         $ext =~ s/.*\.//;
         $img_ct += 1;
         $localname = "image-$name-$img_ct.$ext";
         system "cp $cwd/characters/$ifile image-$name-$img_ct.$ext";
         system "identify -format \@$cwd/identify_format.txt image-$name-$img_ct.$ext > image-$name-$img_ct.$ext.info";
         xml_set ($image, 'file', $localname);
      }
   } else {
      $defn = xml_parse "";
   }

   open D, ">definition-$name.xml";
   xml_write (*D, $defn);
   close D;
} else {
Next, we load the definition.
 
   open IN, "definition-$name.xml" or die "Can't open definition";
   $defn = xml_read (*IN);
   close IN;
}
Now we instantiate the definition according to the spec in the scene frame. This code is copied from the old Toon-o-Matic, actually, and uses the drawing_instantiate defined there.
 
# Relative height and width of the bounding rectangle.
xml_set ($character, 'rel-h', xml_attrval ($defn, 'rel-h'));
xml_set ($character, 'rel-w', xml_attrval ($defn, 'rel-w'));

# Where images are located and whether to display the bounding box (for debugging).
xml_set ($character, 'imagebase', xml_attrval ($defn, 'imagebase'));
xml_set ($character, 'show-box', xml_attrval ($defn, 'show-box')) unless xml_attrval ($defn, 'show-box') eq '';

# Absolute height and width of the bounding rectangle *in this panel*.
xml_set ($character, 'height', xml_attrval ($character, 'rel-h') * $panel_height / 100);
xml_set ($character, 'width',  xml_attrval ($character, 'rel-w') * xml_attrval ($character, 'height') / 100);

# And use the current state of the visible character to determine from the character description what to draw.
drawing_instantiate ($defn, $character, xml_attrval ($character, "aspect"));

# Finally, modify the character if required (by action="modify")
# 01/03/01 - Check whether the character has been modified and complete modifications if necessary.
if (xml_attrval ($mod, "action") eq 'modify') {
   modify_character ($character, $mod);
}

Finally, we emit our drawing specifications.
 
print xml_string ($character);
print "\n";
Here's where some functions are defined which are used here. This is pretty old code, but it seems to work; the organization of all this is probably not what it should be, but that seems par for the course.
 
See Instantiating a visible character


Drawing raw figures
The raw drawing script renders shapes and other effects into SVG commands. It is called after everything it needs for reference points has already been rendered (i.e. captions). Presumably, once I get character drawing back into action, the character interpreter will generate raw drawing commands for this script to process. I don't know yet; we'll see what makes sense when I get that far. August 25, 2007:
Oy. Drawing of the already-existing figures in the Toonbots pantheon has rapidly outstripped the capabilities of my simplistic Take-2 rendition. Not to mention that I have no convincing place to set the location of drawn figures which don't have a predefined location. And of course few of them do, since Take 1 hadn't even implemented that...
Definition of draw.pl:
 
use Workflow::wftk::XML;
$in = $ARGV[0];
$panel_w = $ARGV[1];
$panel_h = $ARGV[2];

open IN, $in or die "Can't open $in for reading";
$in = xml_read (*IN);
close IN;

$svg = xml_create ('g');
draw ($svg, $in, $panel_w, $panel_h);
print xml_string ($svg) . "\n";
At any rate, a drawing command consists of a "draw" tag which specifies a shape. The shapes are the standard ones (circle, rect, polyline) and at least one (scribble) which I've just made up to aspire to Pokeydom. One way to specify any shape is using an "area". (For the scribble, that's the only way.) The area may -- this is cool -- be relative to something else on the panel. (Now we're talking Take 2!) So before drawing anything, we need to examine the area, if there is one, and calculate anything necessary.
 
sub draw {
   my ($svg, $in, $p_w, $p_h) = @_;
   my ($x, $y, $w, $h) = (0, 0, 0, 0);

   my $area = xml_attrval ($in, 'area');
   if ($area) {
      if ($area =~ /:/) {#Rel
         ($ref, $spec) = split /: */, $area;
         $ref =~ s/\[.*//;
         if ($ref =~ /^!/) {
            $ref =~ s/^!//;
            $refsvg = xml_attrval ($in, "ref-$ref");
            if (-e $refsvg) {
               open IN, $refsvg or die "Can't open $refsvg for reading";
               $s = xml_read (*IN);
               close IN;

               $x = xml_attrval ($s, 'x');
               $y = xml_attrval ($s, 'y');
               $w = xml_attrval ($s, 'w');
               $h = xml_attrval ($s, 'h');
            }
         } else { # Panel?
            $x = 0;
            $y = 0;
            $w = $p_w;
            $h = $p_h;
         }

         foreach $s (split /; */, $spec) {
            @cmd = split / /, $s;
            if ($cmd[0] eq 'extend') {
                 if ($cmd[1] eq 'up') {
                  $x = $x - $cmd[2];
               } elsif ($cmd[1] eq 'down') {
                  $h = $h + $cmd[2];
               } elsif ($cmd[1] eq 'right') {
                  $y = $y - $cmd[2];
               } elsif ($cmd[1] eq 'left') {
                  $w = $w + $cmd[2];
               }
            }
         }
      }
   }

   my $shape = xml_attrval ($in, 'shape');
   $shape = xml_attrval ($in, 'type') unless $shape;

   if ($shape eq 'circle') {
      ($x, $y, $w, $h) = draw_circle ($svg, $in, $x, $y, $w, $h);
   } elsif ($shape eq 'scribble') {
      draw_scribble ($svg, $in, $x, $y, $w, $h);
   } elsif ($shape eq 'image') {
      draw_image ($svg, $in, $x, $y, $w, $h);
   } elsif ($shape eq '') {
      # Do nothing, since this is just a grouping structure.
   } else {
      print STDERR "Unknown drawing shape $shape specified.\n";
   }

   # Now handle the children of this structure.
   foreach my $child (xml_elements ($in)) {
      if (xml_is ($child, 'draw')) {
         draw ($svg, $child, $w, $h);
      }
   }
   xml_set ($svg, 'x', $x);
   xml_set ($svg, 'y', $y);
   xml_set ($svg, 'w', $w);
   xml_set ($svg, 'h', $h);
}
Now some drawing commands. Circle is first.
 
sub draw_circle {
   my ($svg, $in, $x, $y, $w, $h) = @_;

   $cmd = xml_create ('circle');
   xml_append_pretty ($svg, $cmd);
   @center = split /,/, xml_attrval ($in, 'center');
   if ($center[0] =~ /(\d+),(\d+)/) {
      $cx = $1;
      $cy = $2;
   } elsif (!$center[0]) {
      $cx = $x + $w/2;
      $cy = $y + $h/2;
      $center = "$x,$y";
   } else {
      $cx = $panel_w / 2;
      $cy = $panel_h / 2;
   }
   shift @center;
   ($cx, $cy) = split /,/, move_point($cx, $cy, @center);

   xml_set ($cmd, 'cx', $cx);
   xml_set ($cmd, 'cy', $cy);

   $radius = xml_attrval ($in, 'radius');
   xml_set ($cmd, 'r', $radius);

   if (!$w && !$h) {
      $w = $radius * 2;
      $h = $radius * 2;
      $x = $cx - $radius;
      $y = $cy - $radius;
   }

   draw_style ($cmd, $in);

   return ($x, $y, $w, $h);
}
Or maybe we want to draw a scribble instead...
 
sub draw_scribble {
   my ($svg, $in, $x, $y, $w, $h) = @_;

   $cmd = xml_create ('polyline');
   xml_append_pretty ($svg, $cmd);

   $loops = xml_attrval ($in, 'loops');
   $loops = 20 unless $loops;

   $side = 0;
   $line = '';
   for ($i=0; $i < $loops; $i++) {
      if ($side) {
         $lx = $x + $w;
         $side = 0;
      } else {
         $lx = $x;
         $side = 1;
      }
      $ly = $y + rand($h);
      $line .= "$lx,$ly ";
   }

   xml_set ($cmd, 'points', $line);
   draw_style ($cmd, $in);

   return ($x, $y, $w, $h);
}
Image...
 
sub draw_image {
   my ($svg, $in, $x, $y, $w, $h) = @_;

   $cmd = xml_create ("image");
   xml_set ($cmd, "x", $x);
   xml_set ($cmd, "y", $y);
   xml_set ($cmd, "width", xml_attrval ($in, 'width'));
   xml_set ($cmd, "height", xml_attrval ($in, 'height'));

   $imagefile = xml_attrval ($in, 'file');
   if (xml_attrval ($in, 'invert') eq 'yes') {
      $inverted = "flop_" . xml_attrval ($in, 'file');
      if (!-e $inverted) {
         print STDERR "Flopping $imagefile\n";
         system "convert -flop $imagefile $inverted";
      }
      $imagefile = $inverted;
   }

   xml_set ($cmd, "xlink:href", $imagefile);
   xml_append_pretty ($svg, $cmd);
}
Wrap it all up.
 
Now there are a few loose ends we used up above.
 
sub draw_style {
   my ($cmd, $in) = @_;

   $stroke = xml_attrval ($in, 'stroke');
   $stroke = 'black' unless $stroke;
   $width = xml_attrval ($in, 'width');
   $width = '1' unless $width;
   $fill = xml_attrval ($in, 'fill');
   $fill = 'none' unless $fill;
   xml_set ($cmd, 'style', "stroke:$stroke; stroke-width:$width; fill:$fill");
}

sub move_point { # TODO: this goes into a module!
   my ($x, $y, @loc) = @_;

   foreach my $offset (@loc) {
      $offset =~ s/^ *//;
      my @o = split / /, $offset;
      if ($o[0] eq 'up') {
         $y -= $o[1];
      } elsif ($o[0] eq 'down') {
         $y += $o[1];
      } elsif ($o[0] eq 'left') {
         $x -= $o[1];
      } elsif ($o[0] eq 'right') {
         $x += $o[1];
      }
   }

   return "$x,$y";
}


Drawing text
This script is actually used twice for each text piece. The first time, it has an info file of "no-info", and it writes a simple SVG to produce a graphic of the text snippet, which is then clipped, and run through "identify" to produce an info file. That info file thus includes the actual pixel extent of the text graphic.

The second time through, we get that info file. Using that, we can place the text command and draw a box, and we can use the size of the panel to determine placement offsets for the whole kit and caboodle.

November 1, 2006:
OK, then there's now a third time through, in which the entire caption and box have already been rendered, then externally rotated. In that case, we have to either place the image, or determine the proper clipping path then place the image with that path.

Our input in all cases is simply the caption XML.
Definition of draw_panels.pl:
 
use Workflow::wftk::XML;

$in = $ARGV[0];
$info = $ARGV[1];
$panel_w = $ARGV[2];
$panel_h = $ARGV[3];
$rotate = $ARGV[4];

open IN, $in or die "Can't open $in for reading";
$in = xml_read (*IN);
close IN;

$string = xml_stringcontent ($in);
$string =~ s/\n//g;
$string =~ s/<br\/>/\n/g;

sub make_style {
   $color = xml_attrval ($in, 'color');
   $color = xml_attrval ($in, 'fgcolor') unless $color;
   $color = 'black' unless $color;

   $size = xml_attrval ($in, 'size');
   $size = 16 unless $size;

   $stroke = xml_attrval ($in, 'stroke');
   $stroke = 'none' unless $stroke;

   $family = xml_attrval ($in, 'font');
   $family = 'verdana' unless $family;

   $style = xml_attrval ($in, 'style');
   $style = "; $style" if $style;
   foreach $attr ('text-decoration', 'font-weight', 'font-style') {
      my $v = xml_attrval ($in, $attr);
      if ($v) { $style .= "; $attr:" . $v; }
   }

   return "font-family:\@fonts/$family.ttf; font-size:$size; fill:$color; stroke:$stroke$style";
}

sub copy_text_attributes {
   my ($text, $in) = @_;

   foreach $attr ('text-decoration', 'font-weight', 'font-style') {
      xml_set ($text, $attr, xml_attrval ($in, $attr));
   }
}

if ($info eq 'no-info') {
   # First run through.
   
   $svg = xml_create ('svg');
   xml_set ($svg, 'width', $panel_w);
   xml_set ($svg, 'height', $panel_h);

   $text = xml_create ('text');
   xml_set ($text, 'x', '0');
   xml_set ($text, 'y', '0');
   xml_set ($text, 'style', make_style ($in));
   #copy_text_attributes ($text, $in);
   xml_append ($text, xml_createtext ($string));
   xml_append_pretty ($svg, $text);

   print xml_string ($svg) . "\n";
   exit;
}

# Second or third run through, we have an info file from 'identify'.
open IN, $info or die "Can't open $info for reading";
$info = xml_read (*IN);
close IN;

if ($rotate) {
   # if the info file is from a rotated graphic, third time through.

   $h = xml_attrval ($info, 'height');
   $w = xml_attrval ($info, 'width');

   $svg = xml_create ('g');

   $image = xml_create ('image');
   xml_set ($image, 'x', '0');
   xml_set ($image, 'y', '0');
   xml_set ($image, 'height', $h);
   xml_set ($image, 'width', $w);
   xml_set ($image, 'xlink:href', xml_attrval ($info, 'file'));

   if ($rotate == 90 || $rotate == 180 || $rotate == 270) {
      # If the rotation is to a right angle, no need to clip.  Just place the image and go on.
      xml_append_pretty ($svg, $image);
   } else {
      # This is hard, so I'm not doing it yet.  It requires trigonometry, and my brain hurts from it.
      xml_append_pretty ($svg, $image);
   }
} else {
   # Second time through: draw text straight, with box.
   $margin = xml_attrval ($in, 'box-margin');
   $margin = 2 unless $margin;

   $size = xml_attrval ($in, 'size');
   $size = 16 unless $size;
   $h = xml_attrval ($info, 'height') + $margin*2 + $size; # Font size correction because trimming cuts off at top as well...
   $w = xml_attrval ($info, 'width') + $margin*2 + $size;

   # Now build the SVG for the overall caption.
   $svg = xml_create ('g');

   $text = xml_create ('text');
   xml_set ($text, 'x', $margin + $size/2);
   xml_set ($text, 'y', $margin);
   xml_set ($text, 'style', make_style ($in));
   #copy_text_attributes ($text, $in);

   xml_append ($text, xml_createtext ($string));

   if (xml_name($in) eq 'caption') {
      $box = xml_create ('rect');
      xml_set ($box, 'x', '0');
      xml_set ($box, 'y', '0');
      xml_set ($box, 'width', $w);
      xml_set ($box, 'height', $h);
   } elsif (xml_name($in) eq 'dialog') {
      $box = xml_create ('polyline'); # if a balloon, we need to position first and then draw the balloon stem.
   }

   $stroke = xml_attrval ($in, 'box-stroke');
   $stroke = 'black' unless $stroke;
   $width = xml_attrval ($in, 'box-line');
   $width = '1' unless $width;
   $fill = xml_attrval ($in, 'box-fill');
   $fill = 'white' unless $fill;
   $style = xml_attrval ($in, 'box-style');
   $style = "; $style" if $style;

   if (xml_attrval ($in, 'box') eq 'no') {
      $stroke = 'none';
      $fill = 'none';
   }

   xml_set ($box, 'style', "stroke:$stroke; stroke-width:$width; fill:$fill$style");
   xml_append_pretty ($svg, $box);
   xml_append_pretty ($svg, $text);

   # If there is a direction, we rotate the text by a certain number of degrees and if the text is now vertical we need to
   # swap its height and width for location calculation.
   # November 1, 2006 - doesn't work due to IM bugs.  Someday we might want to revisit this; it generates good SVG and SHOULD work.
   #$direction = xml_attrval ($in, 'direction');
   #$rotated = 0;
   #$midpoint = ($w/2) . "," . ($h/2);
   #$negmidpoint = (-$w/2) . "," . (-$h/2);
   #if ($direction eq 'up') {
   #   ($h, $w) = ($w, $h);
   #   $rotated = 1;
   #   xml_set ($svg, 'transform', "translate($midpoint) rotate(-90) translate($negmidpoint)");
   #} elsif ($direction eq 'down') {
   #   ($h, $w) = ($w, $h);
   #   $rotated = 1;
   #   xml_set ($svg, 'transform', "translate($midpoint) rotate(90) translate($negmidpoint)");
   #} elsif ($direction eq 'inverted') {
   #   $rotated = 1;
   #   xml_set ($svg, 'transform', "translate($midpoint) rotate(180) translate($negmidpoint)");
   #}
   #
   #if (xml_attrval ($in, 'rotate')) { # Arbitrary rotation -- doesn't affect placement (I don't care about it *that* much)
   #   $rotated = 1;
   #   $rotation = xml_attrval ($in, 'rotate');
   #
   #   $svg = transform_svg ($svg, "translate($negmidpoint)");
   #   $svg = transform_svg ($svg, "rotate($rotation)");
   #   $svg = transform_svg ($svg, "translate($midpoint)");
   #}
   #
   #if ($rotated) {
   #   $midgroup = $svg;
   #   $svg = xml_create ('g');
   #   xml_append_pretty ($svg, $midgroup);
   #}
}

# Now figure out the location on the panel and set a transformation on the overall 'g' (group) tag, and output that puppy, it's done.
# Caveat: if the panel width and height are 0, then this is the second time through and there will be a third time.  Act accordingly.
if ($panel_h == 0 && $panel_w == 0) {
   $g = $svg;
   xml_set ($g, 'transform', 'translate(5,5)');
   $svg = xml_create ("svg");
   xml_set ($svg, 'height', $h+10);
   xml_set ($svg, 'width', $w+10);
   xml_append_pretty ($svg, $g);
} elsif (xml_name ($in) eq 'caption') {
   $loc = calculate_location(xml_attrval ($in, 'location'), $h, $w, $panel_h, $panel_w);
   ($x, $y) = split /,/, $loc;
   xml_set ($svg, 'transform', "translate(" . calculate_location (xml_attrval ($in, 'location'), $h, $w, $panel_h, $panel_w) . ")");
   xml_set ($svg, 'x', $x);
   xml_set ($svg, 'y', $y);
   xml_set ($svg, 'w', $w);
   xml_set ($svg, 'h', $h);
} else { # Balloon!
   $ref = xml_attrval ($in, 'ref-who');
   open IN, $ref or die "Can't open $ref for reading";
   $s = xml_read (*IN);
   close IN;

   $loc = calculate_rel_location (xml_attrval ($in, 'location'), $s, $h, $w, $panel_h, $panel_w);
   ($x, $y, $dir) = split /,/, $loc;

   xml_set ($text, 'transform', "translate($x,$y)");

   $ptw = xml_attrval ($in, "point-width");
   $ptw = 20 unless $ptw;
   $where = xml_attrval ($in, "where");
   $where = "right, right 5" unless $where;

   my $point = calculate_rel_point ($where, $s, $panel_h, $panel_w);

   $x1 = $x;
   $x2 = $x + $w;
   $y1 = $y;
   $y2 = $y + $h;

   if ($dir =~ /^t/) {
      $mid = $x1 + $w/2;
      $mid1 = $mid - $ptw/2;
      $mid2 = $mid + $ptw/2;

      xml_set ($box, 'points', "$x1,$y1 $mid1,$y1 $point $mid2,$y1 $x2,$y1 $x2,$y2 $x1,$y2 $x1,$y1");
   } elsif ($dir =~ /^r/) {
      $mid = $y1 + $h/2;
      $mid1 = $mid - $ptw/2;
      $mid2 = $mid + $ptw/2;

      xml_set ($box, 'points', "$x1,$y1 $x2,$y1 $x2,$mid1 $point $x2,$mid2 $x2,$y2 $x1,$y2 $x1,$y1");
   } elsif ($dir =~ /^b/) {
      $mid = $x1 + $w/2;
      $mid1 = $mid - $ptw/2;
      $mid2 = $mid + $ptw/2;

      xml_set ($box, 'points', "$x1,$y1 $x2,$y1 $x2,$y2 $mid2,$y2 $point $mid1,$y2 $x1,$y2 $x1,$y1");
   } elsif ($dir =~ /^l/) {
      $mid = $y1 + $h/2;
      $mid1 = $mid - $ptw/2;
      $mid2 = $mid + $ptw/2;

      xml_set ($box, 'points', "$x1,$y1 $x2,$y1 $x2,$y2 $x1,$y2 $x1,$mid2 $point $x1,$mid1 $x1,$y1");
   } else {
      xml_set ($box, 'points', "$x1,$y1 $x2,$y1 $x2,$y2 $x1,$y2 $x1,$y1");
   }
}

print xml_string ($svg) . "\n";

sub transform_svg {
   my ($svg, $t) = @_;

   my $s = xml_create ("g");
   xml_append_pretty ($s, $svg);
   xml_set ($s, 'transform', $t);

   return $s;
}

sub calculate_location {
   my ($location, $h, $w, $panel_h, $panel_w) = @_;

   if ($panel_h == 0 && $panel_w == 0) { return "0,0"; }

   my $x;
   my $y;

   my @loc = split /,/, $location;

   if ($loc[0] =~ /^top/) {
      $y = 0;
   } elsif ($loc[0] =~ /^middle/ || $loc[0] =~ /^center/) {
      $y = int(($panel_h - $h) / 2);
   } else {
      $y = $panel_h - $h;
   }

   if ($loc[0] =~ /right$/) {
      $x = $panel_w - $w;
   } elsif ($loc[0] =~ /middle$/ || $loc[0] =~ /center$/) {
      $x = int(($panel_w - $w) / 2);
   } else {
      $x = 0;
   }

   shift @loc;
   return (move_point ($x, $y, @loc));
}

sub move_point {
   my ($x, $y, @loc) = @_;

   foreach my $offset (@loc) {
      $offset =~ s/^ *//;
      my @o = split / /, $offset;
      if ($o[0] eq 'up') {
         $y -= $o[1];
      } elsif ($o[0] eq 'down') {
         $y += $o[1];
      } elsif ($o[0] eq 'left') {
         $x -= $o[1];
      } elsif ($o[0] eq 'right') {
         $x += $o[1];
      }
   }

   return "$x,$y";
}

sub calculate_rel_location {
   my ($location, $rel, $h, $w, $panel_h, $panel_w) = @_;

   if ($panel_h == 0 && $panel_w == 0) { return "0,0,*"; }

   my $x;
   my $y;
   my $dir;

   my $relx = xml_attrval ($rel, 'x');
   my $rely = xml_attrval ($rel, 'y');
   my $relw = xml_attrval ($rel, 'w');
   my $relh = xml_attrval ($rel, 'h');

   my @loc = split /,/, $location;

   if ($loc[0] =~ /^bottom/) {
      $y = $rely + $relh;
      $dir = "t";
   } elsif ($loc[0] =~ /^middle/ || $loc[0] =~ /^center/) {
      $y = $rely + ($relh - $h) / 2;
      $dir = "";
   } else {
      $y = $rely - $h;
      $dir = "b";
   }

   if ($loc[0] =~ /left$/) {
      $x = $relx - $w;
      $dir .= "r";
   } elsif ($loc[0] =~ /middle$/ || $loc[0] =~ /center$/) {
      $x = $relx + ($relw - $w) / 2;
   } else {
      $x = $relx + $relw;
      $dir .= "l";
   }

   $dir = "*" unless $dir;
   shift @loc;
   return (move_point ($x, $y, @loc) . ",$dir");
}

sub calculate_rel_point {
   my ($location, $rel, $panel_h, $panel_w) = @_;

   $location =~ s/^right/center right/;
   $location =~ s/^left/center left/;

   my ($x, $y, $dir) = split /,/, calculate_rel_location ($location, $rel, 0, 0, $panel_h, $panel_w);
   return "$x,$y";
}

This code and documentation are released under the terms of the GNU license. They are additionally copyright (c) 2001-2007, Vivtek. All rights reserved except those explicitly granted under the terms of the GNU license. This presentation prepared using LPML. Try literate programming. You'll like it.





Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.