The amazing Toon-o-Matic, take 2
Nov 17, 2000
This is yet another prototype workaround for some functionality that has the potential to be really, really, I mean really cool. A visible character has some specific state; when we actually instantiate the visible character in a specific panel, we use that state information to select drawing commands from the character definition. Of course, right now we'll completely ignore that whole issue and simply copy drawing commands from the character definition into the visible character -- but things are going to get a lot more complicated later. Dec 15, 2000
And now they're starting to get complicated. What we want here is the ability to select one aspect from a set of variants. The aspect triggers on the current
%character_aspect
hash value for the character. The aspect (for now) will
just be a list of strings; when we hit a variant, we scan aspects until something matches. If no aspect is active, the
first variant is selected.
Dec 17, 2000The bounding box of a region is going to determine element size as well. As regions become specified, they'll carry along in $visible. I have the feeling that this commentary is going to make no sense whatsoever tomorrow. Dec 31, 2000
I'm gonna have drawing primitives this year, by golly! (OK, it's 10 PM, so maybe not.) The
drawing_instantiate
function is taking on new meaning. Its basic function is to take pieces of the character definition and transfer them to
the visible character. Along the way it's now being asked to set the parameters of the drawing primitives, too. And of
course that also means that it copies the draw, line, point, and region tags, and decides among the aspects in a variant
tag. So that's what it does.
January 3, 2001I'm stashing the
modify_character
function on this page as well. Hoo boy, drawing cartoons is harder than it
looks!
sub drawing_instantiate { my ($character, $visible, $state) = @_; my $child; my $new; foreach $child (xml_elements ($character)) { if ($$child{name} eq 'draw') { $new = xml_create ('draw'); foreach (@{$$child{attrs}}) { xml_set ($new, $_, xml_attrval ($child, $_)); } xml_set ($new, 'rel-h', '100') if xml_attrval ($child, 'rel-h') eq ''; xml_set ($new, 'rel-w', '100') if xml_attrval ($child, 'rel-w') eq ''; # Use bounding box as dimensions of this drawing element. xml_set ($new, 'height', xml_attrval ($visible, 'height') * xml_attrval ($new, 'rel-h') / 100); xml_set ($new, 'width', xml_attrval ($visible, 'width') * xml_attrval ($new, 'rel-w') / 100); # Determine parameters of the new object. drawing_parameterize ($new, $visible, $state); xml_append ($visible, $new); drawing_instantiate ($child, $new, $state); } elsif ($$child{name} eq 'point') { $new = xml_create ('point'); foreach (@{$$child{attrs}}) { xml_set ($new, $_, xml_attrval ($child, $_)); } drawing_parameterize ($new, $visible, $state); xml_append ($visible, $new); } elsif ($$child{name} eq 'line') { $new = xml_create ('line'); foreach (@{$$child{attrs}}) { xml_set ($new, $_, xml_attrval ($child, $_)); } drawing_parameterize ($new, $visible, $state); xml_append ($visible, $new); } elsif ($$child{name} eq 'region') { $new = xml_create ('region'); foreach (@{$$child{attrs}}) { xml_set ($new, $_, xml_attrval ($child, $_)); } drawing_parameterize ($new, $visible, $state); xml_append ($visible, $new); drawing_instantiate ($child, $new, $state); # Regions, unlike points and lines, contain things. } elsif ($$child{name} eq 'variant') { # Here's where we select an aspect. #print "Instantiating variant ($state).\n"; $choice = ''; foreach $variant (xml_elements ($child)) { next if $$variant{name} ne 'aspect'; $choice = $variant unless $choice ne ''; if (match_aspect ($state, $variant)) { $choice = $variant; last; } } if ($choice ne '') { #print "Instantiating as "; xml_write (STDOUT, $choice); print "\n"; drawing_instantiate ($choice, $visible, $state); } } } } |
sub match_aspect { my ($aspect, $variant) = @_; my @alist = split /\s+/, $aspect; return (grep { $_ eq xml_attrval ($variant, 'name'); } @alist); } sub match_aspect_string { my ($aspect, $string) = @_; my @alist = split /\s+/, $aspect; return (grep { $_ eq $string; } @alist); } |
(February 14, 2001): added the ability to specify a list of aspect components, anyway. A little more complex than what it was, which was simply checking for equality. December 31, 2000
And we also need the handy-dandy drawing parameterizer. Again, this could get arbitrarily complex, because it will work with the spatial relationships of points and regions and stuff. This initial implementation will be pretty non-complex, but later this will probably end up containing most of the interesting code of the Toon-o-Matic. January 10, 2001
Oooh. Added lines. Now I have ellipses and lines. Wow!
sub drawing_parameterize { my ($piece, $context, $aspect) = @_; if ($$piece{name} eq 'draw') { if (xml_attrval ($piece, 'type') eq 'image') { xml_set ($piece, 'rel-x', '0') if xml_attrval ($piece, 'rel-x') eq ''; xml_set ($piece, 'rel-y', '0') if xml_attrval ($piece, 'rel-y') eq ''; if (xml_attrval ($piece, 'face') eq 'left' && match_aspect_string ($aspect, 'faceright')) { xml_set ($piece, 'invert', 'yes'); } if (xml_attrval ($piece, 'face') eq 'right' && match_aspect_string ($aspect, 'faceleft')) { xml_set ($piece, 'invert', 'yes'); } } elsif (xml_attrval ($piece, 'type') eq 'ellipse') { # Ellipse needs a center, height and width, start and end angles. Height and width are already known. if (xml_attrval ($piece, 'height') eq '') { xml_set ($piece, 'height', xml_attrval ($context, 'height') * xml_attrval ($piece, 'rel-h') / 100); } if (xml_attrval ($piece, 'width') eq '') { xml_set ($piece, 'width', xml_attrval ($context, 'width') * xml_attrval ($piece, 'rel-w') / 100); } if (xml_attrval ($piece, 'center') eq '' || $aspect eq 'recalc') { xml_set ($piece, 'center', 'middle'); } ($x, $y) = find_point (xml_attrval ($piece, 'center'), $context, $state); # The context is the local region. xml_set ($piece, 'rel-x', $x); xml_set ($piece, 'rel-y', $y); xml_set ($piece, 'start', '0') unless xml_attrval ($piece, 'start') ne ''; xml_set ($piece, 'end', '360') unless xml_attrval ($piece, 'end') ne ''; } elsif (xml_attrval ($piece, 'type') eq 'line') { # Line needs start and end points. ($x, $y) = find_point (xml_attrval ($piece, 'start'), $context, $state); xml_set ($piece, 'start-x', $x); xml_set ($piece, 'start-y', $y); ($x, $y) = find_point (xml_attrval ($piece, 'end'), $context, $state); xml_set ($piece, 'end-x', $x); xml_set ($piece, 'end-y', $y); } elsif (xml_attrval ($piece, 'type') eq 'circle') { } elsif (xml_attrval ($piece, 'type') eq 'rectangle') { } } elsif ($$piece{name} eq 'point') { ($x, $y) = find_point (xml_attrval ($piece, 'loc'), $context, $state); xml_set ($piece, 'rel-x', $x); xml_set ($piece, 'rel-y', $y); } elsif ($$piece{name} eq 'line') { } elsif ($$piece{name} eq 'region') { } } |
Well, so much for getting this working in 2000, eh? No matter, it's starting to make sense. The
find_point
function returns an x and a y relative to the local region, given a point description. The point description is going
to be really baroque and chaotic. This will be fun. Later, the aspect will parameterize point locations, allowing us
to specify what happens to, say, the sizes of eyes when a certain emotive aspect is desired. I love it when a plan comes
together.
sub find_point { my ($loc, $context, $state) = @_; my @loc = split /\s+/, $loc; # Break into words. my $basepoint; my $word; # Great Scott! my $x=0; # The location we're fixing. my $y=0; # The points middle, center, top, bottom, left, right, and combinations of these are the basic points in a region. $word = shift @loc; if ($word eq 'middle' || $word eq 'center') { $basepoint = 'center'; $word = shift @loc; } elsif ($word eq 'top') { $basepoint = 'top'; $word = shift @loc; if ($word eq 'left') { $basepoint = 'topleft'; $word = shift @loc; } elsif ($word eq 'right') { $basepoint = 'topright'; $word = shift @loc; } elsif ($word eq 'middle') { $word = shift @loc; } } elsif ($word eq 'bottom') { $basepoint = 'bottom'; $word = shift @loc; if ($word eq 'left') { $basepoint = 'bottomleft'; $word = shift @loc; } elsif ($word eq 'right') { $basepoint = 'bottomright'; $word = shift @loc; } elsif ($word eq 'middle') { $word = shift @loc; } } elsif ($word eq 'left') { $basepoint = 'left'; $word = shift @loc; if ($word eq 'side') { $word = shift @loc; } } elsif ($word eq 'right') { $basepoint = 'right'; $word = shift @loc; if ($word eq 'side') { $word = shift @loc; } } elsif ($word eq 'between') { # Between *will* take two points and use the midpoint. Later. This whole function is wrong for that. } else { $basepoint = $word; $word = shift @loc; } my $region = $context; # Default region is local region, i.e. local context. my $x_translate = 0; # This will be modified if another region is being used. my $y_translate = 0; if ($word eq 'of') { # We're referring to another region with our basic point, not this one. Handle it later. $word = shift @loc; # $region = find the region; # $x_translate = whatever difference between regions, same for y_translate; $word = shift @loc; } # So we have the region and the base point within it. Let's go fer it. if ($basepoint eq 'center') { $x = $x_translate + xml_attrval ($region, 'width') / 2; $y = $y_translate + xml_attrval ($region, 'height') / 2; } elsif ($basepoint eq 'top') { $x = $x_translate + xml_attrval ($region, 'width') / 2; $y = $y_translate; } elsif ($basepoint eq 'topleft') { $x = $x_translate; $y = $y_translate; } elsif ($basepoint eq 'topright') { $x = $x_translate + xml_attrval ($region, 'width'); $y = $y_translate; } elsif ($basepoint eq 'left') { $x = $x_translate; $y = $y_translate + xml_attrval ($region, 'height') / 2; } elsif ($basepoint eq 'right') { $x = $x_translate + xml_attrval ($region, 'width'); $y = $y_translate + xml_attrval ($region, 'height') / 2; } elsif ($basepoint eq 'bottomleft') { $x = $x_translate; $y = $y_translate + xml_attrval ($region, 'height'); } elsif ($basepoint eq 'bottom') { $x = $x_translate + xml_attrval ($region, 'width') / 2; $y = $y_translate + xml_attrval ($region, 'height'); } elsif ($basepoint eq 'bottomright') { $x = $x_translate + xml_attrval ($region, 'width'); $y = $y_translate + xml_attrval ($region, 'height'); } else { # Look up the point in the given region. If not found, go up to the region's parent, and so on. ($x, $y) = lookup_point ($basepoint, $region); } # Now we can *modify* the point we've found, using up, down, left, right, and maybe other stuff later. if ($word ne '') { ($x, $y) = move_point ($x, $y, $region, join (' ', $word, @loc)); } return ($x, $y); } |
lookup_point
routine to find a named point in the region.
sub lookup_point { my ($point, $region, $x_translate, $y_translate) = @_; $x_translate = 0 if !$x_translate; $y_translate = 0 if !$y_translate; my $x = xml_attrval ($region, 'width') / 2; my $y = xml_attrval ($region, 'height') / 2; my $elem; foreach $elem (xml_elements ($region)) { next if $$elem{name} ne 'point'; if (xml_attrval ($elem, 'name') eq $point) { $x = $x_translate + xml_attrval ($elem, 'rel-x'); $y = $y_translate + xml_attrval ($elem, 'rel-y'); return ($x, $y); } } my $parent = $$elem{parent}; if ($$parent{name} eq 'cartoon') { return ($x, $y); } return lookup_point ($point, $parent, 0 - xml_attrval ($region, 'rel-x'), 0 - xml_attrval ($region, 'rel-y')); } |
So I mentioned above that the
modify_character
function is also going in here -- that's because it
resembles all the foregoing so much. Basically, what it does it allow the metacartoonist to specify pieces of a visible
character by name, and override various aspects of the pieces after the fact. Note that this might be foolhardy -- what if
modification of a large entity's size means that locations of its subentities need to change? Right now I can't think of
a good way to find out what subentities have been determined at runtime (and are thus subject to modification propagation)
and which ones were specified explicitly (and thus probably shouldn't be modified unless explicitly.) Ah well. I suppose
the time will come when I'll make sense of that.
On second thought, the way I'm doing this doesn't make any sense, because if I move a named point, for instance, things
which depend on it won't be affected, and that doesn't work. Later maybe I'll think of a better way of doing things; for
now, I guess I'll just stick with very explicitly and rigidly specified modifications. Sigh.
So here goes:
sub modify_character { my ($character, $mod) = @_; my $piece; my $target; foreach $piece (xml_elements ($mod)) { my $mode = xml_attrval ($piece, 'mode'); $mode = 'add' if $mode eq ''; $target = xml_search_first ($character, $$piece{name}, 'name', xml_attrval ($piece, 'name')); if (!$target && ($mode eq 'add')) { $target = xml_create ($$piece{name}); foreach $attr (@{$$piece{attrs}}) { xml_set ($target, $attr, xml_attrval ($piece, $attr)); } xml_append ($character, $target); drawing_parameterize ($target, $character, ''); next; } if ($target && $mode eq 'suppress') { $$target{name} = 'suppressed-' . $$target{name}; next; } next unless $target; @consumed = ('name', 'type', 'mode'); if (xml_attrval ($target, 'type') eq 'ellipse') { if (xml_attrval ($piece, 'center') ne '') { push @consumed, 'center'; $x = xml_attrval ($target, 'rel-x'); $y = xml_attrval ($target, 'rel-y'); ($x, $y) = move_point ($x, $y, $character, xml_attrval ($piece, 'center')); xml_set ($target, 'rel-x', $x); xml_set ($target, 'rel-y', $y); } if (xml_attrval ($piece, 'size') ne '') { push @consumed, 'size'; xml_set ($target, 'height', modify_scalar (xml_attrval ($target, 'height'), xml_attrval ($piece, 'size'))); xml_set ($target, 'width', modify_scalar (xml_attrval ($target, 'width'), xml_attrval ($piece, 'size'))); } if (xml_attrval ($piece, 'width') ne '') { push @consumed, 'width'; xml_set ($target, 'width', modify_scalar (xml_attrval ($target, 'width'), xml_attrval ($piece, 'width'))); } if (xml_attrval ($piece, 'height') ne '') { push @consumed, 'height'; xml_set ($target, 'height', modify_scalar (xml_attrval ($target, 'height'), xml_attrval ($piece, 'height'))); } } elsif (xml_attrval ($target, 'type') eq 'line') { if (xml_attrval ($piece, 'start') ne '') { push @consumed, 'start'; $x = xml_attrval ($target, 'start-x'); $y = xml_attrval ($target, 'start-y'); ($x, $y) = move_point ($x, $y, $character, xml_attrval ($piece, 'start')); xml_set ($target, 'start-x', $x); xml_set ($target, 'start-y', $y); } if (xml_attrval ($piece, 'end') ne '') { push @consumed, 'end'; $x = xml_attrval ($target, 'end-x'); $y = xml_attrval ($target, 'end-y'); ($x, $y) = move_point ($x, $y, $character, xml_attrval ($piece, 'end')); xml_set ($target, 'end-x', $x); xml_set ($target, 'end-y', $y); } } # Now mop up any other attributes that the modifying entry has. (Jan. 7, 2001) foreach $attr (@{$$piece{attrs}}) { next if grep { $_ eq $attr } @consumed; xml_set ($target, $attr, xml_attrval ($piece, $attr)); } if ($mode eq 'recalc') { drawing_parameterize ($target, $$target{parent}, 'recalc'); } } } |
move_point
and modify_scalar
. Note that move_point
will be useful for rewriting our initial point finder, as it will be able to consume point modifiers like "down 10".
sub move_point { my ($x, $y, $context, $modifier) = @_; my @mod = split /\s+/, $modifier; my $width = xml_attrval ($context, 'width'); $width = 100 if !$width; my $height = xml_attrval ($context, 'height'); $height = 100 if !$height; my $word = shift @mod; if ($word eq 'down') { $word = shift @mod; $y += $word * $height / 100; } elsif ($word eq 'up') { $word = shift @mod; $y -= $word * $height / 100; } elsif ($word eq 'left') { $word = shift @mod; $x -= $word * $width / 100; } elsif ($word eq 'right') { $word = shift @mod; $x += $word * $width / 100; } if (@mod) { return move_point ($x, $y, $context, join (' ', @mod)); } return ($x, $y); } |
sub modify_scalar { my ($val, $modifier) = @_; if ($modifier =~ /%$/) { $modifier =~ s/%$//; $val = $val * $modifier / 100; } return $val; } |
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.