Two-Dimensional Cellular Automaton in JavaScript

I recently posted my implementation of a one-dimensional or elementary cellular automaton in JavaScript. For this post I will add a whole new dimension and implement a 2D version.

Two-Dimensional Cellular Automata

While elementary cellular automata are interesting 2D automata are in a different league. Ostensibly they do no more than provide relaxing and therapeutic patterns on the screen but they actually provide a tantalising glimpse into something much more profound.

The central lesson of modern science since the days of Issac Newton has been that the unfathomably complex universe is actually governed by just a few simple rules, and the biggest challenge for science in the future is to simplify these even more into a single Grand Unified Theory. Something that is still difficult to get your head round is the concept of emergence, or the idea that these basic forces of nature, acting on just a few fundamental types of matter, can produce things as complex as you, me and the entire universe.

Nobody has (yet!) developed a cellular automaton as complex as you, me or the entire universe* but watching one run does give the very faintest inkling of emergence in action.

* Unless the universe is just a cellular automaton...

At its simplest a 2D cellular automaton consists of a grid of Boolean values - on or off, which can be represented visually by two different colours. (If you wish to think of a cellular automaton in terms of biology or an ecosystem you can think instead of cells being "live" or "dead".) It transitions from one generation to the next by following a simple set of rules which specify the next state of each cell depending on its current state and the number of on and off cells in its "neighbourhood" of eight adjacent cells.

Although typically represented as a rectangle the cells actually wrap around, so cells on the edges have opposite cells as part of their neighbourhood. They are therefore logically on the surface of a torus, one of these things.

A set of rules can be written down using a very simple notation in the format B35678/S34678. The B stands for "born" (ie. changed from off to on) and the S stands for "survive", ie left at on if already on.

In the example rule set shown (which incidentally is one of a number of well-known named rules, this one being called Diamoeba) the next state of each cell is determined as follows:

  • If its current state if "off" and it has 3, 5, 6, 7 or 8 "on" neighbours then its next state is "on"

  • If its current state is "on" and it has 3, 4, 6, 7 or 8 "on" neighbours then it is left at "on"

  • For all other combinations the cell is set to or left at "off"

Watching cellular automata run is one thing, but to truly appreciate them you need to code one yourself. So lets do so.

The Project

This project consists of the following files which you can download as a zip, or you can clone/download the Github repository if you prefer.

  • ca2d.js
  • ca2d_rules.js
  • ca2d_patterns.js
  • ca2d_svg.js
  • ca2d.htm
  • CiJSListBox.js
  • ca2d_page.js

Source Code Links

ZIP File
GitHub

The Cellular Automaton runs in a web page, an example of which is shown below.

Lets get stuck into the code, starting with the CellularAutomaton2D class in ca2d.js.

ca2d.js part 1: Constructor

class CellularAutomaton2D
{
    constructor()
    {
        this._CurrentState; // [row][column]
        this._NumberOfColumns = 128;
        this._NumberOfRows = 128;
        this._RuleTable = null;
        this._Initialized = false;
        this._Running = false;
        this._Timer = null;
        this._Generation = null;
        this._SVG = null;
        this._StateChangedEventHandlers = [];
        this._GenerationChangedEventHandlers = [];
        this._NumberOfColumnsOrRowsChangedEventHandlers = [];

        this._InitializeRuleTable();
    }

The constructor is very simple, just creating a number of backing variables for various properties. It also calls a function to create a sample set of rules.

The class is designed to be independent of any graphical representation and does not produce any output. Instead it fires events when any changes occur. Code producing visual output can then subscribe to these events, doing whatever is necessary to display the cellular automaton. These events consist of the three arrays named ...ChangedEventHandlers, client code adding functions to these to be run when the event is fired.

ca2d.js part 2: Properties

    //---------------------------------------------------
    // PROPERTIES
    //---------------------------------------------------

    get StateChangedEventHandlers() { return this._StateChangedEventHandlers; }

    get GenerationChangedEventHandlers() { return this._GenerationChangedEventHandlers; }

    get NumberOfColumnsOrRowsChangedEventHandlers() { return this._NumberOfColumnsOrRowsChangedEventHandlers; }

    get RuleTable() { return this._RuleTable; }

    get NumberOfRows() { return this._NumberOfRows; }

