|
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, 2000 | Panel layout |
October 28, 2000 | Caption layout |
up to Nov 10 or so | Various panel and caption features |
November 12, 2000 | Rudimentary character sizing and placement |
December 2000 | A first stab at variant structures for characters |
January 1, 2001 | The first drawing commands, definition of named points for reference, etc. |
April 2001 | Conversion 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, 2001 | Rework of text handling to use XML font summaries generated by my new tool ttfx.
Captions again! |
|
|
October, 2006 - January, 2007 | Take 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.
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.
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.
|

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