Javascript Puzzle - a better implementation
Yesterday I've come across a JS tutorial on how to create a javascript puzzle - Anatomy of JavaScript Puzzle. Although I liked the concept, I found the implementation extremely lacking, so I decided to write a better one.
So what's wrong with the original?
There are a few points that i found lacking, but the main ones were:
- It uses
document.write. I mean - common!document.write? That's probably one of the worst uses JS enables. - It uses
CSS clipto create the peices. By itself this isn't that bad, but the way he does it requiers way too much markup -div>div>imgfor every peice - The guide isn't complete - it ony gets to the point of populating the peices, but that's quite usesless without an interface to play with.
So here come my own implementation. This will also prove to be anice guide on using the Mootools Class.
Some theory
First let's outline what our Class will do:
- It will recieve an image.
- It will devide it to rows and columns.
- We are going to be using the
background-imageandbackground-positionattributes to position the image in the peices. - Shuffle the peices (will be using Mootools
Array.Extraso accomplish this). - Populate them.
- Provide a Drag interface to allow users to solve the puzzle (Mootoolas
Drag.Move).
The code
So let's get our hands dirty. First of all - some initial definitins for our Class:
var ImagePuzzle;
(function($,window,document,undefined){
ImagePuzzle = new Class({
Implements:Options
,options:{
rows : 10
, cols : 10
}
, element : null
, src : ''
, xSize : 0
, ySize : 0
, peices : []
});
})(document.id,this,this.document);;
A quick explanation - first, I'm using a closure to define some basic tools - the important parts are the $ function and the undefiened. The first is for cross-lib compatability, the second to make sure the Class won't be easy to break. The window and document are just good habits.
Second - the Class itself. For implementation - I almost always use Options. I've only defiened 2 options - rows and cols. I also make it a habit to define all class members before the methods. For our class we have:
elementthe puzzle container.srcthe image url.xSizethe width of each peice in the puzzle.ySizethe height of each peice.peicesthe pile of the puzzle's peices.
Initialize
The initialize method will set default settings, init the class members and call the class's methods. As a rule of thumb, the initialize method doesn't contain logic. Login will always exist as a seperate method. This will help with both refactring and extending the Class.
/**
* @param Element|string the puzzle board
* @param string a url to an image
* @param Object a list of extra options
*/
initialize : function(container,src,options){
this.setOptions(options);
this.element = $(container);
//fetch the image for dimentions calculations
var image = new Image();
this.src = image.src = src;
//calculate peice size
this.xSize = image.width / this.options.cols;
this.ySize = image.height / this.options.rows;
//set the puzzle board's size, with some margins to allow more room to move the peices around
this.element.setStyles({
height:image.height this.ySize * 2
, width:image.width this.xSize * 2
});
this.createPeices();
this.populatePeices();
}
Notice that I'm pre-fetching the image. This is needed for the calculations, but it will also cache it, so once we create our peices the browser will already have the image in store.
I've supplied the board with some extra space on it's margins. This will give the user some more space to move the peices around.
Creating the peices
createPeices : function(){
var style = { //define the style of all peices
width : this.xSize
, height : this.ySize
, position: 'absolute'
}
, i , j;
for (i=0;i<this.options.rows; i++){
for (j = 0; j<this.options.cols; j++){
// set the background position of the peice
style['background'] ='url('+ this.src+') no-repeat'
+' -' + Math.round(this.xSize * j,10) + 'px' //horizontal positioning
+' -' + Math.round(this.ySize * i,10) + 'px'; //vertical positioning
//create the peice and populate it in the matrix
this.peices.push( new Element('div').setStyles(style) );
}
}
}
Few notes - first - we create an object containing the style to be used for all peices. Next, we populate the peices matrix with peices. We are using the background CSS attribute to set the image and position it.
Notice that we are using negative values for the positon. This is because the position is calculated from the top left, moving to the bottom right. Sience our peices are ordered from left to right and top to bottm, we need the image to move on the oposite direction.
Populating the peices
populatePeices : function(){
var zindex=0
, l=0
,i,j;
this.peices.shuffle();
//this function wil make sure that when a peice is picked up it will become the topmost peice
function dragStart(el){
$(el).setStyle('z-index',++zindex);
}
for (i=0;i<this.options.rows;i++){
for (j=0;j<this.options.cols;j++){
//position the current piece
this.peices[l++].setStyles({
'top' : (i+1) * this.ySize
, 'left' : (j+1) * this.xSize
})
//apply Drag.Move
.makeDraggable({
droppables : this.element //make the peices droppable only on our board
, onStart : dragStart
, grid: (this.xSize == this.ySize) ? this.xSize : false //if image is square, apply snap grid to make interface easier to manage
});
}
}
this.element.adopt(this.peices); //populate the peices
}
There's a lot going on so lets go through it step by step:
- First we shuffle the rows, then we go on each row and shuffle it's columns.
- Next, we position our peice.
- Finaly, we make our peice draggable. This is done using Mootools'
Drag.MoveClass. I've made sure the peices can only be dropped on our board. - I've also added a
onStartevent. What it will do is to increase the zindex of the pickedup peice so that it will always be on top. - As for the grid -
Dragsupplies a neat trick that allows snapping to grid. It's problem is that it can only create square grids, so we can only use it for square images. This is a shame as it make the interface much easier to use. - And offcourse - we finaly populate the peices on the board.
An interface
I've created a very simple interface for the demo. You can find it here. It's quite simple - it recieves an image url, and a number of rows and columns, and creates a puzzle out of it.
Links
- The original peice.
- The complete class code.
- Drag.Move and Drag documentation on the MT site.
- Class Demo.