Jump to content

MediaWiki:Gadget-Vivarium.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/******************************************************************************/
/**** THIS PAGE TRACKS [[mw:MediaWiki:Gadget-Global-Vivarium.js]]. PLEASE AVOID EDITING DIRECTLY. 
/**** EDITS SHOULD BE PROPOSED DIRECTLY to [[mw:MediaWiki:Gadget-Global-Vivarium.js]].
/**** A BOT WILL RAISE AN EDIT REQUEST IF IT BECOMES DIFFERENT FROM UPSTREAM.
/******************************************************************************/

/**
 * Vivarium is an implementation of Conway's Game of Life
 * Documentation: https://www.mediawiki.org/wiki/Template:Vivarium
 * Author: Felipe Schenone (User:Sophivorus)
 * License: GNU General Public License (http://www.gnu.org/licenses/gpl.html)
 */
var Vivarium = {

	messages: {
		'de': {
			'cell-button': 'Zelle',
			'cell-button-tooltip': 'Zelle hinzufügen oder entfernen',
			'move-button': 'Bewegen',
			'move-button-tooltip': 'Board bewegen',
			'zoom-in-button': 'Einzoomen',
			'zoom-in-button-tooltip': 'Einzoomen',
			'zoom-out-button': 'Auszoomen',
			'zoom-out-button-tooltip': 'Auszoomen',
			'grid-button': 'Raster',
			'grid-button-tooltip': 'Raster',
			'reset-button': 'Zurücksetzen',
			'reset-button-tooltip': 'Zurücksetzen',
			'play-button': 'Abspielen',
			'play-button-tooltip': 'Abspielen',
			'pause-button': 'Pause',
			'pause-button-tooltip': 'Pause', 
			'next-generation-button': 'Weiter',
			'next-generation-button-tooltip': 'Nächste Generation',
		},
		'en': {
			'cell-button': 'Cell',
			'cell-button-tooltip': 'Add or remove cells',
			'move-button': 'Move',
			'move-button-tooltip': 'Move the board',
			'zoom-in-button': 'Zoom in',
			'zoom-in-button-tooltip': 'Zoom in',
			'zoom-out-button': 'Zoom out',
			'zoom-out-button-tooltip': 'Zoom out',
			'grid-button': 'Grid',
			'grid-button-tooltip': 'Grid',
			'reset-button': 'Reset',
			'reset-button-tooltip': 'Reset',
			'play-button': 'Play',
			'play-button-tooltip': 'Play',
			'pause-button': 'Pause',
			'pause-button-tooltip': 'Pause',
			'next-generation-button': 'Next generation',
			'next-generation-button-tooltip': 'Next generation',
			'generation-counter': 'Generation ',
			'population-counter': 'Population ',
		},
		'es': {
			'cell-button': 'Celda',
			'cell-button-tooltip': 'Agregar o quitar celdas',
			'move-button': 'Mover',
			'move-button-tooltip': 'Mover el tablero',
			'zoom-in-button': 'Acercar',
			'zoom-in-button-tooltip': 'Acercar',
			'zoom-out-button': 'Alejar',
			'zoom-out-button-tooltip': 'Alejar',
			'grid-button': 'Grilla',
			'grid-button-tooltip': 'Grilla',
			'reset-button': 'Reiniciar',
			'reset-button-tooltip': 'Reiniciar',
			'play-button': 'Reproducir',
			'play-button-tooltip': 'Reproducir',
			'pause-button': 'Pausar',
			'pause-button-tooltip': 'Pausar',
			'next-generation-button': 'Generación siguiente',
			'next-generation-button-tooltip': 'Generación siguiente',
			'generation-counter': 'Generación ',
			'population-counter': 'Población ',
		},
		'fr': {
			'cell-button': 'Cellule',
			'cell-button-tooltip': 'Ajouter ou enlever des cellules',
			'move-button': 'Déplacer',
			'move-button-tooltip': 'Déplacer la carte',
			'zoom-in-button': 'Se rapprocher',
			'zoom-in-button-tooltip': 'Se rapprocher',
			'zoom-out-button': "S'éloigner",
			'zoom-out-button-tooltip': "S'éloigner",
			'grid-button': 'Grille',
			'grid-button-tooltip': 'Grille',
			'reset-button': 'Recommencer',
			'reset-button-tooltip': 'Recommencer',
			'play-button': 'Reproduire',
			'play-button-tooltip': 'Reproduire',
			'pause-button': 'Mettre sur pause',
			'pause-button-tooltip': 'Mettre sur pause',
			'next-generation-button': 'Suivant',
			'next-generation-button-tooltip': 'Generation suivante',
		},
		'it': {
			'cell-button': 'Cellula',
			'cell-button-tooltip': 'Aggiungere o rimuovere le cellule',
			'move-button': 'Spostare',
			'move-button-tooltip': "Spostare l'asse",
			'zoom-in-button': 'Ingrandire',
			'zoom-in-button-tooltip': 'Ingrandire',
			'zoom-out-button': 'Rimpicciolire',
			'zoom-out-button-tooltip': 'Rimpicciolire',
			'grid-button': 'Griglia',
			'grid-button-tooltip': 'Griglia',
			'reset-button': 'Reset',
			'reset-button-tooltip': 'Reset',
			'play-button': 'Giocare',
			'play-button-tooltip': 'Giocare',
			'pause-button': 'Pausa',
			'pause-button-tooltip': 'Pausa',
			'next-generation-button': 'Il prossimo',
			'next-generation-button-tooltip': 'Generazione successiva',
		},
		'pl': {
			'cell-button': 'Komórka',
			'cell-button-tooltip': 'Dodaj lub odejmij komórki',
			'move-button': 'Przejdź dalej',
			'move-button-tooltip': "Przestaw planszę",
			'zoom-in-button': 'Przybliż',
			'zoom-in-button-tooltip': 'Przybliż',
			'zoom-out-button': 'Oddal',
			'zoom-out-button-tooltip': 'Oddal',
			'grid-button': 'Siatka',
			'grid-button-tooltip': 'Siatka',
			'reset-button': 'Reset',
			'reset-button-tooltip': 'Reset',
			'play-button': 'Odtwórz',
			'play-button-tooltip': 'Odtwórz',
			'pause-button': 'Zatrzymaj',
			'pause-button-tooltip': 'Zatrzymaj',
			'next-generation-button': 'Dalej',
			'next-generation-button-tooltip': 'Następne pokolenie',
		},
	},

	/**
	 * Initialisation script
	 */
	init: function () {
		// Set the interface language
		var lang = mw.config.get( 'wgUserLanguage' );
		if ( ! ( lang in Vivarium.messages ) ) {
			lang = 'en'; // Fallback to English
		}
		mw.messages.set( Vivarium.messages[ lang ] );

		$( '.Vivarium' ).each( function () {
			var gui = new Vivarium.GUI( this ),
				board = new Vivarium.Board( gui ),
				game = new Vivarium.Game( board ),
				mouse = new Vivarium.Mouse( board, game ),
				touch = new Vivarium.Touch( board );

			gui.bindEvents( board, game, mouse, touch );

			board.init();

			if ( $( this ).data( 'autoplay' ) ) {
				game.play();
			}
		});
	},

	GUI: function ( wrapper ) {

		this.wrapper = $( wrapper );

		this.container = $( '<div>' ).addClass( 'VivariumContainer' );

		this.canvas = $( '<canvas>' ).addClass( 'VivariumCanvas' );

		this.generationCounter = $( '<span>' ).addClass( 'VivariumCounter VivariumGenerationCounter' ).text( mw.message( 'generation-counter' ) + 0 );

		this.populationCounter = $( '<span>' ).addClass( 'VivariumCounter VivariumPopulationCounter' ).text( mw.message( 'population-counter' ) + 0 );

		this.menu = $( '<div>' ).addClass( 'VivariumMenu' );

		this.zoomInButton = $( '<img>' ).attr({
			'class': 'VivariumButton VivariumZoomInButton',
			'src': '//upload.wikimedia.org/wikipedia/commons/2/2e/WikiWidgetZoomInButton.png',
			'title': mw.message( 'zoom-in-button-tooltip' ),
			'alt': mw.message( 'zoom-in-button' )
		});

		this.zoomOutButton = $( '<img>' ).attr({
			'class': 'VivariumButton VivariumZoomOutButton',
			'src': '//upload.wikimedia.org/wikipedia/commons/6/63/WikiWidgetZoomOutButton.png',
			'title': mw.message( 'zoom-out-button-tooltip' ),
			'alt': mw.message( 'zoom-out-button' )
		});

		this.gridButton = $( '<img>' ).attr({
			'class': 'VivariumButton VivariumGridButton',
			'src': '//upload.wikimedia.org/wikipedia/commons/a/a9/WikiWidgetGridButton.png',
			'title': mw.message( 'grid-button-tooltip' ),
			'alt': mw.message( 'grid-button' )
		});

		this.resetButton = $( '<img>' ).attr({
			'class': 'VivariumButton VivariumResetButton',
			'src': '//upload.wikimedia.org/wikipedia/commons/0/0e/WikiWidgetResetButton.png',
			'title': mw.message( 'reset-button-tooltip' ),
			'alt': mw.message( 'reset-button' )
		});

		this.playButton = $( '<img>' ).attr({
			'class': 'VivariumButton VivariumPlayButton',
			'src': '//upload.wikimedia.org/wikipedia/commons/b/b8/WikiWidgetPlayButton.png',
			'title': mw.message( 'play-button-tooltip' ),
			'alt': mw.message( 'play-button' )
		});

		this.pauseButton = $( '<img>' ).attr({
			'class': 'VivariumButton VivariumPauseButton',
			'src': '//upload.wikimedia.org/wikipedia/commons/6/6e/WikiWidgetPauseButton.png',
			'title': mw.message( 'pause-button-tooltip' ),
			'alt': mw.message( 'pause-button' )
		}).hide(); // The pause button starts hidden

		this.nextGenerationButton = $( '<img>' ).attr({
			'class': 'VivariumButton VivariumNextGenerationButton',
			'src': '//upload.wikimedia.org/wikipedia/commons/b/bf/WikiWidgetNextFrameButton.png',
			'title': mw.message( 'next-generation-button-tooltip' ),
			'alt': mw.message( 'next-generation-button' )
		});

		// Put it all together
		this.menu.append(
			this.zoomInButton,
			this.zoomOutButton,
			this.gridButton,
			this.resetButton,
			this.playButton,
			this.pauseButton,
			this.nextGenerationButton
		);
		this.container.append(
			this.canvas,
			this.menu,
			this.generationCounter,
			this.populationCounter
		);
		this.wrapper.html( this.container );

		this.bindEvents = function ( board, game, mouse, touch ) {

			// Board events
			this.zoomOutButton.click( function () { board.zoomOut(); } );
			this.zoomInButton.click( function () { board.zoomIn(); } );
			this.gridButton.click( function () { board.toggleGrid(); } );

			// Game events
			this.resetButton.click( function () { game.reset(); } );
			this.playButton.click( function () { game.play(); } );
			this.pauseButton.click( function () { game.pause(); } );
			this.nextGenerationButton.click( function () { game.nextGeneration(); } );

			// Mouse events
			this.canvas.mousedown( function ( event ) { mouse.down( event ) } );
			this.canvas.mousemove( function ( event ) { mouse.move( event ) } );
			this.canvas.mouseup( function ( event ) { mouse.up( event ) } );

			// Touch events
			this.canvas.on( 'touchstart', function ( event ) { touch.start( event ) } );
			this.canvas.on( 'touchmove', function ( event ) { touch.move( event ) } );
			this.canvas.on( 'touchend', function ( event ) { touch.end( event ) } );
		};
	},

	Board: function ( gui ) {

		this.gui = gui;

		this.canvas = this.gui.canvas[0];
		this.context = this.canvas.getContext( '2d' );

		this.width = this.canvas.width;
		this.height = this.canvas.height;

		this.centerX = 0;
		this.centerY = 0;

		this.cellSize = 4;

		this.xCells = Math.floor( this.width / this.cellSize );
		this.yCells = Math.floor( this.height / this.cellSize );

		this.grid = false;

		this.populationCounter = 0;

		/**
		 * These arrays hold the coordinates of the live cells
		 */
		this.newLiveCells = [];
		this.oldLiveCells = [];

		/**
		 * Constructor
		 */
		this.init = function () {
			this.oldLiveCells = [];
			this.newLiveCells = [];
			this.centerX = 0;
			this.centerY = 0;
			this.setPopulationCounter( 0 );

			var wrapper = this.gui.wrapper,
				width = wrapper.data( 'width' ),
				height = wrapper.data( 'height' ),
				cells = wrapper.data( 'cells' ),
				zoom = wrapper.data( 'zoom' ),
				grid = wrapper.data( 'grid' );

			if ( width ) {
				this.setWidth( width );
			}

			if ( height ) {
				this.setHeight( height );
			}

			if ( cells ) {
				cells = cells.replace( /\s/g, '' ).split( ';' );
				for ( var i in cells ) {
					this.addCell( cells[ i ] );
				}
			}

			if ( zoom ) {
				this.setCellSize( zoom );
			}

			if ( grid ) {
				this.grid = true;
			}

			this.refill();
		};

		/* Getters */

		this.getXcells = function () {
			return Math.floor( this.width / this.cellSize );
		};

		this.getYcells = function () {
			return Math.floor( this.height / this.cellSize );
		};

		/**
		 * Takes a string of coordinates (like "23,-75")
		 * and returns the state of the cell
		 */
		this.getNewState = function ( coords ) {
			if ( this.newLiveCells.indexOf( coords ) === -1 ) {
				return 0; // Dead
			}
			return 1; // Alive
		};
		this.getOldState = function ( coords ) {
			if ( this.oldLiveCells.indexOf( coords ) === -1 ) {
				return 0; // Dead
			}
			return 1; // Alive
		};

		/**
		 * Takes a string of coordinates (like "23,-75")
		 * and returns an array with the neighboring coordinates
		 */
		this.getNeighbors = function ( coords ) {
			coords = coords.split( ',' );
			var x = parseInt( coords[0] ),
				y = parseInt( coords[1] );
			return [
				( x - 1 ) + ',' + ( y - 1 ),
				( x - 1 ) + ',' + ( y + 0 ),
				( x - 1 ) + ',' + ( y + 1 ),
				( x + 0 ) + ',' + ( y + 1 ),
				( x + 0 ) + ',' + ( y - 1 ),
				( x + 1 ) + ',' + ( y - 1 ),
				( x + 1 ) + ',' + ( y + 0 ),
				( x + 1 ) + ',' + ( y + 1 )
			];
		};

		/**
		 * Takes a string of coordinates (like "23,-75")
		 * and returns the number of live neighbors
		 */
		this.getLiveNeighborCount = function ( coords ) {
			var neighbors = this.getNeighbors( coords ),
				liveNeighborCount = 0;
			for ( var i = 0, len = neighbors.length; i < len; i++ ) {
				if ( this.oldLiveCells.indexOf( neighbors[ i ] ) > -1 ) {
					liveNeighborCount++;
				}
			}
			return liveNeighborCount;
		};

		/* Setters */

		this.setWidth = function ( value ) {
			this.width = value;
			this.canvas.setAttribute( 'width', value );
			this.xCells = this.getXcells();
		};

		this.setHeight = function ( value ) {
			this.height = value;
			this.canvas.setAttribute( 'height', value );
			this.yCells = this.getYcells();
		};

		this.setCellSize = function ( value ) {
			this.cellSize = parseInt( value );
			this.xCells = this.getXcells();
			this.yCells = this.getYcells();
		};

		this.setPopulationCounter = function ( value ) {
			this.populationCounter = value;
			this.gui.populationCounter.text( mw.message( 'population-counter' ) + value );
		};

		/* Actions */

		this.zoomIn = function () {
			if ( this.cellSize === 32 ) {
				return;
			}
			this.setCellSize( this.cellSize * 2 );
			this.refill();
		};

		this.zoomOut = function () {
			if ( this.cellSize === 1 ) {
				return;
			}
			this.setCellSize( this.cellSize / 2 );
			this.refill();
		};

		this.toggleGrid = function () {
			this.grid = this.grid ? false : true;
			this.refill();
		};

		this.drawGrid = function () {
			if ( this.cellSize < 4 ) {
				return; // Cells are too small for the grid
			}
			this.context.beginPath();
			for ( var x = 0; x <= this.xCells; x++ ) {
				this.context.moveTo( x * this.cellSize - 0.5, 0 ); // The 0.5 avoids getting blury lines
				this.context.lineTo( x * this.cellSize - 0.5, this.height );
			}
			for ( var y = 0; y <= this.yCells; y++ ) {
				this.context.moveTo( 0, y * this.cellSize - 0.5 );
				this.context.lineTo( this.width, y * this.cellSize - 0.5 );
			}
			this.context.strokeStyle = '#333';
			this.context.stroke();
		};

		this.fill = function () {
			for ( var i = 0, len = this.newLiveCells.length; i < len; i++ ) {
				this.fillCell( this.newLiveCells[ i ] );
			}
			if ( this.grid ) {
				this.drawGrid();
			}
		};

		this.clear = function () {
			this.context.clearRect( 0, 0, this.canvas.width, this.canvas.height );
		};

		this.refill = function () {
			this.clear();
			this.fill();
		};

		this.fillCell = function ( coords ) {
			coords = coords.split( ',' );
			var x = parseInt( coords[0] );
			var y = parseInt( coords[1] );
			var minX = this.centerX - Math.floor( this.xCells / 2 );
			var minY = this.centerY - Math.floor( this.yCells / 2 );
			var maxX = minX + this.xCells;
			var maxY = minY + this.yCells;
			if ( x < minX || y < minY || x > maxX || y > maxY ) {
				return; // If the cell is beyond view, don't draw it
			}
			var rectX = Math.abs( this.centerX - Math.floor( this.xCells / 2 ) - x ) * this.cellSize,
				rectY = Math.abs( this.centerY - Math.floor( this.yCells / 2 ) - y ) * this.cellSize,
				rectW = this.cellSize - ( this.grid && this.cellSize >= 4 ? 1 : 0 ), // Don't draw over the grid
				rectH = this.cellSize - ( this.grid && this.cellSize >= 4 ? 1 : 0 );
			this.context.fillStyle = 'white';
			this.context.fillRect( rectX, rectY, rectW, rectH );
		};

		this.clearCell = function ( coords ) {
			coords = coords.split( ',' );
			var x = parseInt( coords[0] );
			var y = parseInt( coords[1] );
			var minX = this.centerX - Math.floor( this.xCells / 2 );
			var minY = this.centerY - Math.floor( this.yCells / 2 );
			var maxX = minX + this.xCells;
			var maxY = minY + this.yCells;
			if ( x < minX || y < minY || x > maxX || y > maxY ) {
				return; // If the cell is beyond view, there's no need to erase it
			}
			var rectX = Math.abs( this.centerX - Math.floor( this.xCells / 2 ) - x ) * this.cellSize,
				rectY = Math.abs( this.centerY - Math.floor( this.yCells / 2 ) - y ) * this.cellSize,
				rectW = this.cellSize - ( this.grid && this.cellSize >= 4 ? 1 : 0 ), // Don't erase the grid
				rectH = this.cellSize - ( this.grid && this.cellSize >= 4 ? 1 : 0 );
			this.context.clearRect( rectX, rectY, rectW, rectH );
		};

		this.addCell = function ( coords ) {
			this.newLiveCells.push( coords );
			this.fillCell( coords );
			this.setPopulationCounter( this.populationCounter + 1 );
		};

		this.removeCell = function ( coords ) {
			var index = this.newLiveCells.indexOf( coords );
			this.newLiveCells.splice( index, 1 ); // Remove the coords from the array
			this.clearCell( coords );
			this.setPopulationCounter( this.populationCounter - 1 );
		};
	},

	Game: function ( board ) {

		this.board = board;

		this.playing = false;

		this.generationCounter = 0;

		/* Setters */

		this.setGenerationCounter = function ( value ) {
			this.generationCounter = value;
			this.board.gui.generationCounter.text( mw.message( 'generation-counter' ) + value );
		};

		/* Actions */

		/**
		 * This method is the heart of the widget
		 */
		this.nextGeneration = function () {
			this.setGenerationCounter( this.generationCounter + 1 );
			this.board.oldLiveCells = this.board.newLiveCells.slice(); // Clone the array
			var liveCells = this.board.oldLiveCells,
				coords,
				neighbors,
				relevantCells = liveCells, // The relevant cells are the live ones plus their neighbors minus the duplicates
				seen = [],
				state,
				liveNeighborCount,
				i;
			for ( i = 0, len = liveCells.length; i < len; i++ ) {
				coords = liveCells[ i ];
				neighbors = this.board.getNeighbors( coords );
				relevantCells = relevantCells.concat( neighbors );
			}
			for ( i = 0, len = relevantCells.length; i < len; i++ ) {
				coords = relevantCells[ i ];
				if ( seen.indexOf( coords ) > -1 ) {
					continue; // Ignore duplicates
				}
				seen.push( coords );
				state = this.board.getOldState( coords );
				liveNeighborCount = this.board.getLiveNeighborCount( coords );
				// Death by underpopulation or overpopulation
				if ( state === 1 && ( liveNeighborCount < 2 || liveNeighborCount > 3 ) ) {
					this.board.removeCell( coords );
				}
				// Reproduction
				else if ( state === 0 && liveNeighborCount === 3 ) {
					this.board.addCell( coords );
				}
			}
		};

		this.reset = function () {
			// Reset the board
			this.board.init();

			// Reset the game
			this.pause();
			this.setGenerationCounter( 0 );
		};

		this.play = function () {
			if ( this.playing ) {
				return; // The game is already playing
			}
			var game = this;
			this.playing = setInterval( function () { game.nextGeneration(); }, 1 ); // The interval id is stored in the playing property
			this.board.gui.playButton.hide();
			this.board.gui.pauseButton.show();
		};

		this.pause = function () {
			if ( !this.playing ) {
				return; // The game is already paused
			}
			clearInterval( this.playing );
			this.playing = false;
			this.board.gui.playButton.show();
			this.board.gui.pauseButton.hide();
		};
	},

	Mouse: function ( board, game ) {

		this.board = board;
		this.game = game;

		/**
		 * The position relative to the origin of the coordinate system of the board (in cells, not pixels)
		 */
		this.newX = null;
		this.newY = null;
		this.oldX = null;
		this.oldY = null;

		this.state = null; // up or down
		this.drag = false;

		/**
		 * Getters
		 */
		this.getX = function ( event ) {
			var offsetX = event.pageX - $( event.target ).offset().left - 1, // The -1 is to correct a minor displacement
				newX = this.board.centerX - Math.floor( this.board.xCells / 2 ) + Math.floor( offsetX / this.board.cellSize );
			return newX;
		};

		this.getY = function ( event ) {
			var offsetY = event.pageY - $( event.target ).offset().top - 2, // The -2 is to correct a minor displacement
				newY = this.board.centerY - Math.floor( this.board.yCells / 2 ) + Math.floor( offsetY / this.board.cellSize );
			return newY;
		};

		/**
		 * Event handlers
		 */
		this.down = function ( event ) {
			this.state = 'down';
			this.newX = this.getX( event );
			this.newY = this.getY( event );
		};

		this.move = function ( event ) {
			if ( this.state === 'down' ) {

				this.oldX = this.newX;
				this.oldY = this.newY;
				this.newX = this.getX( event );
				this.newY = this.getY( event );

				if ( this.newX !== this.oldX || this.newY !== this.oldY ) {

					this.drag = true;

					this.board.centerX += this.oldX - this.newX;
					this.board.centerY += this.oldY - this.newY;
					this.board.refill();

					// Bugfix: without the following, the board flickers when moving, not sure why
					this.newX = this.getX( event );
					this.newY = this.getY( event );
				}
			}
		};

		this.up = function ( event ) {
			this.state = 'up';

			if ( !this.drag ) {

				this.game.pause();

				var coords = String( this.newX + ',' + this.newY );

				if ( this.board.getNewState( coords ) === 0 ) {
					this.board.addCell( coords );
				} else {
					this.board.removeCell( coords );
				}
			}
			this.drag = false;
		};
	},

	Touch: function ( board ) {

		this.board = board;

		// The distance from the origin of the coordinate system in virtual pixels (not real ones)
		this.newX = null;
		this.newX = null;
		this.oldX = null;
		this.oldY = null;

		this.moved = false;

		/**
		 * Getters
		 */
		this.getX = function ( event ) {
			var offsetX = event.originalEvent.changedTouches[0].pageX - $( event.target ).offset().left,
				newX = this.board.centerX - Math.floor( this.board.xCells / 2 ) + Math.floor( offsetX / this.board.cellSize );
			return newX;
		};

		this.getY = function ( event ) {
			var offsetY = event.originalEvent.changedTouches[0].pageY - $( event.target ).offset().top,
				newY = this.board.centerY - Math.floor( this.board.yCells / 2 ) + Math.floor( offsetY / this.board.cellSize );
			return newY;
		};

		/**
		 * Event handlers
		 */
		this.start = function ( event ) {
			this.newX = this.getX( event );
			this.newY = this.getY( event );
		};

		this.move = function ( event ) {
			this.oldX = this.newX;
			this.oldY = this.newY;
			this.newX = this.getX( event );
			this.newY = this.getY( event );

			this.board.centerX += this.oldX - this.newX;
			this.board.centerY += this.oldY - this.newY;
			this.board.refill();

			// Bugfix: without the following, the board flickers when moving, not sure why
			this.newX = this.getX( event );
			this.newY = this.getY( event );

			this.moved = true;

			event.preventDefault();
		};

		this.end = function ( event ) {
			this.moved = false;
		};
	}
};

jQuery( Vivarium.init );