MediaWiki:Gadget-Vivarium.js

/******************************************************************************/

/**** 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 = $( '

' ).addClass( 'VivariumContainer' );

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

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

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

this.menu = $( '

' ).addClass( 'VivariumMenu' );

this.zoomInButton = $( '' ).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 = $( '' ).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 = $( '' ).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 = $( '' ).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 = $( '' ).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 = $( '' ).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 = $( '' ).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 );