img2puzzle.pl
Photo Puzzle: image dicer with HTML page output for playing the game
  1. Begin by examining the image below whose reconstruction will be the objective of the puzzle.
  2. Press the Scramble button in order to mix up the tiles randomly.
  3. Re-assemble the tiles in their correct order by relocating the tiles in pairs. This is done by first single-clicking on a tile. Then, after single-clicking on any second tile, the two tiles will swap their positions. If you need some help, you can click on the Show/Hide Hint button to display the original image. Moreover, the Show/Hide Errors button will indicate the misplaced tiles.
  4. When all the tiles are back in their original locations, you will be notified.
  5. Finally, if you press the I Give Up! button, the misplaced tiles will be relocated to their correct position.
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X
X

I recently read a blog entry extolling a site allowing a user to create and play a photo puzzle. Such puzzles have been around for awhile. In the late 1990s, I use to have one on my site. But, with only HTML 2 at hand, it was a tad primitive compared to what is possible today. So I decided to revisit my code and upgrade it to dice an image and generate the required Cascading Style Sheets and JavaScript to control play. The above is an example of what can be obtained. It's a fully functional puzzle. Give it a try!

The required task sequence is straighforward. In accordance with specified width and height dimensions for the tiles, dice a scaled version of a given image into a collection of tile images. Then create a basic HTML page by filling in a template with the image and tile parameters at hand. The template must contain all the generic CSS declarations and JavaScript code required to control the play. The resulting page elements can then be copied and pasted into a user's presentation, as was done for the above puzzle. It is left to the user to modify the CSS definitions to match the style of their pages.

As I did for my mosaic filter, I've turned to Perl with ImageMagick's drawing primitives accessed through its PerlMagick interface. The script img2puzzle.pl, displayed below, accomplishes the objective. It is released for personal, non-commercial and non-profit use only. The listing includes the line numbers in order to reference them in the following general remarks.

The relations between the page events and the JavaScript functions can be illustrated as follows:

Anyway, if you have any questions regarding the code or my explanations, please do not hesitate in contacting me.


