Games::Maze::SVG - Build mazes in SVG.


Games-Maze-SVG documentation Contained in the Games-Maze-SVG distribution.

Index


Code Index:

NAME

Top

Games::Maze::SVG - Build mazes in SVG.

VERSION

Top

Version 0.75

SYNOPSIS

Top

Games::Maze::SVG uses the Games::Maze module to create mazes in SVG.

    use Games::Maze::SVG;

    my $foo = Games::Maze::SVG->new();
    ...

See Games::Maze::SVG::Manual for more information on using the module.

FUNCTIONS

Top

new

Create a new Games::Maze::SVG object. Supports the following named parameters:

Takes one positional parameter that is the maze type: Rect, RectHex, or Hex

wallform

String naming the wall format. Legal values are bevel, round, roundcorners, and straight. Not all formats work with all maze shapes.

crumb

String describing the breadcrumb design. Legal values are dash, dot, line, and none

dir

Directory in which to find the ecmascript for the maze interactivity. Should either be relative, or in URL form.

interactive

This parameter determines if the maze will be interactive. If the value of the parameter is true (1), the appropriate scripting and support is written into the SVG. If the parameter is omitted or false, no interactive support is provided.

cols

The number of columns used in creating the maze. Default value is 12.

rows

The number of rows used in creating the maze. Default value is 12.

startcol

The column where the entry is found. Default value is random.

endcol

The column where the exit is found. Default value is random.

init_object

Initializes the maze object with the default values for all mazes. The derived classes should call this method in their constructors.

Returns the initial data members as a list.

set_interactive

Method makes the maze interactive.

Returns a reference to self for chaining.

set_breadcrumb

$bcs

String specifying the breadcrumb style. Generates an exception if the breadcrumb style is not recognized.

Returns a reference to self for chaining.

get_crumbstyle

Returns the CSS style for the breadcrumb.

get_script

Method that returns the path to the interactivity script.

toString

Method that converts the current maze into an SVG string.

make_board_array

Build a two-dimensional array of integers that maps the board from the two dimensional matrix of wall descriptions.

get_script_list

Returns a list of script URLs that will be needed by the interactive maze.

build_all_script

Generate the full set of script sections for the maze.

build_board_element

Create the element that describes the board.

$rows reference to an array of rows.

build_control_panel

Create the displayable control panel

$startx the starting x coordinate for the panel


Games-Maze-SVG documentation Contained in the Games-Maze-SVG distribution.
#  SVG maze output
#  Performs transformation, cleanup, and printing of output of Games::Maze

package Games::Maze::SVG;

use Carp;
use Games::Maze;
use Games::Maze::SVG::Rect;
use Games::Maze::SVG::RectHex;
use Games::Maze::SVG::Hex;

use strict;
use warnings;

our $VERSION = 0.75;

use constant SIGN_HEIGHT => 20;
use constant SIDE_MARGIN => 10;
use constant PANEL_WIDTH => 250;
use constant PANEL_MIN_HEIGHT => 365;

my %crumbstyles = (
                   dash => "stroke-width:1px; stroke-dasharray:5px,3px;",
                   dot  => "stroke-width:2px; stroke-dasharray:2px,6px;",
                   line => "stroke-width:1px;",
		   none => "visibility:hidden;",
                  );

my $license = <<'EOL';
  <metadata>
    <!--
        Copyright 2004-2006, G. Wade Johnson
	Some rights reserved.
    -->
    <rdf:RDF xmlns="http://web.resource.org/cc/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <Work rdf:about="">
       <dc:title>SVG Maze</dc:title>
       <dc:date>2006</dc:date>
       <dc:description>An SVG-based Game</dc:description>
       <dc:creator><Agent>
	  <dc:title>G. Wade Johnson</dc:title>
       </Agent></dc:creator>
       <dc:rights><Agent>
	  <dc:title>G. Wade Johnson</dc:title>
       </Agent></dc:rights>
       <dc:type rdf:resource="http://purl.org/dc/dcmitype/Interactive" />
       <license rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" />
    </Work>

    <License rdf:about="http://creativecommons.org/licenses/by-sa/2.0/">
       <permits rdf:resource="http://web.resource.org/cc/Reproduction" />
       <permits rdf:resource="http://web.resource.org/cc/Distribution" />
       <requires rdf:resource="http://web.resource.org/cc/Notice" />
       <requires rdf:resource="http://web.resource.org/cc/Attribution" />
       <permits rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
       <requires rdf:resource="http://web.resource.org/cc/ShareAlike" />
    </License>

    </rdf:RDF>
  </metadata>