    get NumberOfColumns() { return this._NumberOfColumns; }

Again a simple bit of code providing getters for various properties, including the event handler arrays.

ca2d.js part 3: Methods

    //---------------------------------------------------
    // METHODS
    //---------------------------------------------------

    _FireStateChangedEvent(changed)
    {
        this._StateChangedEventHandlers.every(function (Handler) { Handler(changed); });
    }

    _FireGenerationChangedEvent(GenerationDetails)
    {
        this._GenerationChangedEventHandlers.every(function (Handler) { Handler(GenerationDetails); });
    }

    _FireNumberOfColumnsOrRowsChangedEvent()
    {
        this._NumberOfColumnsOrRowsChangedEventHandlers.every(function (Handler) { Handler(); });
    }

    _InitializeArrays()
    {
        this._CurrentState = [];

        for(let r = 0; r < this._NumberOfRows; r++)
        {
            this._CurrentState[r] = [];

            for(let c = 0; c < this._NumberOfColumns; c++)
            {
                this._CurrentState[r][c] = 0;
            }
        }

        this._Initialized = true;
    }

    _InitializeRuleTable()
    {
        this._RuleTable = [];

        this._RuleTable[0] =  {Neighbours: 0, Current: 0, Next: 0};
        this._RuleTable[1] =  {Neighbours: 1, Current: 0, Next: 0};
        this._RuleTable[2] =  {Neighbours: 2, Current: 0, Next: 0};
        this._RuleTable[3] =  {Neighbours: 3, Current: 0, Next: 0};
        this._RuleTable[4] =  {Neighbours: 4, Current: 0, Next: 0};
        this._RuleTable[5] =  {Neighbours: 5, Current: 0, Next: 0};
        this._RuleTable[6] =  {Neighbours: 6, Current: 0, Next: 0};
        this._RuleTable[7] =  {Neighbours: 7, Current: 0, Next: 0};
        this._RuleTable[8] =  {Neighbours: 8, Current: 0, Next: 0};

        this._RuleTable[9] =  {Neighbours: 0, Current: 1, Next: 0};
        this._RuleTable[10] = {Neighbours: 1, Current: 1, Next: 0};
        this._RuleTable[11] = {Neighbours: 2, Current: 1, Next: 0};
        this._RuleTable[12] = {Neighbours: 3, Current: 1, Next: 0};
        this._RuleTable[13] = {Neighbours: 4, Current: 1, Next: 0};
        this._RuleTable[14] = {Neighbours: 5, Current: 1, Next: 0};
        this._RuleTable[15] = {Neighbours: 6, Current: 1, Next: 0};
        this._RuleTable[16] = {Neighbours: 7, Current: 1, Next: 0};
        this._RuleTable[17] = {Neighbours: 8, Current: 1, Next: 0};
    }

    _GetNeighbourCount(Row, Column)
    {
        let NeighbourCount = 0;

        let RollaroundColumn;
        let RollaroundRow;

        for(let r = (Row - 1); r <= (Row + 1); r++)
        {
            for(let c = (Column - 1); c <= (Column + 1); c++)
            {
                RollaroundColumn = c;
                RollaroundRow = r;

                if(RollaroundRow < 0) RollaroundRow = this._NumberOfRows - 1;
                if(RollaroundRow > (this._NumberOfRows - 1)) RollaroundRow = 0;
                if(RollaroundColumn < 0) RollaroundColumn = this._NumberOfColumns - 1;
                if(RollaroundColumn > (this._NumberOfColumns - 1)) RollaroundColumn = 0;

                if( !(r == Row && c == Column) )
                {
                    if(this._CurrentState[RollaroundRow][RollaroundColumn] == 1)
                    {
                        NeighbourCount++;
                    }
                }
            }
        }

        return NeighbourCount;
    }

    SetNumberOfRowsAndColumns(rows, columns)
    {
        this._NumberOfRows = rows;
        this._NumberOfColumns = columns;

        this._InitializeArrays();

        this._FireNumberOfColumnsOrRowsChangedEvent();
    }

    InitializeToPercentage(Percentage)
    {
        let NumberToInitialize = Math.round((this._NumberOfRows * this._NumberOfColumns) * (Percentage / 100));

        this.InitializeToCount(NumberToInitialize);
    }

