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
© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.