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.
perl img2puzzle.pl eyp107.jpg 120 100
Note that the "Verbose" parameter can be omitted or set to "0" to suppress the computational results for each tile. If set, say "1", then the details will be displayed. The following line is a typical example:Tile# 0 @ 0,0-119,99 => .\eyp107_tile_0.gif
The tile number follows an integer sequence, starting at 0. The corresponding image coordinates are stated next with the top right corner (0,0) followed by the bottom left corner (119,99). Finally, the path of the created tile file is printed.eyp107_tile_0.gif, eyp107_tile_1.gif, ..., eyp107_tile_41.gif
|
+ | + | X
|
= |
X
|
|
| z-index:1 | z-index:2 visibility:hidden |
z-index:3 visibility:hidden |
|
+ | + | X
|
= |
X
|
|
| z-index:1 | z-index:2 visibility:visible |
z-index:3 visibility:hidden |
|
+ | + | X
|
= |
X
|
|
| z-index:1 | z-index:2 visibility:hidden |
z-index:3 visibility:visible |
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.
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
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.