    InitializeToPattern(Pattern)
    {
        let Columns = Pattern[0].length;
        let Rows = Pattern.length;

        if(Columns <= this._NumberOfColumns && Rows <= this._NumberOfRows)
        {
            let ChangedCells = [];

            this.InitializeToZero();

            let StartRow = Math.round((this._NumberOfRows / 2) - (Rows / 2));
            let StartColumn = Math.round((this._NumberOfColumns / 2) - (Columns / 2));

            for(let r = 0; r < Rows; r++)
            {
                for(let c = 0; c < Columns; c++)
                {
                    this._CurrentState[r + StartRow][c + StartColumn] = Pattern[r][c];

                    ChangedCells.push({c:c + StartColumn, r:r + StartRow, State:Pattern[r][c]});
                }
            }

            this._FireStateChangedEvent(ChangedCells);

            Generation = 0;

            this._FireGenerationChangedEvent({Generation: Generation, Born:0, Died:0});
        }
        else
        {
            alert("Pattern of " + Columns + " columns and " + Rows + " rows is too large for current cellular automaton size");
        }
    }

    InitializeToCount(NumberToInitialize)
    {
        let ChangedCells = [];

        let c;
        let r;

        this._InitializeArrays();

        while(NumberToInitialize > 0)
        {
            c = Math.floor(Math.random() * this._NumberOfColumns);
            r = Math.floor(Math.random() * this._NumberOfRows);

            if(this._CurrentState[r][c] === 0)
            {
                this._CurrentState[r][c] = 1;

                ChangedCells.push({c:c, r:r, State:1});

                NumberToInitialize--;
            }
        }

        this._FireStateChangedEvent(ChangedCells);

        this._Generation = 0;

        this._FireGenerationChangedEvent({Generation: this._Generation, Born:0, Died:0});
    }

    _CalculateNextState()
    {
        let ChangedCells = [];

        let NeighbourCount;
        let RuleTableIndex;
        let NextState;

        let Born = 0;
        let Died = 0;

        for(let r = 0; r < this._NumberOfRows; r++)
        {
            for(let c = 0; c < this._NumberOfColumns; c++)
            {
                NeighbourCount = this._GetNeighbourCount(r, c);

                for(RuleTableIndex = 0; RuleTableIndex <= 17; RuleTableIndex++)
                {
                    if(this._RuleTable[RuleTableIndex].Neighbours == NeighbourCount && this._RuleTable[RuleTableIndex].Current == this._CurrentState[r][c])
                    {
                        NextState = this._RuleTable[RuleTableIndex].Next;

                        if(NextState != this._CurrentState[r][c])
                        {
                            ChangedCells.push({c:c, r:r, State:NextState});

                            if(NextState === 0)
                            {
                                Died++;
                            }
                            else
                            {
                                Born++;
                            }
                        }
                    }
                }
            }
        }

        this._FireStateChangedEvent(ChangedCells);

        this._Generation++;

        this._FireGenerationChangedEvent({Generation: this._Generation, Born: Born, Died: Died});

        if(Born === 0 && Died === 0)
        {
            clearInterval(this._Timer);
        }

        for(let i = 0, l = ChangedCells.length; i < l; i++)
        {
            this._CurrentState[ ChangedCells[i].r ][ ChangedCells[i].c ] = ChangedCells[i].State;
        }
    }

    StartStop(DelayInMilliseconds)
    {
        if(this._Initialized === true)
        {
            if(this._Running === false)
            {
                let THIS = this;

                this._Timer = setInterval(function(){ THIS._CalculateNextState(); }, DelayInMilliseconds);

                this._Running = true;
            }
            else
            {
                clearInterval(this._Timer);

                this._Running = false;
            }
        }
        else
        {
            alert("Cellular Automaton is not initialized");
        }
    }

    Step()
    {
        if(Initialized === true)
        {
            this._CalculateNextState();
        }
        else
        {
            alert("Cellular Automaton is not initialized");
        }
    }
}

_FireStateChangedEvent

_FireGenerationChangedEvent

_FireNumberOfColumnsOrRowsChangedEvent

These call all of the functions in their respective arrays, and exist to be called by other functions when their respective changes occur.

_InitializeArrays

Here we create a two-dimensional array representing the state of the entire cellular automaton. All cells are initialized to 0 or off.

_InitializeRuleTable

The rule table is created here, and consists of an array of 16 objects representing all possible current cell value/neighbour count combinations, and the next state of the cell for each.

_GetNeighbourCount