img2puzzle.pl -- [Download latest version: v1.0.2 - February 17, 2008]   [MD5 checksum]
001 use strict;
002 use File::Basename;
003 use Image::Magick;
004 #===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
005 # img2puzzle.pl: Puzzle image dicer with HTML page for playing the puzzle.
006 #===============================================================================================================================
007 #           Usage : perl img2puzzle.pl ImageFile TileWidth TileHeight Verbose
008 #       Arguments : ImageFile  = path of image file to be diced.
009 #                   TileWidth  = width of a tile in pixels
010 #                   TileHeight = height of a tile in pixels
011 #                   Verbose    = boolean flag for verbose output
012 #     Input Files : See arguments.
013 #    Output Files : Tile files in the form "basename_tile_nnn.gif" where "basename" denotes the basename of the
014 #                     source image file and "nnn" denotes the tile index number.
015 #                   Image GIF file in the form "basename_puzzle.gif" which is a scaled version of the source image.
016 #                   HTML page for playing the puzzle in the form "basename_puzzle.htm".
017 # Temporary Files : None.
018 #         Remarks : Requires PerlMagick. Used module from ImageMagick 6.3.7.
019 #         History : v1.0.0 - February 4, 2008 - Original release.
020 #===============================================================================================================================
021 # 0) INITIALIZE:
022 system( 'cls' ) if $^O =~ /^MSWin/;                                                         #clear screen if running Windows
023 print "$0\n", '=' x length($0), "\n\n";                                                     #display program name
024
025 my $SourceFile      = shift || die "ERROR: No image file specified\n";                      #get path of source image file
026 my $TileWidth       = shift || die "ERROR: No tile width specified\n";                      #get pixel width of a tile
027 my $TileHeight      = shift || die "ERROR: No tile height specified\n";                     #get pixel height of a tile
028 my $Verbose         = shift;                                                                #get boolean flag for verbose mode
029 die "ERROR: Cannot locate image file\n" unless -e $SourceFile;
030
031 my ( $basename,                                                                             #extract basename &
032      $path                                                                                  #extract path
033    )                = fileparse( $SourceFile, '\..*' );                                     # of source image file
034 my $TileBaseName    = "${basename}_tile_";                                                  #compose basename of all tile files
035 my $HtmlFile        = "$path${basename}_puzzle.htm";                                        #compose filename of HTML page
036 my $PuzzleFile      = "$path${basename}_puzzle.gif";                                        #compose filename of puzzle image
037 my $PuzzleTable;                                                                            #body of the puzzle table
038 local *HTML;                                                                                #define filehandle for the HTML page
039 #-------------------------------------------------------------------------------------------------------------------------------
040 # 1) SETUP PUZZLE IMAGE:
041 my $source          = Image::Magick->new;                                                   #instantiate an image object for the source
042 my ( $SourceWidth,                                                                          #get width &
043      $SourceHeight                                                                          #get height
044    )                = ( $source->Ping( $SourceFile ) )[0,1];                                #of source
045 undef $source;                                                                              #destroy the image object for the source
046
047 my $No_Tile_Cols    = $SourceWidth / $TileWidth;                                            #compute raw number of tile columns
048    $No_Tile_Cols    = int( ++$No_Tile_Cols )                                                #adjust to an integral number
049                        unless $No_Tile_Cols == int( $No_Tile_Cols );                        # if necessary
050 my $No_Tile_Rows    = $SourceHeight / $TileHeight;                                          #compute raw number of tile rows
051    $No_Tile_Rows    = int( ++$No_Tile_Rows )                                                #adjust to an integral number
052                        unless $No_Tile_Rows == int( $No_Tile_Rows );                        # if necessary
053 my $TileCount       = $No_Tile_Cols * $No_Tile_Rows;                                        #compute total number of tiles
054
055 my $PuzzleWidth     = $No_Tile_Cols * $TileWidth;                                           #compute pixel width of puzzle image
056 my $PuzzleHeight    = $No_Tile_Rows * $TileHeight;                                          #compute pixel height of puzzle image
057 my $Puzzle          = Image::Magick->new( magick=>"GIF" );                                  #instantiate an image object for the puzzle
058    $Puzzle->Read( $SourceFile );                                                            #init puzzle with source image file
059    $Puzzle->Scale( width=>$PuzzleWidth, height=>$PuzzleHeight );                            #scale puzzle image
060    $Puzzle->Write( filename => $PuzzleFile );                                               #save the puzzle image to file
061
062 print "Source Image:   $SourceFile\n",                                                      #echo initialization results
063       "  Width:        $SourceWidth px\n",
064       "  Height:       $SourceHeight px\n",
065       "Puzzle Image:   $PuzzleFile\n",
066       "  Width:        $PuzzleWidth px\n",
067       "  Height:       $PuzzleHeight px\n",
068       "HTML File:      $HtmlFile\n\n",
069       "Puzzle Details: $TileCount tiles\n",
070       "  No. of rows:  $No_Tile_Rows\n",
071       "  No. of cols:  $No_Tile_Cols\n",
072       "  Tile width:   $TileWidth px\n",
073       "  Tile height:  $TileHeight px\n\n";
074 print( "Pause. Press the ENTER key to continue..." ), <STDIN> if $Verbose;
075 #-------------------------------------------------------------------------------------------------------------------------------
076 # 2) CREATE TILE IMAGES & PUZZLE TABLE:
077 {                                                                                           #start naked block
078     my $tile            = Image::Magick->new( magick=>"GIF" );                              # instantiate an image object for a tile
079     my $tileIdx         = 0;                                                                # tile index
080     my $tileFile;                                                                           # tile file
081     my $geometry;                                                                           # geometry of a tile: width, height & x,y offsets
082     my $x_top_left;                                                                         # x-coordinate of a tile's top-left corner
083     my $y_top_left;                                                                         # y-coordinate of a tile's top-left corner
084     my $x_bottom_right;                                                                     # x-coordinate of a tile's bottom-right corner
085     my $y_bottom_right;                                                                     # y-coordinate of a tile's bottom-right corner
086
087     print "Creating tile images...\n";
088     for ( $y_top_left  = 0;                                                                 # for each tile row: start at top and work down
089           $y_top_left <= $PuzzleHeight - $TileHeight;
090           $y_top_left += $TileHeight
091         ) {
092             $PuzzleTable    .= "<tr>\n";                                                    #  init puzzle table row
093             $y_bottom_right = $y_top_left + $TileHeight - 1;                                #  compute y-coord. of the tile's bottom-right corner
094             for ( $x_top_left = 0;                                                          #  for each tile column: work left to right
095                   $x_top_left < $PuzzleWidth - 1;
096                   $x_top_left += $TileWidth
097                 ) {
098                     $tileFile        =  "${TileBaseName}${tileIdx}.gif";                    #   compose path of tile file
099                     $PuzzleTable    .=  qq|<td>\n| .                                        #   compose cell content: tile, hilight & error layers
100                                         qq|<div class="puzzle_cell" onClick="puzzle_select($tileIdx);">\n| .
101                                         qq| <img class="puzzle_tile"   name="tile$tileIdx" src="$tileFile" alt="">\n| .
102                                         qq| <div class="puzzle_hilite" id="hilite$tileIdx"></div>\n| .
103                                         qq| <div class="puzzle_error"  id="error$tileIdx">X</div>\n| .
104                                         qq|</div>\n| .
105                                         qq|</td>\n|;
106                     $x_bottom_right  = $x_top_left + $TileWidth - 1;                        #   compute x-coord. of the tile's bottom-right corner
107                     print "\tTile# $tileIdx \@ $x_top_left,$y_top_left-$x_bottom_right,$y_bottom_right "
108                      if $Verbose;                                                           #   report progress if requested
109
110                     $geometry   = "${TileWidth}x${TileHeight}+$x_top_left+$y_top_left";     #   define tile geometry
111                     $tile       = $Puzzle->Clone();                                         #   init tile to full puzzle image
112                     $tile->Crop( geometry => $geometry );                                   #   crop tile area
113                     $tile->Set( page => '0x0+0+0' );                                        #   shrink canvas
114                     $tile->Write( filename => $tileFile );                                  #   save the tile image to file
115
116                     print "=> $tileFile\n" if $Verbose;                                     #   report progress if requested
117                     ++$tileIdx;                                                             #   update tile index
118             }                                                                               #  until all tile columns processed
119             $PuzzleTable .= qq|</tr>\n|;                                                    #  end table row
120     }                                                                                       # until all tile rows processed
121     undef $Puzzle;                                                                          # destroy the puzzle image object
122     undef $tile;                                                                            # destroy the tile image object
123 }                                                                                           #end naked block
124 #-------------------------------------------------------------------------------------------------------------------------------
125 # 3) OUTPUT THE PUZZLE PAGE:
126 my $min             = sub { my( $x, $y ) = @_; return ( $x < $y ) ? $x : $y; };             #anonymous sub: returns the minimum of 2 numbers
127 my $BorderWidth     = 5;                                                                    #border width of puzzle board
128 my $IE_Width        = $PuzzleWidth  + 2 * $BorderWidth;                                     #outer board width for IE
129 my $IE_Height       = $PuzzleHeight + 2 * $BorderWidth;                                     #outer board height for IE
130 my $ErrorFontSize   = int( 0.90 * &$min( $TileWidth, $TileHeight ) );                       #define font size for indicating incorrect tile
131
132 open HTML, ">$HtmlFile" or die "ERROR: Cannot create HTML file: $!\n";                      #open HTML file for output
133 print HTML <<PUZZLE_PAGE;                                                                   #output filled-in template
134 <html>
135 <head>
136     <title>Photo Puzzle Page</title>
137     <style type="text/css">
138         /* Puzzle board consists of 3 main layers: instructions, hint & tiles */
139         DIV.puzzle_board {
140             position:           relative;
141             top:                0px;
142             left:               0px;
143             border:             ${BorderWidth}px outset darkgray;
144             width:              ${PuzzleWidth}px;
145             height:             ${PuzzleHeight}px;
146         }
147         /* Puzzle board instructions layer */
148         DIV.puzzle_board DIV#puzzle_howto {
149             position:           absolute;
150             top:                0px;
151             left:               0px;
152             width:              ${PuzzleWidth}px;
153             height:             ${PuzzleHeight}px;
154             background-color:   silver;
155             text-align:         left;
156             z-index:            1;
157         }
158         /* Puzzle board hint layer */
159         DIV.puzzle_board IMG#puzzle_hint {
160             position:           absolute;
161             top:                0px;
162             left:               0px;
163             width:              ${PuzzleWidth}px;
164             height:             ${PuzzleHeight}px;
165             z-index:            2;
166         }
167         /* Puzzle board tiles layer */
168         DIV.puzzle_board TABLE#puzzle_table {
169             position:           absolute;
170             top:                0px;
171             left:               0px;
172             border:             0;
173             border-collapse:    collapse;
174             width:              ${PuzzleWidth}px;
175             height:             ${PuzzleHeight}px;
176             z-index:            3;
177         }
178         /* Puzzle board tile cells */
179         DIV.puzzle_board TABLE#puzzle_table TD {
180             border:             0;
181             padding:            0;
182             width:              ${TileWidth}px;
183             height:             ${TileHeight}px;
184         }
185
186         /* Puzzle tile cells consists of 3 layers: tile, hilight & error marker */
187         DIV.puzzle_cell {
188             position:           relative;
189             top:                0px;
190             left:               0px;
191             width:              ${TileWidth}px;
192             height:             ${TileHeight}px;
193         }
194         /* Puzzle tile layer */
195         DIV.puzzle_cell IMG.puzzle_tile {
196             position:           absolute;
197             top:                0px;
198             left:               0px;
199             width:              ${TileWidth}px;
200             height:             ${TileHeight}px;
201             z-index:            1;
202         }
203         /* Tile-hilight layer */
204         DIV.puzzle_cell DIV.puzzle_hilite {
205             position:           absolute;
206             top:                0px;
207             left:               0px;
208             background-color:   red;
209             opacity:            0.2;
210             filter:             alpha(opacity=20);
211             width:              ${TileWidth}px;
212             height:             ${TileHeight}px;
213             visibility:         hidden;
214             z-index:            2;
215         }
216         /* Incorrect-tile marker layer */
217         DIV.puzzle_cell DIV.puzzle_error {
218             position:           absolute;
219             top:                0px;
220             left:               0px;
221             color:              red;
222             font-family:        arial,helvetica,sans-serif;
223             font-size:          ${ErrorFontSize}px;
224             font-weight:        bold;
225             width:              ${TileWidth}px;
226             height:             ${TileHeight}px;
227             text-align:         center;
228             visibility:         hidden;
229             z-index:            3;
230         }
231     </style>
232     <!--[if IE]>
233     <style type="text/css">
234         /* Override for puzzle board dimensions */
235         DIV.puzzle_board {
236             width:              ${IE_Width}px;
237             height:             ${IE_Height}px;
238         }
239     </style>
240     <![endif]-->
241     <script language="Javascript">
242         var ButtonIds               = new Array( 'button_howto', 'button_scramble', 'button_hint', 'button_errors', 'button_solve' );
243         var FirstTile               = null;         /* Index of first of two tiles selected */
244         var FirstElemHilite;                        /* Hilight element corresponding to FirstTile */
245         var Status_ErrorsToggled    = false;        /* Boolean flag indicating if error markers displayed */
246         var Status_Scrambled        = false;        /* Boolean flag indicating if tiles have been scrambled */
247         var Status_Solving          = false;        /* Boolean flag indicating if puzzle solving in progress */
248         var TileCount               = $TileCount;   /* Total number of tiles */
249
250         /* Returns Boolean for whether a tile's image file agrees wih the tile's index or not */
251         function puzzle_concordance( tileIdx ) {
252             var imgSrc  = document['tile' + tileIdx.toString()].src;
253             var regex   = new RegExp( '_' + tileIdx.toString() + '\\.gif\$' );
254             return regex.test(imgSrc);
255         }
256         /* Checks if the puzzle has been solved */
257         function puzzle_check() {
258             for( var tileIdx = 0; tileIdx < TileCount; ++tileIdx ) {
259                 if( !puzzle_concordance( tileIdx ) ) return;
260             }
261             alert("\\nBravo! Well done!");
262             Status_Scrambled = false;
263             puzzle_toggleButtons( new Array(1,2,3,4) );
264         }
265         /* Controls the selection of tile pairs */
266         function puzzle_select( tileIdx ) {
267             if( !Status_Scrambled || Status_ErrorsToggled || Status_Solving ) return;
268             var selectedTile = 'tile' + tileIdx.toString();
269             if( !FirstTile ) {
270                 FirstTile                           = selectedTile;
271                 FirstElemHilite                     = document.getElementById( 'hilite' + tileIdx.toString() );
272                 FirstElemHilite.style.visibility    = 'visible';
273             } else {
274                 puzzle_swap( FirstTile, selectedTile );
275                 FirstElemHilite.style.visibility    = 'hidden';
276                 FirstTile                           = null;
277                 puzzle_check();
278             }
279         }
280         /* Randomly scrambles the tiles */
281         function puzzle_scramble() {
282             var targetName;
283             var sourceName;
284             for( var tileIdx = TileCount-1; tileIdx > 0; --tileIdx ) {
285                 targetName = 'tile' + tileIdx.toString();
286                 sourceName = 'tile' + ( Math.floor( tileIdx * Math.random() ) ).toString();
287                 puzzle_swap( targetName, sourceName );
288             }
289             Status_Scrambled    = 1;
290             puzzle_toggleButtons( new Array(1,2,3,4) );
291         }
292         /* Solves the puzzle by tile swapping every 75 milliseconds */
293         function puzzle_solve() {
294             if( confirm("\\nAre you sure?") ) {
295                 Status_Solving = true;
296                 if( FirstTile ) FirstElemHilite.style.visibility = 'hidden';
297                 puzzle_toggleButtons( new Array(0,2,3,4) );
298                 for( var imgIdx = 0; imgIdx < TileCount-1; ++imgIdx ) {
299                     setTimeout( eval( "\\"puzzle_correct( " + imgIdx + " )\\"" ), 75*imgIdx );
300                 }
301             }
302         }
303         /* Locates and swaps in the correct image for a specified tile */
304         function puzzle_correct( imgIdx ) {
305             var imgSrc;
306             var regex       = new RegExp('_' + imgIdx +'\\.gif\$');
307             var sourceName;
308             for( var tileIdx = imgIdx; tileIdx < TileCount; ++tileIdx ) {
309                 sourceName  = 'tile' + tileIdx.toString();
310                 imgSrc      = document[sourceName].src;
311                 if( regex.test(imgSrc) ) break;
312             }
313             puzzle_swap( 'tile' + imgIdx.toString(), sourceName );
314             if( imgIdx == TileCount - 2 ) {
315                 Status_Scrambled    = false;
316                 Status_Solving      = false;
317                 FirstTile           = null;
318                 puzzle_toggleButtons( new Array(0,1) );
319             }
320         }
321         /* Swaps images between the specified tile pair */
322         function puzzle_swap( tile1Name, tile2Name ) {
323             var temp                = document[tile1Name].src;
324             document[tile1Name].src = document[tile2Name].src;
325             document[tile2Name].src = temp;
326         }
327         /* Toggles the disabled state for the specified button indexes */
328         function puzzle_toggleButtons( Idxs ) {
329             var elem_button;
330             for( var i = 0; i < Idxs.length; ++i ) {
331                 elem_button             = document.getElementById( ButtonIds[Idxs[i]] );
332                 elem_button.disabled    = !elem_button.disabled;
333             }
334         }
335         /* Toggles the puzzle-image display by changing its layer index */
336         function puzzle_toggleHint() {
337             puzzle_toggleButtons( new Array(0,3,4) );
338             document.puzzle_image.style.zIndex = (document.puzzle_image.style.zIndex == 4) ? 2 : 4;
339         }
340         /* Toggles the instructions display by changing its layer index */
341         function puzzle_toggleHowTo() {
342             puzzle_toggleButtons( (Status_Scrambled) ? new Array(2,3,4) : new Array('1') );
343             var elem_howto          = document.getElementById( 'puzzle_howto' );
344             elem_howto.style.zIndex = (elem_howto.style.zIndex == 4) ? 1 : 4;
345         }
346         /* Toggles the display of the markers for misplaced tiles */
347         function puzzle_toggleErrors() {
348             puzzle_toggleButtons( new Array(0,2,4) );
349             var elem_error;
350             if( !Status_ErrorsToggled ) {
351                 for( var tileIdx = 0; tileIdx < TileCount; ++tileIdx ) {
352                     if( !puzzle_concordance( tileIdx ) ) {
353                         elem_error                  = document.getElementById( 'error' + tileIdx.toString() );
354                         elem_error.style.visibility = 'visible';
355                     }
356                 }
357             } else {
358                 for( var tileIdx = 0; tileIdx < TileCount; ++tileIdx ) {
359                     elem_error                  = document.getElementById( 'error' + tileIdx.toString() );
360                     elem_error.style.visibility = 'hidden';
361                 }
362             }
363             Status_ErrorsToggled = !Status_ErrorsToggled;
364         }
365     </script>
366 </head>
367 <body>
368 <div align="center">
369     <form>
370         <input type="button" id="button_howto"    value="Show/Hide Instructions" onClick="puzzle_toggleHowTo();">
371         <input type="button" id="button_scramble" value="Scramble"               onClick="puzzle_scramble();">
372         <input type="button" id="button_hint"     value="Show/Hide Hint"         onClick="puzzle_toggleHint();"   disabled>
373         <input type="button" id="button_errors"   value="Show/Hide Errors"       onClick="puzzle_toggleErrors();" disabled>
374         <input type="button" id="button_solve"    value="I Give Up!"             onClick="puzzle_solve();"        disabled>
375     </form>
376     <div class="puzzle_board">
377         <div id="puzzle_howto">
378             <ol>
379                 <li>Begin by examining the image below whose reconstruction will be the objective of the puzzle.
380                 <li>Press the <b><i>Scramble</i></b> button in order to mix up the tiles randomly.
381                 <li>Re-assemble the tiles in their correct order by relocating the tiles in pairs. This is
382                     done by first single-clicking on a tile. Then, after single-clicking on any second tile,
383                     the two tiles will swap their positions. If you need some help, you can click on the
384                     <b><i>Show/Hide Hint</i></b> button to display the original image.
385                     Moreover, the <b><i>Show/Hide Errors</i></b> button will indicate the misplaced tiles.
386                 <li>When all the tiles are back in their original locations, you will be notified.
387                 <li>Finally, if you press the <b><i>I Give Up!</i></b> button, the misplaced tiles will be
388                     relocated to their correct position.
389             </ol>
390         </div>
391         <img id="puzzle_hint" name="puzzle_image" src="$PuzzleFile">
392         <table id="puzzle_table">
393             $PuzzleTable
394         </table>
395     </div>
396 </div>
397 </body>
398 </html>
399 PUZZLE_PAGE
400 close HTML;                                                                                 #close HTML file
401 exit;
402 #===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
403 # end of img2puzzle.pl
    

© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.