function GameOfLife( element, width, height ) {
    //
    // general initialisation
    //
    var _this           = this;
    this.intervalId     = null;
    this.mouseIsDown    = false;
    this.selectedI      = -1;
    this.selectedJ      = -1;
    this.fillColour     = "#FFF";
    this.clearColour    = "#000";
    //
    // initialise rendering context
    //
    this.canvas     = document.getElementById( element );
    this.context    = this.canvas.getContext( '2d' );
    this.width      = window.innerWidth;
    this.height     = window.innerHeight;
    //
    // initialise event handling
    //
    document.addEventListener('mousedown', function(e) { _this.onmousedown(e); }, false);
    document.addEventListener('mouseup', function(e) { _this.onmouseup(e); }, false);
    document.addEventListener('mousemove', function(e) { _this.onmousemove(e); }, false);
    window.addEventListener('resize', function() { _this.resize(); }, false);
    //
    // initialise simulation
    //
    this.cells = new Array();
    this.cells.width    = width;
    this.cells.height   = height;
    this.cells.current  = 0;
    for ( var i = 0; i < 2; i++ ) {
        this.cells[i] = new Array();
        for ( var j = 0; j < this.cells.height; j++ ) {
            for ( var k = 0; k < this.cells.width; k++ ) {
                this.cells[i][(j*this.cells.width)+k] = i;
            }
        }
        
    }
};
//
// accessors
//
GameOfLife.prototype.setFillColour = function( colour ) {
    this.fillColour = colour;
};
GameOfLife.prototype.setClearColour = function( colour ) {
    this.clearColour = colour;
};
//
// simulation and rendering
//
GameOfLife.prototype.start = function() {
	var _this = this;

	if (this.intervalId !== null) {
		return; // already running
	}
    this.intervalId = setInterval( function() {
        //
        // check size
        //
        _this.checkSize();
        //
        // clear
        //
        //_this.clear();
        //
        // update
        //
        _this.update();
        //
        // render
        //
        _this.render();
    }, 50 );
};
GameOfLife.prototype.stop = function() {
	if (this.intervalId !== null) {
		clearInterval(this.intervalId);
	}
};
GameOfLife.prototype.checkSize = function() {
    //
    // check for resize
    //
    var redraw = false;
    if ( this.canvas.width != this.width ) {
        this.canvas.width = this.width;
        redraw = true;
    }
    if ( this.canvas.height != this.height ) {
        this.canvas.height = this.height;
        redraw = true;
    }
    if ( redraw ) {
        this.clear();
    }
};
GameOfLife.prototype.clear = function() {
    this.context.fillStyle = this.clearColour;
    this.context.fillRect(0,0,this.canvas.width,this.canvas.height);
};
GameOfLife.prototype.update = function() {
    if ( !this.mouseIsDown ) {
        
        var current = this.cells[ this.cells.current ];
        var next = this.cells[ this.cells.current == 0 ? 1 : 0 ];
        for ( var i = 0; i < this.cells.height; i++ ) {
            for ( var j = 0; j < this.cells.width; j++ ) {
                //
                // sum neighbours
                //
                /*
                var val = 0;
                var kMin = Math.max( 0, i - 1 );
                var kMax = Math.min( this.cells.height - 1, i + 1 );
                var lMin = Math.max( 0, j - 1 );
                var lMax = Math.min( this.cells.width - 1, j + 1 );
                for ( var k = kMin; k <= kMax; k++ ) {
                    for ( var l = lMin; l <= lMax; l++ ) {
                        if ( k != i || l != j ) {
                            val += current[ (k*this.cells.width)+l ];
                        }
                    }
                }
                */
                //
                // sum neighbours ( wrapping grid )
                //
                var i0 = i > 0 ? i - 1 : this.cells.height + ( i - 1 );
                var i1 = i < this.cells.height - 1 ? i + 1 : ( i + 1 ) - this.cells.height;
                var j0 = j > 0 ? j - 1 : this.cells.width + ( j - 1 );
                var j1 = j < this.cells.width - 1 ? j + 1 : ( j + 1 ) - this .cells.width;
                var val = current[ ( i0 * this.cells.width ) + j0 ]; 
                val += current[ ( i0 * this.cells.width ) + j ]; 
                val += current[ ( i0 * this.cells.width ) + j1 ]; 
                val += current[ ( i * this.cells.width ) + j0 ]; 
                val += current[ ( i * this.cells.width ) + j1 ]; 
                val += current[ ( i1 * this.cells.width ) + j0 ]; 
                val += current[ ( i1 * this.cells.width ) + j ]; 
                val += current[ ( i1 * this.cells.width ) + j1 ]; 
                //
                // update cell
                //
                var index = (i*this.cells.width)+j;                
                next[ index ] = ( current[ index ] == 1 && ( val == 2 || val == 3 ) ) || ( current[ index ] == 0 && val == 3 ) ? 1 : 0;
            }
        }
        this.cells.current = this.cells.current == 0 ? 1 : 0;
    }
};
GameOfLife.prototype.render = function() {
    //
    // render cells
    //
    var dim = Math.ceil( this.canvas.width / this.cells.width );
    var y = 0;
    this.context.strokeStyle = this.fillColour;
    this.context.lineWidth   = 1;
    var previous = this.cells[ this.cells.current == 0 ? 1 : 0 ];
    var current = this.cells[ this.cells.current ];
    for ( var i = 0; i < this.cells.height && y < this.canvas.height; i++ ) {
        var x = 0;
        for ( var j = 0; j < this.cells.width && x < this.canvas.width; j++ ) {
            //
            // draw cell
            //
            var index = (i*this.cells.width)+j;
            if ( previous[ index ] != current[ index ] ) {
                this.context.fillStyle = current[ index ] == 0 ? this.clearColour : this.fillColour;
                this.context.fillRect(x, y, dim, dim);
            }
            //
            // draw hilite
            //
            /*
            if ( i == this.selectedI && j == this.selectedJ ) {
                this.context.strokeRect(x, y, dim, dim);
            }
            */
            x+=dim;
        }
        y+=dim;
    }
};
//
// event handling
//
GameOfLife.prototype.onmousedown = function(e) {
    this.mouseIsDown = true;
    this.toggleCellAt( e.clientX, e.clientY );
}
GameOfLife.prototype.onmouseup = function(e) {
    this.mouseIsDown = false;
}
GameOfLife.prototype.onmousemove = function(e) {
    if ( this.mouseIsDown ) {
        this.setCellAt( e.clientX, e.clientY, 1 );
    }
    var dim = this.canvas.width / this.cells.width;
    this.selectedI = Math.floor( e.clientY / dim );
    this.selectedJ = Math.floor( e.clientX / dim );
}
GameOfLife.prototype.resize = function() {
    this.width = window.innerWidth;
    this.height = window.innerHeight;
}
//
// simulation control
//
GameOfLife.prototype.toggleCellAt = function( x, y ) {
    var dim = Math.ceil( this.canvas.width / this.cells.width );
    var i   = Math.floor( y / dim );
    var j   = Math.floor( x / dim );
    var current = this.cells[ this.cells.current ];
    var index = (i*this.cells.width)+j;
    current[ index ] = current[ index ] == 0 ? 1 : 0;
}
GameOfLife.prototype.setCellAt = function( x, y, n ) {
    var dim = Math.ceil( this.canvas.width / this.cells.width );
    var i   = Math.floor( y / dim );
    var j   = Math.floor( x / dim );
    var current = this.cells[ this.cells.current ];
    var index = (i*this.cells.width)+j;
    current[ index ] = n;
}