Calculates the number of cells set to "on" in any given cell's neighbourhood. Note that although the cellular automaton is represented here as a rectangle rows and columns of cells actually roll round so we need to take this into account here.

SetNumberOfRowsAndColumns

As well as setting the variables we also need to re-initialize the cells array, and fire the appropriate event so the UI knows to re-drawn the display.

InitializeToPercentage

Sets the specified percentage of cells to "on" by calculating the number of cells to be set and then using the InitializeToCount method.

InitializeToPattern

Here we initialize the cellular automaton to one of the built-in patterns. Firstly we need to check if the number of rows and columns is sufficient to contain the pattern, and then calculate the starting position needed to centre the pattern. We then iterate the pattern, setting the cells to "on" and adding them to the ChangedCells array. This array is passed as an argument of _FireStateChangedEvent for the UI to use to re-draw changed cells. This ensures it only needs to redraw cells which have had their values changed.

InitializeToCount

Again we use the ChangedCells array, and maintain a count of the number of cells which have been set to "on". We then enter a while loop to keep on setting cells at random until the required number have been set. Obviously we cannot just use a for loop as we might randomly select cells which are already "on". Finally we fire the relevant events.

_CalculateNextState

Right then, this is the big one, the function at the heart of the whole project.

Firstly we need a few variables, including an array for changed cells as seen previously.

Next we need nested loops for iterating rows and columns. Here we get the neighbour count and pick up the next cell state for the particular current state/neighbour count combination.

After the loop we fire the relevant events, before checking the Born and Died counts. If these are both 0 then the CA has entered a static state after which it can never change. In this case we stop it by calling clearInterval.

Finally we copy the changed cells to the _CurrentState array.

StartStop

After checking that the CA is initialized we then carry out one of two actions - if it is not running we set it running using setInterval with a call to _CalculateNextState and the required interval. If it is already running we stop using clearInterval.

You could argue that this should be two separate methods but I think this makes the code simpler to use, and streamlines the use of a single Start/Stop button in the UI as I have done.

Step

After checking the CA is initialized we simply call _CalculateNextState once.

ca2d_patterns.js

class CellularAutomaton2DPatterns
{
    constructor()
    {
        this._Patterns = []; // [row][column]

        this._InitializePatterns();
    }

    //---------------------------------------------------
    // PROPERTIES
    //---------------------------------------------------

    get Patterns() { return this._Patterns; }

    //---------------------------------------------------
    // METHODS
    //---------------------------------------------------