EOL

# ----------------------------------------------
#  Subroutines

sub  new
{
    my $class = shift;
    
    my $shape = shift || 'Rect';
    
    my %params = @_;
    
    if(exists $params{crumb} && !exists $crumbstyles{$params{crumb}})
    {
        croak "Unrecognized breadcrumb style '$params{crumb}'.\n"
    }

    return Games::Maze::SVG::Rect->new( @_ )    if 'Rect' eq $shape;
    return Games::Maze::SVG::RectHex->new( @_ ) if 'RectHex' eq $shape;
    return Games::Maze::SVG::Hex->new( @_ )     if 'Hex' eq $shape;

    croak "Unrecognized maze shape '$shape'.\n";
}


sub init_object
{
    my %parms = @_;

    my %obj =
    (
        mazeparms => {},
	wallform  => 'straight',
	crumb     => 'dash',
	dir       => 'scripts/',
    );
    $obj{mazeparms}->{dimensions} = [ $parms{cols}||12, $parms{rows}||12, 1 ];
    $obj{mazeparms}->{entry} = [ $parms{startcol}, 1, 1 ] if $parms{startcol};
    
    if($parms{endcol})
    {
        $obj{mazeparms}->{exit} = [
            $parms{endcol}, $obj{mazeparms}->{dimensions}->[1], 1
        ];
    }

    return %obj;
}

sub  set_interactive
{
    my $self = shift;
    $self->{interactive} = 1;
    return $self;
}


sub  set_breadcrumb
{
    my $self = shift;
    my $bcs  = shift;

    return unless defined $bcs;

    croak "Unrecognized breadcrumb style '$bcs'.\n"
      unless exists $crumbstyles{$bcs};
    $self->{crumb} = $bcs;
    $self->{crumbstyle} = $crumbstyles{$bcs};

    return $self;
}


sub  get_crumbstyle
{
    my $self = shift;

    return $self->{crumbstyle} ||= $crumbstyles{$self->{crumb}};
}


sub get_script
{
    my $self = shift;
    
    return "$self->{dir}$self->{scriptname}";
}