GameOfLife.prototype.setCellAtIndex = function( i, j, n ) {
    while ( i >= this.cells.height ) i -= this.cells.height;
    while ( i < 0 ) i += this.cells.height;
    while ( j >= this.cells.width ) j -= this.cells.width;
    while ( j < 0 ) j += this.cells.width;
    var current = this.cells[ this.cells.current ];
    var index = (i*this.cells.width)+j;
    current[ index ] = n;
}

GameOfLife.prototype.seedRandom = function( count ) {
    for ( var k = 0; k < count; k++ ) {
        var selector = this.rangeRandom( 0, 70 );
        var i = this.rangeRandom( 0, this.cells.height );
        var j = this.rangeRandom( 0, this.cells.width );
        if ( selector < 10 ) {
            this.block(i, j);
        } else if ( selector < 20 ) {
            this.glider(i, j);
        } else if ( selector < 30 ) {
            this.gliderGun(i, j);
        } else if ( selector < 40 ) {
            this.blinker(i, j);
        } else if ( selector < 50 ) {
            this.peminto(i, j);
        } else if ( selector < 60 ) {
            this.diehard(i, j);
        } else {
            this.acorn(i, j);
        }
    }
}
GameOfLife.prototype.rangeRandom = function( min, max ) {
    return Math.floor(( Math.random() * ( max - min ) ) + min );
}
GameOfLife.prototype.block = function(i,j) {
    for ( var k = i; k < i + 1; k++ ) {
        for ( var l = j; l < j + 1; l++ ) {
            this.setCellAtIndex( k, l, 1 );
        }
    }
}
GameOfLife.prototype.glider = function(i, j) {
    this.setCellAtIndex( i, j+1, 1 );
    this.setCellAtIndex( i+1, j+2, 1 );
    this.setCellAtIndex( i+2, j, 1 );
    this.setCellAtIndex( i+2, j+1, 1 );
    this.setCellAtIndex( i+2, j+2, 1 );
}
GameOfLife.prototype.gliderGun = function(i, j) {
    this.setCellAtIndex( i, j+24, 1 );
    this.setCellAtIndex( i+1, j+22, 1 );
    this.setCellAtIndex( i+1, j+24, 1 );
    this.setCellAtIndex( i+2, j+12, 1 );
    this.setCellAtIndex( i+2, j+13, 1 );
    this.setCellAtIndex( i+2, j+20, 1 );
    this.setCellAtIndex( i+2, j+21, 1 );
    this.setCellAtIndex( i+2, j+34, 1 );
    this.setCellAtIndex( i+2, j+35, 1 );
    this.setCellAtIndex( i+3, j+11, 1 );
    this.setCellAtIndex( i+3, j+15, 1 );
    this.setCellAtIndex( i+3, j+20, 1 );
    this.setCellAtIndex( i+3, j+21, 1 );
    this.setCellAtIndex( i+3, j+34, 1 );
    this.setCellAtIndex( i+3, j+35, 1 );
    this.setCellAtIndex( i+4, j, 1 );
    this.setCellAtIndex( i+4, j+1, 1 );
    this.setCellAtIndex( i+4, j+10, 1 );
    this.setCellAtIndex( i+4, j+16, 1 );
    this.setCellAtIndex( i+4, j+20, 1 );
    this.setCellAtIndex( i+4, j+21, 1 );
    this.setCellAtIndex( i+5, j, 1 );
    this.setCellAtIndex( i+5, j+1, 1 );
    this.setCellAtIndex( i+5, j+10, 1 );
    this.setCellAtIndex( i+5, j+14, 1 );
    this.setCellAtIndex( i+5, j+16, 1 );
    this.setCellAtIndex( i+5, j+17, 1 );
    this.setCellAtIndex( i+5, j+22, 1 );
    this.setCellAtIndex( i+5, j+24, 1 );
    this.setCellAtIndex( i+6, j+10, 1 );
    this.setCellAtIndex( i+6, j+16, 1 );
    this.setCellAtIndex( i+6, j+24, 1 );
    this.setCellAtIndex( i+7, j+11, 1 );
    this.setCellAtIndex( i+7, j+15, 1 );
    this.setCellAtIndex( i+8, j+12, 1 );
    this.setCellAtIndex( i+8, j+13, 1 );
}
GameOfLife.prototype.blinker = function(i, j) {
    this.setCellAtIndex( i, j, 1 );
    this.setCellAtIndex( i+1, j, 1 );
    this.setCellAtIndex( i+2, j, 1 );
}
GameOfLife.prototype.peminto = function( i, j ) {
    this.setCellAtIndex( i, j+1, 1 );
    this.setCellAtIndex( i, j+2, 1 );
    this.setCellAtIndex( i+1, j, 1 );
    this.setCellAtIndex( i+1, j+1, 1 );
    this.setCellAtIndex( i+2, j+1, 1 );
}
GameOfLife.prototype.diehard = function( i, j ) {
    this.setCellAtIndex( i, j+6, 1 );
    this.setCellAtIndex( i+1, j, 1 );
    this.setCellAtIndex( i+1, j+1, 1 );
    this.setCellAtIndex( i+2, j+1, 1 );
    this.setCellAtIndex( i+2, j+5, 1 );
    this.setCellAtIndex( i+2, j+6, 1 );
    this.setCellAtIndex( i+2, j+7, 1 );
}
GameOfLife.prototype.acorn = function( i, j ) {
    this.setCellAtIndex( i, j+1, 1 );
    this.setCellAtIndex( i+1, j+3, 1 );
    this.setCellAtIndex( i+2, j, 1 );
    this.setCellAtIndex( i+2, j+1, 1 );
    this.setCellAtIndex( i+2, j+4, 1 );
    this.setCellAtIndex( i+2, j+5, 1 );
    this.setCellAtIndex( i+2, j+6, 1 );
}
GameOfLife.prototype.seedWithImage = function( imageURL ) {
    var _this = this;
    var image = new Image();
    image.onload = function() {
        //
        // create temporary canvas
        //
        var canvas      = document.createElement("canvas");
        var context     = canvas.getContext("2d");
        canvas.width    = _this.cells.width;
        canvas.height   = _this.cells.height;
        //
        // down res image
        //
        context.drawImage( image, 0, 0, canvas.width, canvas.height );
        //
        // parse image data
        //
        var current = _this.cells[ _this.cells.current ];
        var previous = _this.cells[ _this.cells.current == 0 ? 1 : 0 ];
        var imageData = context.getImageData(0, 0, canvas.width, canvas.height );
        for ( var i = 0; i < imageData.height; i++ ) {
            for ( var j = 0; j < imageData.width; j++ ) {
                var imageDataIndex = ( i * 4  * imageData.width ) + (j * 4);
                var cellIndex = ( i * _this.cells.width ) + j;
                var r = imageData.data[ imageDataIndex ];
                var g = imageData.data[ imageDataIndex + 1 ];
                var b = imageData.data[ imageDataIndex + 2 ];
                var gray = Math.round( 0.3*r + 0.59*g + 0.11*b );
                current[ cellIndex ] = gray > 50 ? 1 : 0;
                previous[ cellIndex ] = current[ cellIndex ] == 0 ? 1 : 0;
            }
        }
    };
    image.src = imageURL;
};