    _InitializePatterns()
    {
        this._Patterns.push(
        {Name: "Block (Still Life)",
        Pattern:[[1,1],
                 [1,1]]});

        this._Patterns.push({Name: "Beehive (Still Life)",
        Pattern:[[0,1,1,0],
                 [1,0,0,1],
                 [0,1,1,0]]});

        this._Patterns.push({Name: "Loaf (Still Life)",
        Pattern:[[0,1,1,0],
                 [1,0,0,1],
                 [0,1,0,1],
                 [0,0,1,0]]});

        this._Patterns.push({Name: "Boat (Still Life)",
        Pattern:[[1,1,0],
                 [1,0,1],
                 [0,1,0]]});

        this._Patterns.push({Name: "Tub (Still Life)",
        Pattern:[[0,1,0],
                 [1,0,1],
                 [0,1,0]]});

        this._Patterns.push({Name: "Glider (Spaceship)",
        Pattern:[[1,0,0],
                 [0,1,1],
                 [1,1,0]]});

        this._Patterns.push({Name: "LWSS (Spaceship)",
        Pattern:[[0,1,1,0,0],
                 [1,1,1,1,0],
                 [1,1,0,1,1],
                 [0,0,1,1,0]]});

        this._Patterns.push({Name: "Blinker (Oscillator)",
        Pattern:[[1,1,1]]});

        this._Patterns.push({Name: "Toad (Oscillator)",
        Pattern:[[0,1,1,1],
                 [1,1,1,0]]});

        this._Patterns.push({Name: "Beacon (Oscillator)",
        Pattern:[[1,1,0,0],
                 [1,1,0,0],
                 [0,0,1,1],
                 [0,0,1,1]]});

        this._Patterns.push({Name: "Pentadecathlon (Oscillator)",
        Pattern:[[0,1,0],
                 [0,1,0],
                 [1,1,1],
                 [0,0,0],
                 [0,0,0],
                 [1,1,1],
                 [0,1,0],
                 [0,1,0],
                 [0,1,0],
                 [0,1,0],
                 [1,1,1],
                 [0,0,0],
                 [0,0,0],
                 [1,1,1,],
                 [0,1,0],
                 [0,1,0,]]});

        this._Patterns.push({Name: "Pulsar (Oscillator)",
        Pattern:[[0,0,1,1,1,0,0,0,1,1,1,0,0],
                 [0,0,0,0,0,0,0,0,0,0,0,0,0],
                 [1,0,0,0,0,1,0,1,0,0,0,0,1],
                 [1,0,0,0,0,1,0,1,0,0,0,0,1],
                 [1,0,0,0,0,1,0,1,0,0,0,0,1],
                 [0,0,1,1,1,0,0,0,1,1,1,0,0],
                 [0,0,0,0,0,0,0,0,0,0,0,0,0],
                 [0,0,1,1,1,0,0,0,1,1,1,0,0],
                 [1,0,0,0,0,1,0,1,0,0,0,0,1],
                 [1,0,0,0,0,1,0,1,0,0,0,0,1],
                 [1,0,0,0,0,1,0,1,0,0,0,0,1],
                 [0,0,0,0,0,0,0,0,0,0,0,0,0],
                 [0,0,1,1,1,0,0,0,1,1,1,0,0]]});

        this._Patterns.push({Name: "Gosper's Glider Gun (Gun)", Pattern:[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],
                 [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
                 [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
                 [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
                 [1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
                 [1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
                 [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],
                 [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
                 [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]});
    }
}

As well as random initialisation there are many patterns known to produce specific results with specific rules. This class provides a hard-coded set of sample patterns which can be used to initialize a cellular automaton. You can of course edit this file to add your own. We'll see it in use a bit later.

ca2d_rules.js

class CellularAutomaton2DRules
{
    constructor()
    {
        this._Rules = [];

        this._InitializeRules();
    }

    //---------------------------------------------------
    // PROPERTIES
    //---------------------------------------------------

    get Rules() { return this._Rules; }

    //---------------------------------------------------
    // METHODS
    //---------------------------------------------------

    _InitializeRules()
    {
        // NEIGHBOURS                                                         0 1 2 3 4 5 6 7 8   0 1 2 3 4 5 6 7 8
        // CURRENT                                                            0 0 0 0 0 0 0 0 0   1 1 1 1 1 1 1 1 1
        this._Rules.push({Name: "[no rule]",                         Pattern:[0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0]});
        this._Rules.push({Name: "Game of Life B3/S23",               Pattern:[0,0,0,1,0,0,0,0,0,  0,0,1,1,0,0,0,0,0]});
        this._Rules.push({Name: "Diamoeba B35678/S34678",            Pattern:[0,0,0,1,0,1,1,1,1,  0,0,0,1,1,0,1,1,1]});
        this._Rules.push({Name: "Seeds B2/S",                        Pattern:[0,0,1,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0]});
        this._Rules.push({Name: "Anneal B4678/S35678",               Pattern:[0,0,0,0,1,0,1,1,1,  0,0,0,1,0,1,1,1,1]});
        this._Rules.push({Name: "Replicator B1357/S1357",            Pattern:[0,1,0,1,0,1,0,1,0,  0,1,0,1,0,1,0,1,0]});
        this._Rules.push({Name: "B25/S4",                            Pattern:[0,0,1,0,0,1,0,0,0,  0,0,0,0,1,0,0,0,0]});
        this._Rules.push({Name: "Life Without Death B3/S012345678",  Pattern:[0,0,0,1,0,0,0,0,0,  1,1,1,1,1,1,1,1,1]});
        this._Rules.push({Name: "34 Life B34/S34",                   Pattern:[0,0,0,1,1,0,0,0,0,  0,0,0,1,1,0,0,0,0]});
        this._Rules.push({Name: "2x2 B36/S125",                      Pattern:[0,0,0,1,0,0,1,0,0,  0,1,1,0,0,1,0,0,0]});
        this._Rules.push({Name: "HighLife B36/S23",                  Pattern:[0,0,0,1,0,0,1,0,0,  0,0,1,1,0,0,0,0,0]});
        this._Rules.push({Name: "Day and Night B3678/S34678",        Pattern:[0,0,0,1,0,0,1,1,1,  0,0,0,1,1,0,1,1,1]});
        this._Rules.push({Name: "Morley B368/S245",                  Pattern:[0,0,0,1,0,0,1,0,1,  0,0,1,0,1,1,0,0,0]});
    }
}

The number of possible rules is very large, and this class provides an array of some of the better-known and more interesting ones. As with the patterns you can add more to this file.

Now lets look at the CellularAutomaton2DSVG class which is responsible for drawing the cellular automaton on the screen.

ca2d_svg.js part 1: Constructor

class CellularAutomaton2DSVG
{
    constructor(SVGID, CA)
    {
        this._SVGID = SVGID;
        this._CA = CA;
        this._CellSize = 8;
        this._CellZeroColor = "#000000";
        this._CellOneColor = "#FFFFFF";
        this._GridColor = "#FF0000";
        this._ShowGrid = false;
        this._Width = 0;
        this._Height = 0;
        this._CellElements = null;

        this._CA.StateChangedEventHandlers.push((changed) =>
        {
            this._DrawState(changed);
        });

        this._CA.NumberOfColumnsOrRowsChangedEventHandlers.push(() =>
        {
            this._SetHeightAndWidth(this._CellSize * this._CA.NumberOfRows, this._CellSize * this._CA.NumberOfColumns);
            this._DrawGridAndCells();
        });
    }

The constructor takes as arguments the ID of the SVG element on the page, and also an instance of the CellularAutomaton2D class. It then creates a number of variables with default values. Lastly it adds functions to the cellular automaton's event handler arrays.

For these I have used the arrow notation => which has the big advantage of this actually being this, ie the CellularAutomaton2DSVG object. If you used regular function notation then this would be the object firing the event and we would need to do something like let that = this; before this bit of code, and then use that instead of this inside the event handlers.

ca2d_svg.js part 2: Properties

    //---------------------------------------------------
    // PROPERTIES
    //---------------------------------------------------

    get CellSize() { return this._CellSize; }
    set CellSize(CellSize) { this._CellSize = CellSize; }

    get CellZeroColor() { return this._CellZeroColor; }
    set CellZeroColor(CellZeroColor) { this._CellZeroColor = CellZeroColor; }

    get CellOneColor() { return this._CellOneColor; }
    set CellOneColor(CellOneColor) { this._CellOneColor = CellOneColor; }

    get GridColor() { return this._GridColor; }
    set GridColor(GridColor) { this._GridColor = GridColor; }

    get ShowGrid() { return this._ShowGrid; }
    set ShowGrid(ShowGrid) { this._ShowGrid = ShowGrid; }

This is very straightforward, just a set of getters and setters for various properties.

ca2d_svg.js part 3: Methods

    //---------------------------------------------------
    // METHODS
    //---------------------------------------------------

    _DrawState(changed)
    {
        for(let i = 0, l = changed.length; i < l; i++)
        {
            if(changed[i].State === 0)
            {
                this._CellElements[changed[i].c][changed[i].r].setAttributeNS(null, 'style', "fill:" + this._CellZeroColor + "; stroke:" + this._CellZeroColor + ";");
            }
            else if(changed[i].State === 1)
            {
                this._CellElements[changed[i].c][changed[i].r].setAttributeNS(null, 'style', "fill:" + this._CellOneColor + "; stroke:" + this._CellOneColor + ";");
            }
        }
    }

    _DrawGridAndCells()
    {
        this._MoveToCentre();

        // Variable declarations
        let x = 0;
        let y = 0;
        let line;

        // Create and initialize CellElements array
        this._CellElements = [];
        for(let c = 0; c < this._CA.NumberOfColumns; c++)
        {
            this._CellElements[c] = [];
        }

        // Remove existing stuff
        document.getElementById(this._SVGID).innerHTML = "";

        if(this._ShowGrid === true)
        {
            // Vertical lines
            for (let v = 0; v <= this._CA.NumberOfColumns; v++)
            {
                line = document.createElementNS("http://www.w3.org/2000/svg", "line");

                line.setAttributeNS(null, "x1", x);
                line.setAttributeNS(null, "y1", 0);
                line.setAttributeNS(null, "x2", x);
                line.setAttributeNS(null, "y2", Height);

                line.setAttributeNS(null, "style", "fill:" + this._GridColor + "; stroke:" + this._GridColor + "; stroke-width: 1px;");

                document.getElementById(this._SVGID).appendChild(line);

                x += this._CellSize;
            }

            // Horizontal lines
            for (let h = 0; h <= this._CA.NumberOfRows; h++)
            {
                line = document.createElementNS("http://www.w3.org/2000/svg", "line");

                line.setAttributeNS(null, "x1", 0);
                line.setAttributeNS(null, "y1", y);
                line.setAttributeNS(null, "x2", Width);
                line.setAttributeNS(null, "y2", y);

                line.setAttributeNS(null, "style", "fill:" + this._GridColor + "; stroke:" + this._GridColor + "; stroke-width: 1px;");

                document.getElementById(this._SVGID).appendChild(line);

                y += this._CellSize;
            }
        }

        x = 0;
        y = 0;

        let GridSpacing;
        if(this._ShowGrid)
            GridSpacing = 1;
        else
            GridSpacing = 0;

        // Cells
        for(let c = 0; c < this._CA.NumberOfColumns; c++)
        {
            for(let r = 0; r < this._CA.NumberOfRows; r++)
            {
                this._CellElements[c][r] = document.createElementNS("http://www.w3.org/2000/svg", 'rect');

                this._CellElements[c][r].setAttributeNS(null, 'x', x + GridSpacing);
                this._CellElements[c][r].setAttributeNS(null, 'y', y + GridSpacing);
                this._CellElements[c][r].setAttributeNS(null, 'height', this._CellSize - (GridSpacing * 2));
                this._CellElements[c][r].setAttributeNS(null, 'width', this._CellSize - (GridSpacing * 2));
                this._CellElements[c][r].setAttributeNS(null, "style", "fill:" + this._CellZeroColor + "; stroke:" + this._CellZeroColor     + "; stroke-width: 1px;");

                this._CellElements[c][r].setAttributeNS(null, "class", "CACell");
                this._CellElements[c][r].setAttributeNS(null, "data-row", r);
                this._CellElements[c][r].setAttributeNS(null, "data-column", c);

                document.getElementById(this._SVGID).appendChild(this._CellElements[c][r]);

                y += this._CellSize;
            }

            x += this._CellSize;
            y = 0;
        }

        let cells = document.getElementsByClassName("CACell");
        for(let i = 0, l = cells.length; i < l; i++)
        {
            cells[i].onclick = function()
            {
                let Row = this.getAttribute("data-row");
                let Column = this.getAttribute("data-column");

                FireCellClickedEvent(Row, Column);
            };
        }
    }

    _SetHeightAndWidth(height, width)
    {
        this._Height = height;
        document.getElementById(this._SVGID).setAttribute("height", height);

        this._Width = width;
        document.getElementById(this._SVGID).setAttribute("width", width);
    }

    _MoveToCentre(height, width)
    {
        let SVGDivWidth = document.getElementById("svgdiv").offsetWidth;
        let SVGDivHeight = document.getElementById("svgdiv").offsetHeight;

        let SVGDivLeft = (SVGDivWidth / 2) - (this._Width / 2);
        let SVGDivTop = (SVGDivHeight / 2) - (this._Height / 2);

        document.getElementById(this._SVGID).style.top = SVGDivTop + 'px';
        document.getElementById(this._SVGID).style.left = SVGDivLeft + 'px';
    }
}

_DrawState

This function simply iterates the array of cells which have been changed, setting their colour.

_DrawGridAndCells

Here we create the SVG elements representing each cell, and also the grid lines if they are to be shown. They are stored in a 2D array and then added to the main SVG element.

_SetHeightAndWidth

A simple little function which sets the main SVG element's size.

_MoveToCentre

Sets the position of the main SVG element to the centre of its parent.

Time to Run

That's the coding finished so open ca1d.htm in your browser. Click one of the initialize buttons, select a rule and click the Start/Stop button. Here are a couple more samples.

Quo Vadis?

At the moment this project is pretty much the bare minimum required to get a cellular automaton up and running. There are many possible enhancements and a few I have lined up for the next version are:

  • Creating and saving new rules via the UI
  • Creating and saving new initialization patterns via the UI
  • Adding a selection of built-in colour schemes
  • Saving the state as a graphic file

Please follow Code in JavaScript on Twitter for news of future posts and other useful JavaScript stuff.