sub  toString
{
    my $self = shift;
    my $maze = Games::Maze->new( %{$self->{mazeparms}} );

    $maze->make();
    my @rows = map { [ split q{}, $_ ] }
                   split( "\n", $maze->to_ascii() );

    my $crumb  = q{};
    my $color  = {
                  mazebg => '#ffc',
                  panel  => '#ccc',
                  crumb  => '#f3f',
                  sprite => 'orange',
		  button => '#ccf',
        	 };

    my $crumbstyle = $self->get_crumbstyle();

    $self->transform_grid( \@rows, $self->{wallform} );
    $self->_just_maze( \@rows );

    my ($xp, $yp) = $self->convert_start_position( @{$maze->{entry}} );
    my ($xe, $ye) = $self->convert_end_position( @{$maze->{exit}} );
    my ($xenter, $yenter) = $self->convert_sign_position( $xp, $yp );
    my ($xexit, $yexit) = $self->convert_sign_position( $xe, $ye );

    my $width = $self->{width} + 2 * SIDE_MARGIN;
    my $height = $self->{height} + 2 * SIGN_HEIGHT;
    my $sprite_def = $self->create_sprite();

    my $output = qq{<?xml version="1.0"?>\n} ;
    my $offsety = - SIGN_HEIGHT;
    my $offsetx = - SIDE_MARGIN;
    my ($xme, $yme) = ($xp*$self->dx(), $yp*$self->dy());
    my ($xcrumb, $ycrumb) = ($xme+$self->dx()/2, $yme+$self->dy()/2);

    my $panelheight = $height > PANEL_MIN_HEIGHT ? $height : PANEL_MIN_HEIGHT;
    if($self->{interactive})
    {
        my $script = $self->build_all_script();
	my $boardelem = $self->build_board_element(
	    \@rows, $xp, $yp, $xe, $ye
	);

        my $totalwidth = $width + PANEL_WIDTH;
        $output .= <<"EOH";
<svg width="$totalwidth" height="$panelheight"
     xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     xmlns:maze="http://www.anomaly.org/2005/maze"
     onload="initialize()">
  <title>A Playable SVG Maze</title>
  <desc>This maze was generated using the Games::Maze::SVG Perl
    module.</desc>
$license
  <defs>
     <style type="text/css">
	text { font-family: sans-serif; font-size: 10px; }
	.panel  { fill:$color->{panel}; stroke:none; }
	.button {
                   cursor: pointer;
        	}
	.button rect { fill: #33f; stroke: none; filter: url(#bevel);
                    }
	.button text { text-anchor:middle; fill:#fff; font-weight:bold; }
	.button polygon { fill:white; stroke:none; }
	.ctrllabel { text-anchor:middle; font-weight:bold; }
	#solvedmsg { text-anchor:middle; pointer-events:none; font-size:80px; fill:red;
                   }
     </style>
     <filter id="bevel">
       <feFlood flood-color="#ccf" result="lite-flood"/>
       <feFlood flood-color="#006" result="dark-flood"/>
       <feComposite operator="in" in="lite-flood" in2="SourceAlpha"
                    result="lighter"/>
       <feOffset in="lighter" result="lightedge" dx="-1" dy="-1"/>
       <feComposite operator="in" in="dark-flood" in2="SourceAlpha"
                    result="darker"/>
       <feOffset in="darker" result="darkedge" dx="1" dy="1"/>
       <feMerge>
         <feMergeNode in="lightedge"/>
         <feMergeNode in="darkedge"/>
         <feMergeNode in="SourceGraphic"/>
        </feMerge>
     </filter>
$script
$boardelem
  </defs>
  <svg x="@{[ PANEL_WIDTH ]}" y="0" width="$width" height="$height"
       viewBox="$offsetx $offsety $width $height" id="maze">
EOH
    }
    else
    {
	$color->{mazebg} = '#fff';

        $output .= <<"EOH";
<svg width="$width" height="$height"
     xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
  <title>An SVG Maze</title>
  <desc>This maze was generated using the Games::Maze::SVG Perl
    module.</desc>
$license
  <svg x="0" y="0" width="$width" height="$height"
       viewBox="$offsetx $offsety $width $height" id="maze">
EOH
    }

    $output .= <<"EOH";
    <defs>
      <style type="text/css">
	path    { stroke: black; fill: none; }
	polygon { stroke: black; fill: grey; }
	#sprite { stroke: grey; stroke-width:0.2px; fill: $color->{sprite}; }
	.crumbs { fill:none; $crumbstyle }
	.mazebg { fill:$color->{mazebg}; stroke:none; }
	text { font-family: sans-serif; font-size: 10px; }
	.sign text {  fill:#fff;text-anchor:middle; font-weight:bold; }
	.exit rect {  fill:red; stroke:none; }
	.entry rect {  fill:green; stroke:none; }
      </style>
      <circle id="savemark" r="3" fill="#6f6" stroke="none"/>
$sprite_def
@{[$self->wall_definitions()]}
    </defs>
    <rect id="mazebg" class="mazebg" x="$offsetx" y="$offsety" width="100%" height="100%"/>

$self->{mazeout}
    <polyline id="crumb" class="crumbs" stroke="$color->{crumb}" points="$xcrumb,$ycrumb"/>
    <use id="me" x="$xme" y="$yme" xlink:href="#sprite" visibility="hidden"/>

    <g transform="translate($xenter,$yenter)" class="entry sign">
      <rect x="-16" y="-8" width="35" height="16" rx="3" ry="3"/>
      <text x="2" y="4">Entry</text>
    </g>
    <g transform="translate($xexit,$yexit)" class="exit sign">
      <rect x="-16" y="-8" width="32" height="16" rx="3" ry="3"/>
      <text x="0" y="4">Exit</text>
    </g>
  </svg>
EOH

    if($self->{interactive})
    {
        my ($cx,$cy) = (($self->{width}+PANEL_WIDTH)/2, (35+$panelheight/2));
        $output .= $self->build_control_panel( 0, $panelheight );
	$output .= <<"EOM";
  <text id="solvedmsg" x="$cx" y="$cy" visibility="hidden">Solved!</text>
EOM
    }
    return $output . "</svg>\n";
}


sub make_board_array
{
    my $self = shift;
    my $rows = shift;
    my @board = ();

    foreach my $row (@{$rows})
    {
        push @board, [ map { $_ ? 1 : 0 } @{$row} ];
    }
    
    return \@board;
}


sub get_script_list
{
    my $self = shift;
    my @scripts = (
        "$self->{dir}point.es",
        "$self->{dir}sprite.es",
	"$self->{dir}maze.es",
	$self->get_script(),
    );

    return @scripts;
}


sub build_all_script
{
    my $self = shift;

    my $script = q{};
    
    foreach my $url ($self->get_script_list())
    {
        $script .= qq{    <script type="text/ecmascript" xlink:href="$url"/>\n};
    }

    $script .= <<"EOS";
    <script type="text/ecmascript">
      function push( evt )
      {
          var btn = evt.currentTarget;
          btn.setAttributeNS( null, "opacity", "0.5" );
      }
      function release( evt )
      {
          var btn = evt.currentTarget;
          var opval = btn.getAttributeNS( null, "opacity" );
          if("" != opval &amp;&amp; 1.0 != opval)
              btn.setAttributeNS( null, "opacity", '1.0' );
      }
    </script>
EOS

   return $script;
}



sub build_board_element
{
    my $self = shift;
    my $rows = shift;
    my ($xp, $yp, $xe, $ye) = @_;

    my $tilex = $self->dx();
    my $tiley = $self->dy();
    
    my $board = $self->make_board_array( $rows );

    my $elem .= qq{    <maze:board start="$xp,$yp" end="$xe,$ye" tile="$tilex,$tiley">\n};
    foreach my $row (@{$board})
    {
	$elem .= qq{      } . join( q{}, @{$row} ) ."\n";
    }
    $elem .= <<'EOS';
    </maze:board>
EOS

   return $elem;
}



sub build_control_panel
{
    my $self = shift;
    my $startx = shift;
    my $height = shift;
    my $panelwidth = PANEL_WIDTH;

    my $offset = 20;
    my $output .= <<"EOB";
  <g id="control_panel" transform="translate($startx,0)">
    <rect x="0" y="0" width="$panelwidth" height="$height"
          class="panel"/>
EOB
    $output .= _create_text_button( 'restart', $offset, 20, 50, 20, 'Begin' );
    $output .= _create_text_button(
        'save_position', $offset+60, 20, 50, 20, 'Save'
    );
    $output .= _create_text_button(
        'restore_position', $offset+120, 20, 50, 20, 'Back'
    );
    $output .= <<"EOB";

    <g transform="translate(20,65)">
      <rect x="-2" y="-2" rx="25" ry="25" width="68" height="68"
          fill="none" stroke-width="0.5" stroke="black"/>
      <text x="34" y="-5" class="ctrllabel">Move View</text>
EOB
    $output .= _create_view_button( 'maze_up',    22,  0, '10,5 5,15 15,15' );
    $output .= _create_view_button( 'maze_left',   0, 22, '5,10 15,5 15,15' );
    $output .= _create_view_button( 'maze_right', 44, 22, '15,10 5,5 5,15' );
    $output .= _create_view_button( 'maze_down',  22, 44, '10,15 5,5 15,5' );
    $output .= _create_view_button( 'maze_reset', 22, 22, '7,7 7,13 13,13 13,7' );

    $output .= <<"EOB";
    </g>

    <g class="instruct" transform="translate($offset,165)">
      <text x="0" y="0">Click Begin button to start</text>
      <text x="0" y="30">Use the arrow keys to move the sprite</text>
      <text x="0" y="50">Hold the shift to move quickly.</text>
      <text x="0" y="70">The mouse must remain over the</text>
      <text x="0" y="90">maze for the keys to work.</text>
      <text x="0" y="120">Use arrow buttons to shift the maze</text>
      <text x="0" y="140">Center button centers view on sprite</text>
      <text x="0" y="160">Save button saves current position</text>
      <text x="0" y="180">Back button restores last position</text>
    </g>
  </g>
EOB

    return $output;
}


# _create_text_button
#
#  $function - function name to call
#  $x - x-coordinate of the button
#  $y - y-coordinate of the button
#  $width - width of the button
#  $height - height of the button
#  $text - text to be displayed on the button

sub _create_text_button
{
    my $fn = shift;
    my $x = shift;
    my $y = shift;
    my $width = shift;
    my $height = shift;
    my $text = shift;

    my $tx = $width/2;
    my $ty = $height/2 + 5;

    my $output = <<"EOF";

    <g onclick="$fn()" transform="translate($x,$y)" class="button"
       onmousedown="push(evt)" onmouseup="release(evt)" onmouseout="release(evt)">
      <rect x="0" y="0" width="$width" height="$height" rx="5" ry="5"/>
      <text x="$tx" y="$ty">$text</text>
    </g>
EOF

    return $output;
}


# _create_view_button
#
#  $function - function name to call
#  $x - x-coordinate of the button
#  $y - y-coordinate of the button
#  $icon - list of points for the icon

sub _create_view_button
{
    my $fn = shift;
    my $x = shift;
    my $y = shift;
    my $icon = shift;

    my $output = <<"EOF";

      <g onclick="$fn()" transform="translate($x,$y)" class="button"
	 onmousedown="push(evt)" onmouseup="release(evt)" onmouseout="release(evt)">
	<rect x="0" y="0" width="20" height="20" rx="5" ry="5"/>
	<polygon points="$icon"/>
      </g>
EOF

    return $output;
}


sub  create_sprite
{
    my $self = shift;
    my ($dx2, $dy2) = ($self->dx()/2, $self->dy()/2);

    return qq|      | .
    qq|<path id="sprite" d="M0,0 Q$dx2,$dy2 0,@{[$self->dy()]} Q$dx2,$dy2 @{[$self->dx()]},@{[$self->dy()]} Q$dx2,$dy2 @{[$self->dx()]},0 Q$dx2,$dy2 0,0"/>|;
}


#
# Generates just the maze portion of the SVG.
#
# $dx - The size of the tiles in the X direction.
# $dy - The size of the tiles in the Y direction.
# $rows - Reference to an array of row data.
#
# returns a string containing the SVG for the maze description.
sub  _just_maze
{
    my $self = shift;
    my $dx   = $self->dx();
    my $dy   = $self->dy();
    my $rows = shift;

    my $output = q{};
    my ($maxx,$y) = (0,0);

    foreach my $r (@{$rows})
    {
        my $x = 0;
        foreach my $c (@{$r})
        {
            $output .= qq{    <use x="$x" y="$y" xlink:href="#$c"/>\n}
                if $c and $c ne q{$};
            $x += $dx;
        }
        $y += $dy;
        $maxx = $x if $maxx < $x;
    }

    $self->{width} = $maxx;
    $self->{height} = $y;
    $self->{mazeout} = $output;

    return $self;
}


sub dx
{
    my $self = shift;

    return $self->{dx};
}


sub dy
{
    my $self = shift;

    return $self->{dy};
}

1;