// LifeViewer engine
// Implements many rule families and rendering.

/*
This file is part of LifeViewer
 Copyright (C) 2015-2026 Chris Rowett

 LifeViewer is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

	// Life constants
	/** @const */
	var LifeConstants = {
		// how long to show notification before Identify phase 2 begins
		/** @const {number} */ identifyNotificationDuration : 15,

		// safe update period in ms (just under 3 per second)
		/** @type {number} */ safeUpdatePeriod : 350,

		// cell shader modes for 2-state Life-like patterns
		/** @const {number} */ shaderBasic : 0,
		/** @const {number} */ shaderCellAge : 1,
		/** @const {number} */ shaderRainbow : 2,
		/** @const {number} */ shaderNeighbourCount : 3,

		// maximum number of generations for quick oscillator detection
		/** @const {number} */ maxOscillatorGens : 4194304,

		// maximum number of generations for short oscillator buffer
		/** @const {number} */ negOscillatorGens : 256,

		// how often in seconds to save an Identify Snapshot after quick detection completes
		/** @const {number} */ identifySnapshotInterval : 300,

		// RLE options (bit fields)
		/** @const {number} */ rleBlanks : 1,
		/** @const {number} */ rleNoNewLines : 2,
		/** @const {number} */ rleNoHeader : 4,
		/** @const {number} */ rleComments : 8,

		// state modes
		/** @const {number} */ mode2 : 0,
		/** @const {number} */ mode2Table : 1,
		/** @const {number} */ mode2History : 2,
		/** @const {number} */ modeAny : 3,

		// cell period table State 6 label for [R]History and [R]Super
		/** @const {string} */ state6Label : "Off DF",

		// cell period table State 3 label for [R]Extended
		/** @const {string} */ state3Label : "Off DF",

		// number of snowflakes
		/** @const {number} */ flakes : 50000,

		// transformation for mod hash
		/** @const {number} */ modRot90 : 0,
		/** @const {number} */ modRot270 : 1,
		/** @const {number} */ modFlipX : 2,
		/** @const {number} */ modFlipY : 3,
		/** @const {number} */ modRot180 : 4,
		/** @const {number} */ modRot90FlipX : 5,
		/** @const {number} */ modRot90FlipY : 6,

		// transformation combinations
		/** @const {number} */ modFlipXorY : 7,
		/** @const {number} */ modRotCWorCCW : 8,
		/** @const {number} */ modFlipXorYorRotCWorCCW : 9,
		/** @const {number} */ modFlipDiag: 10,
		/** @const {number} */ modFlipDiagorRot90 : 11,
		/** @const {number} */ modFlipXorRot180 : 12,
		/** @const {number} */ modFlipYorRot180 : 13,
		/** @const {number} */ modFlipDiagLRot180 : 14,
		/** @const {number} */ modFlipDiagRRot180 : 15,
		/** @const {number} */ modFlipOrthorDiag : 16,

		// transformations to check for oscillators
		/** @const {Array<number>} */ modOscillator : [0, 1, 2, 3, 4, 5, 6],

		// transformations to check for spaceships
		/** @const {Array<number>} */ modSpaceship : [2, 3, 5, 6],

		// cell types for rotor/stator calculations
		/** @const {number} */ cellWasDead : 0,
		/** @const {number} */ cellWasAlive : 1,
		/** @const {number} */ cellHasChanged : 2,

		// maximum number of bits for rule tree/table lookup
		/** @const {number} */ maxRuleTreeLookupBits : 27,

		// maximum number of bits for rule tree partial lookup
		/** @const {number} */ maxRuleTreePartialLookupBits : 36,

		// mod type names
		/** @const {Array<string>} */ modTypeName : ["Rot90CW", "Rot90CCW", "FlipX", "FlipY", "Rot180", "Flip" + String.fromCharCode(10189), "Flip" + String.fromCharCode(10187), "FlipOrth", "Rot90", "FlipOrthOrRot90", "FlipDiag", "FlipDiagOrRot90", "FlipXOrRot180", "FlipYOrRot180", "Flip" + String.fromCharCode(10189) + "OrRot180", "Flip" + String.fromCharCode(10187) + "OrRot180", "FlipOrthOrDiag"],

		// maxmimum memory to compute strict volatility
		/** @const {number} */ maxStrictMemory : 256 * 1024 * 1024,

		// buffer full
		/** @const {string} */ bufferFullMessage : "Buffer Full",

		// Golly Theme colours for regular and [R]History states
		/** @const {ColourRange} */ gollyStandardAliveRange : new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)),
		/** @const {ColourRange} */ gollyHistoryAliveRange : new ColourRange(new Colour(0, 255, 0), new Colour(0, 255, 0)),
		/** @const {ColourRange} */ gollyStandardDeadRange : new ColourRange(new Colour(48, 48, 48), new Colour(48, 48, 48)),
		/** @const {ColourRange} */ gollyHistoryDeadRange : new ColourRange(new Colour(0, 0, 128), new Colour(0, 0, 128)),

		// [R]Extended state colours
		/** @const {Array<Array<number>>} */ coloursExtended : [[0, 0, 0], [0, 236, 91], [0, 192, 254], [254, 0, 0], [254, 254, 254], [75, 75, 75],
															 [233, 41, 254], [64, 0, 128], [254, 230, 0], [150, 128, 0], [130, 200, 0],
															 [0, 120, 40], [254, 140, 0], [140, 70, 0], [0, 0, 254], [192, 192, 192],
															 [128, 128, 128], [254, 112, 140], [174, 0, 168], [0, 152, 127], [0, 73, 59]],

		// [R]Extended state names
		/** @const {Array<string>} */ namesExtended : ["off", "on", "killer on", "killer off", "eternal on", "eternal off",
										"p2 killer on", "p2 killer off", "egg on", "egg off", "p2 on",
										"p2 off", "p2 egg on", "p2 egg off", "inverter", "catalyst",
										"catalyst killer", "on births", "off births", "on p2 births", "off p2 births"],

		// [R]Super state colours
		/** @const {Array<Array<number>>} */ coloursSuper : [[0, 0, 0], [0, 255, 0], [0, 0, 160], [255, 216, 255], [255, 0, 0], [255, 255, 0],
															 [96, 96, 96], [255, 105, 180], [128, 0, 128], [0, 191, 255], [0, 64, 128],
															 [64, 224, 208], [0, 128, 64], [255, 255, 255], [255, 99, 71], [250, 128, 114],
															 [219, 112, 147], [255, 165, 0], [245, 222, 179], [0, 255, 255], [192, 192, 192],
															 [192, 255, 128], [255, 182, 193], [0, 255, 127], [1, 1, 1], [255, 0, 127]],

		// [R]Super state names
		/** @const {Array<string>} */ namesSuper : ["dead", "on", "off", "marked on trail", "marked off trail", "marked on alt", "boundary",
													"marked on no-trail", "marked off no-trail", "on trail 2", "off trail 2", "on trail 3", "off trail 3",
													"on no-trail 1", "off disappearing", "on no-trail 2", "off vanishing", "on no-trail 3", "off cycling 1",
													"on no-trail 4", "off cycling 2", "on no-trail 5", "off cycling 3", "on no-trail 6", "hidden", "on no-trail 7"],

		// NW glider
		/** @const {Array<Array<number>>} */ gliderNW0 : [[1, 1, 1],
										  [1, 0, 0],
										  [0, 1, 0]],

		/** @const {Array<Array<number>>} */ gliderNW1 : [[0, 1, 0],
										  [1, 1, 0],
										  [1, 0, 1]],

		/** @const {Array<Array<number>>} */ gliderNW2 : [[1, 1, 0],
										  [1, 0, 1],
										  [1, 0, 0]],

		/** @const {Array<Array<number>>} */ gliderNW3 : [[0, 1, 1],
										  [1, 1, 0],
										  [0, 0, 1]],

		// NE glider
		/** @const {Array<Array<number>>} */ gliderNE0 : [[1, 1, 1],
										  [0, 0, 1],
										  [0, 1, 0]],

		/** @const {Array<Array<number>>} */ gliderNE1 : [[0, 1, 0],
										  [0, 1, 1],
										  [1, 0, 1]],

		/** @const {Array<Array<number>>} */ gliderNE2 : [[0, 1, 1],
										  [1, 0, 1],
										  [0, 0, 1]],

		/** @const {Array<Array<number>>} */ gliderNE3 : [[1, 1, 0],
										  [0, 1, 1],
										  [1, 0, 0]],

		// SW glider
		/** @const {Array<Array<number>>} */ gliderSW0 : [[0, 1, 0],
										  [1, 0, 0],
										  [1, 1, 1]],

		/** @const {Array<Array<number>>} */ gliderSW1 : [[1, 0, 1],
										  [1, 1, 0],
										  [0, 1, 0]],

		/** @const {Array<Array<number>>} */ gliderSW2 : [[1, 0, 0],
										  [1, 0, 1],
										  [1, 1, 0]],

		/** @const {Array<Array<number>>} */ gliderSW3 : [[0, 0, 1],
										  [1, 1, 0],
										  [0, 1, 1]],

		// SE glider
		/** @const {Array<Array<number>>} */ gliderSE0 : [[0, 1, 0],
										  [0, 0, 1],
										  [1, 1, 1]],

		/** @const {Array<Array<number>>} */ gliderSE1 : [[1, 0, 1],
										  [0, 1, 1],
										  [0, 1, 0]],

		/** @const {Array<Array<number>>} */ gliderSE2 : [[0, 0, 1],
										  [1, 0, 1],
										  [0, 1, 1]],

		/** @const {Array<Array<number>>} */ gliderSE3 : [[1, 0, 0],
										  [0, 1, 1],
										  [1, 1, 0]],

		// glider orientations
		/** @const {number} */ gliderNE : 0,
		/** @const {number} */ gliderNW : 1,
		/** @const {number} */ gliderSE : 2,
		/** @const {number} */ gliderSW : 3,

		// glider bounding box edge
		/** @const {number} */ gliderN: 0,
		/** @const {number} */ gliderE: 1,
		/** @const {number} */ gliderS: 2,
		/** @const {number} */ gliderW: 3,

		// hex and triangle cell coordinate buffer size
		/** @const {number} */ coordBufferSize : 4096,

		// hex cell bits for buffer (must be coordBufferSize * 16 bits big)
		/** @const {number} */ coordBufferBits : 16,

		// triangle line width
		/** @const {number} */ triangleLineWidth : 1.6,

		// remove pattern cell buffer (must be power of 2)
		/** @const {number} */ removePatternBufferSize : 4096,

		// size of 3x3 hash lookup array
		/** @const {number} */ hash33 : 512,

		// size of 6x3 hash lookup array
		/** @const {number} */ hash63 : 262144,

		// size of Margolus lookup array
		/** @const {number} */ hashMargolus : 65536,

		// size of PCA lookup array
		/** @const {number} */ hashPCA : 65536,

		// size of 13bit triangular single cell hash lookup array
		/** @const {number} */ hashTriangular : 8192,

		// size of 17bit triangular double cell hash lookup array (includes odd/even bit)
		/** @const {number} */ hashTriDouble : 131072,

		// snapshot interval
		/** @const {number} */ snapshotInterval : 64,

		// maximum number of population samples for graph
		/** @const {number} */ maxPopSamples : 524288,

		// population samples chunk size 2^n
		/** @const {number} */ popChunkPower : 12,

		// cell value for alive start (for 2-state Themes)
		/** @const {number} */ aliveStart : 64,

		// cell value for alive maximum (for 2-state Themes)
		/** @const {number} */ aliveMax : 127,

		// cell colour index for cell just dead (for 2-state Themes)
		/** @const {number} */ deadStart : 63,

		// cell colour index for cell dead longest (for 2-state Themes)
		/** @const {number} */ deadMin : 1,

		// bit masks for surrounding tiles
		/** @const {number} */ leftSet : 1,
		/** @const {number} */ rightSet : 2,
		/** @const {number} */ topSet : 4,
		/** @const {number} */ bottomSet : 8,
		/** @const {number} */ topLeftSet : 16,
		/** @const {number} */ topRightSet : 32,
		/** @const {number} */ bottomLeftSet : 64,
		/** @const {number} */ bottomRightSet : 128,

		// predefined Cell Period map subperiod colours
		/** @const <Array{string}> */ subPeriodColours : [
			"#1C92CD",
			"#0AB87B",
			"#E86075",
			"#F8F290",
			"#BA4117",
			"#D91E9B",
			"#BEF0E9",
			"#428C28",
			"#6F1044",
			"#76ADF4",
			"#2D5963",
			"#D9B790",
			"#80FF80",
			"#9090FF",
			"#C0C000",
			"#80FFFF",
			"#FFC0FF",
			"#FFC0C0",
			"#FFFF00",
			"#FF8000",
		]
	};

	// ModCheck object
	/**
	 * @constructor
	 */
	function ModCheck(/** @type {number} */ checkGen, /** @type {number} */ modType) {
		/** @type {number} */ this.checkGen = checkGen;
		/** @type {number} */ this.modType = modType;
	}

	// Theme object
	/**
	 * @constructor
	 */
	function Theme(/** @type {string} */ name, /** @type {ColourRange} */ deadRange, /** @type {ColourRange} */ aliveRange, /** @type {Colour} */ unoccupied, /** @type {Colour} */ aliveGen, /** @type {ColourRange} */ dyingRangeGen, /** @type {ColourRange} */ deadRangeGen, /** @type {Colour} */ unoccupiedGen, /** @type {Array<Colour>} */ pcaCols) {
		/** @type {string} */ this.name = name;
		/** @type {boolean} */ this.gridDefined = false;
		/** @type {number} */ this.gridMajor = 10;
		/** @type {number} */ this.gridColour = 0;
		/** @type {number} */ this.gridMajorColour = 0;

		// 2-state theme
		/** @type {Colour} */ this.unoccupied = unoccupied;
		/** @type {ColourRange} */ this.aliveRange = aliveRange;
		/** @type {ColourRange} */ this.deadRange = deadRange;

		// generations theme
		/** @type {Colour} */ this.aliveGen = aliveGen;
		/** @type {ColourRange} */ this.dyingRangeGen = dyingRangeGen;
		/** @type {ColourRange} */ this.deadRangeGen = deadRangeGen;
		/** @type {Colour} */ this.unoccupiedGen = unoccupiedGen;
		/** @type {boolean} */ this.dyingRangeDynamic = false;

		// PCA theme
		/** @type {Array<Colour>} */ this.pcaCols = pcaCols;

		// check for dynamic dying range
		if (this.dyingRangeGen.endColour.red === -1) {
			this.dyingRangeDynamic = true;
		}

		// pick light or dark grid lines based on theme background
		if (((this.unoccupied.red + this.unoccupied.green + this.unoccupied.blue) / 3) >= 128) {
			this.gridColour = ViewConstants.gridLineLightRawDefault;
			this.gridMajorColour = ViewConstants.gridLineLightBoldRawDefault;
		} else {
			this.gridColour = ViewConstants.gridLineRawDefault;
			this.gridMajorColour = ViewConstants.gridLineBoldRawDefault;
		}
	}

	// copy theme
	Theme.prototype.set = function(/** @type {Theme} */ source) {
		var	/** @type {number} */ i = 0;

		this.gridDefined = source.gridDefined;
		this.gridMajor = source.gridMajor;
		this.gridColour = source.gridColour;
		this.gridMajorColour = source.gridMajorColour;

		this.unoccupied.set(source.unoccupied);
		this.aliveRange.set(source.aliveRange);
		this.deadRange.set(source.deadRange);

		this.aliveGen.set(source.aliveGen);
		this.dyingRangeGen.set(source.dyingRangeGen);
		this.deadRangeGen.set(source.deadRangeGen);
		this.unoccupiedGen.set(source.unoccupiedGen);
		this.dyingRangeGen.set(source.dyingRangeGen);

		this.dyingRangeDynamic = source.dyingRangeDynamic;

		for (i = 0; i < source.pcaCols.length; i += 1) {
			this.pcaCols[i].set(source.pcaCols[i]);
		}
	};

	// add grid line settings to the theme
	Theme.prototype.setGridLines = function(/** @type {number} */ gridMajor, /** @type {Colour} */ gridColour, /** @type {Colour} */ gridMajorColour) {
		this.gridDefined = true;
		this.gridMajor = gridMajor;
		this.gridColour = (gridColour.red << 16) | (gridColour.green << 8) | (gridColour.blue);
		this.gridMajorColour = (gridMajorColour.red << 16) | (gridMajorColour.green << 8) | (gridMajorColour.blue);
	};

	// set custom grid lines colours
	Theme.prototype.setGridLineColours = function(/** @type {number} */ gridColour, /** @type {number} */ gridMajorColour) {
		// check if the grid lines colour was specified
		if (gridColour !== -1) {
			// yes so set it
			this.gridColour = gridColour;
		}

		// check if the major grid lines colour was specified
		if (gridMajorColour !== -1) {
			// yes so set it
			this.gridMajorColour = gridMajorColour;
		}
	};

	// check if theme has colour history
	/** @returns {boolean} */
	Theme.prototype.hasHistory = function(/** @type {boolean} */ isLifeHistory, /** @type {View} */ view) {
		var	/** @type {boolean} */ result = true;

		// always return true if the pattern is [R]History since history states are saved in RLE
		if (!isLifeHistory) {
			// check if the alive start and end colour are the same, the dead start and end colour are the same and the dead and unoccupied colour are the same
			if ((this.aliveRange.startColour.isSameColour(this.aliveRange.endColour)) && (this.deadRange.startColour.isSameColour(this.deadRange.endColour)) && (this.deadRange.startColour.isSameColour(this.unoccupied))) {
				result = false;
			}

			// check if there are ALIVESTATES and HISTORYSTATES are both 0
			if (view.historyStates === 0 && view.aliveStates === 0) {
				result = false;
			}
		}

		return result;
	};

	// Colour object
	/**
	 * @constructor
	 */
	function Colour(/** @type {number} */ red, /** @type {number} */ green, /** @type {number} */ blue) {
		/** @type {number} */ this.red = red;
		/** @type {number} */ this.green = green;
		/** @type {number} */ this.blue = blue;
	}

	// set function
	Colour.prototype.set = function(/** @type {Colour} */ fromColour) {
		this.red = fromColour.red;
		this.green = fromColour.green;
		this.blue = fromColour.blue;
	};

	// is same colour function
	/** @returns {boolean} */
	Colour.prototype.isSameColour = function(/** @type {Colour} */ compareColour) {
		var	/** @type {boolean} */ result = false;

		// check if components are identical
		if ((this.red === compareColour.red) && (this.green === compareColour.green) && (this.blue === compareColour.blue)) {
			// colours are the same
			result = true;
		}

		// return whether the colours were the same
		return result;
	};

	// ColourRange object
	/**
	 * @constructor
	 */
	function ColourRange(/** @type {Colour} */ startColour, /** @type {Colour} */ endColour) {
		/** @type {Colour} */ this.startColour = startColour;
		/** @type {Colour} */ this.endColour = endColour;
	}

	// set function
	ColourRange.prototype.set = function(/** @type {ColourRange} */ fromRange) {
		this.startColour.set(fromRange.startColour);
		this.endColour.set(fromRange.endColour);
	};

	// Life object
	/**
	 * @constructor
	 */
	function Life(/** @type {CanvasRenderingContext2D} */ context, /** @type {number} */ displayWidth, /** @type {number} */ displayHeight, /** @type {number} */ gridWidth, /** @type {number} */ gridHeight, /** @type {PatternManager} */ manager, /** @type {View} */ view) {
		// pattern manager
		/** @type {PatternManager} */ this.manager = manager;

		// view
		/** @type {View} */ this.view = view;

		// whether snapshot save is required
		/** @type {boolean} */ this.snapshotNeeded = false;

		// hash lower and upper 32 bits
		/** @type {number} */ this.hashLower = 0;
		/** @type {number} */ this.hashUpper = 0;

		// canvas for cell icons at supported resolutions
		/** @type {HTMLCanvasElement} */ this.cellIconCanvas31 = null;
		/** @type {HTMLCanvasElement} */ this.cellIconCanvas15 = null;
		/** @type {HTMLCanvasElement} */ this.cellIconCanvas7 = null;
		/** @type {CanvasRenderingContext2D} */ this.cellIconContext31 = null;
		/** @type {CanvasRenderingContext2D} */ this.cellIconContext15 = null;
		/** @type {CanvasRenderingContext2D} */ this.cellIconContext7 = null;
		/** @type {ImageData} */ this.cellIconImageData31 = null;
		/** @type {ImageData} */ this.cellIconImageData15 = null;
		/** @type {ImageData} */ this.cellIconImageData7 = null;

		/** @type {boolean} */ this.iconsAvailable = false;

		// canvas to scale cell icons
		/** @type {HTMLCanvasElement} */ this.scaleIconCanvas = null;

		// canvas for cell period map
		/** @type {HTMLCanvasElement} */ this.cellPeriodCanvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
		this.cellPeriodCanvas.width = 1;
		this.cellPeriodCanvas.height = 1;
		/** @type {CanvasRenderingContext2D} */ this.cellPeriodContext = /** @type {!CanvasRenderingContext2D} */ (this.cellPeriodCanvas.getContext("2d", {"willReadFrequently": true}));
		/** @type {Array<number>} */ this.cellPeriodRGB = [];
		/** @type {number} */ this.cellPeriodNumCols = 0;
		/** @type {number} */ this.cellPeriodCellSize = 8;
		/** @type {number} */ this.cellBorderSize = 1;
		/** @type {number} */ this.cellPeriodState6 = 0;

		// canvas for cell frequency map
		/** @type {HTMLCanvasElement} */ this.cellFrequencyCanvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
		this.cellFrequencyCanvas.width = 1;
		this.cellFrequencyCanvas.height = 1;
		/** @type {CanvasRenderingContext2D} */ this.cellFrequencyContext = /** @type {!CanvasRenderingContext2D} */ (this.cellFrequencyCanvas.getContext("2d", {"willReadFrequently": true}));
		/** @type {number} */ this.cellFrequencyNumCols = 0;
		/** @type {Array<number>} */ this.cellFrequencyRGB = [];
		/** @type {Uint32Array} */ this.cellFrequencyCounts = null;

		// identify temporary results
		/** @type {string} */ this.identifyMessage = "";
		/** @type {number} */ this.identifyPeriod = 0;
		/** @type {number} */ this.identifyDeltaX = 0;
		/** @type {number} */ this.identifyDeltaY = 0;
		/** @type {number} */ this.identifyBoxWidth = 0;
		/** @type {number} */ this.identifyBoxHeight = 0;
		/** @type {string} */ this.identifyPopWithTMessage = "";
		/** @type {string} */ this.identifyBoxWithTMessage = "";

		// identify deferred results
		/** @type {Array} */ this.identifyDeferredResults = [];
		/** @type {number} */ this.identifyDeferredCounter = -1;

		// identify min/max/average popultation and generation
		/** @type {number} */ this.identifyMinPop = 0;
		/** @type {number} */ this.identifyMinPopT = 0;

		/** @type {number} */ this.identifyMaxPop = 0;
		/** @type {number} */ this.identifyMaxPopT = 0;

		/** @type {number} */ this.identifyAveragePop = 0;

		// min and max box width and height and generation
		/** @type {number} */ this.identifyMinBoxWidth = 0;
		/** @type {number} */ this.identifyMinBoxHeight = 0;
		/** @type {number} */ this.identifyMinBoxSizeT = 0;

		/** @type {number} */ this.identifyMaxBoxWidth = 0;
		/** @type {number} */ this.identifyMaxBoxHeight = 0;
		/** @type {number} */ this.identifyMaxBoxSizeT = 0;

		/** @type {number} */ this.identifyMinX = 0;
		/** @type {number} */ this.identifyMinY = 0;
		/** @type {number} */ this.identifyFullWidth = 0;
		/** @type {number} */ this.identifyFullHeight = 0;

		// whether any cells were not in rotor
		/** @type {boolean} */ this.identifyAllCells = false;

		/** @type {number} */ this.identifyLastSnapshotTime = 0;

		// period table start row and maximum row
		/** @type {number} */ this.tableStartRow = 0;
		/** @type {number} */ this.tableMaxRow = 0;
		/** @type {number} */ this.tablePageSize = 10;

		// last computed strict volatility
		/** @type {string} */ this.strictVol = "";

		// minimum and maximum heat
		/** @type {number} */ this.minHeat = 0;
		/** @type {number} */ this.maxHeat = 0;
		/** @type {number} */ this.heatVal = 0;

		// last period per cell for oscillator
		/** @type {Int32Array} */ this.cellPeriod = null;
		/** @type {number} */ this.cellPeriodWidth = 0;
		/** @type {number} */ this.cellPeriodHeight = 0;
		/** @type {number} */ this.origCellPeriodWidth = 0;

		// oscillator population subperiod
		/** @type {Uint32Array} */ this.popSubPeriod = null;

		// oscillator population subperiod help list
		/** @type {Array<number>} */ this.popSubPeriodHelp = [];

		// oscillator population total
		/** @type {number} */ this.popTotal = 0;

		// oscillator rotor population
		/** @type {number} */ this.popRotor = 0;

		// oscillator unique cell counts
		/** @type {Uint32Array} */ this.uniqueCellCounts = null;

		// oscillator unique cell counts count (count of number of cells with each unique count)
		/** @type {Uint32Array} */ this.uniqueCellCountsCounts = null;

		// total number of cells
		/** @type {number} */ this.cellCountsTotal = 0;

		// identify start and elapsed time
		this.identifyStartTime = 0;
		this.identifyElapsedTime = 0;
		this.identifyDetectionTime = 0;

		// whether last zoom was < 1/16x
		/** @type {boolean} */ this.lastZoom16 = true;

		// allocator
		/** @type {Allocator} */ this.allocator = WASM.allocator;

		// list of potential gliders to clear
		/** @type {Array} */ this.potentialClears = [];

		// 2-state renderer
		/** @type {number} */ this.cellRenderer = LifeConstants.shaderCellAge;

		// whether to draw grid
		/** @type {boolean} */ this.doDrawGrid = true;

		// whether pretty rendering enabled
		/** @type {boolean} */ this.pretty = false;

		// scaling canvas
		/** @type {HTMLCanvasElement} */ this.sCanvas = null;
		/** @type {CanvasRenderingContext2D} */ this.sContext = null;
		/** @type {ImageData} */ this.sImageData = null;
		/** @type {Uint32Array} */ this.sData32 = null;

		// icon canvas
		/** @type {HTMLCanvasElement} */ this.iconCanvas = null;
		/** @type {CanvasRenderingContext2D} */ this.iconContext = null;
		/** @type {ImageData} */ this.iconImageData = null;

		// snow flakes (x, y, dy)
		/** @type {Uint16Array} */ this.snowX = null;
		/** @type {Float32Array} */ this.snowY = null;
		/** @type {Float32Array} */ this.snowDY = null;

		// snow flake revive list
		/** @type {Uint32Array} */ this.snowRevive = null;

		// initialise random number generator
		/** @type {Random} */ this.randGen = new Random();
		this.randGen.init(Date.now().toString());

		// whether current rule is RuleTree
		/** @type {boolean} */ this.isRuleTree = false;
		/** @type {number} */ this.ruleTreeNeighbours = 8;
		/** @type {number} */ this.ruleTreeStates = 2;
		/** @type {number} */ this.ruleTreeNodes = 0;
		/** @type {number} */ this.ruleTreeBase = -1;
		/** @type {Uint32Array} */ this.ruleTreeA = null;
		/** @type {Uint8Array} */ this.ruleTreeB = null;
		/** @type {Uint32Array} */ this.ruleTreeColours = null;
		/** @type {Array} */ this.ruleTableIcons = null;

		// RuleTable
		/** @type {Array} */ this.ruleTableLUT = [];
		/** @type {Uint32Array} */ this.flatRuleTableLUT = null;
		/** @type {Uint8Array} */ this.ruleTableOutput = null;
		/** @type {number} */ this.ruleTableCompressedRules = 0;
		/** @type {number} */ this.ruleTableNeighbourhood = 0;
		/** @type {number} */ this.ruleTableDups = 0;
		/** @type {boolean} */ this.ruleTableB0 = false;

		// identify lists
		/** @type {Int32Array} */ this.hashListLower = null;
		/** @type {Int32Array} */ this.hashListUpper = null;
		/** @type {Uint32Array} */ this.boxList = null;
		/** @type {Int32Array} */ this.negHashListLower = null;
		/** @type {Int32Array} */ this.negHashListUpper = null;
		/** @type {Uint32Array} */ this.negGenList = null;
		/** @type {Uint32Array} */ this.negPopList = null;
		/** @type {Uint32Array} */ this.negBoxList = null;
		/** @type {number} */ this.oscLength = 0;
		/** @type {number} */ this.fullOscLength = 0;
		/** @type {Array<Uint8Array>} */ this.countList = null;
		/** @type {Array<Uint8Array>} */ this.initList = null;
		/** @type {number} */ this.modValue = -1;
		/** @type {number} */ this.modType = -1;
		/** @type {Uint16Array} */ this.occMap = null;
		/** @type {Array<Uint16Array>} */ this.occTileMap = null;
		/** @type {Array<Uint16Array>} */ this.occMergedTileMap = null;

		// flag for alternating Margolus grid lines
		/** @type {boolean} */ this.altGrid = true;

		// flag for reverse Margolus playback
		/** @type {boolean} */ this.reverseMargolus = false;

		// flag for reverse pending
		/** @type {boolean} */ this.reversePending = false;

		// flag whether alternate rules specified
		/** @type {boolean} */ this.altSpecified = false;

		// flag whether shrink after edit needed
		/** @type {boolean} */ this.shrinkNeeded = false;

		// flag whether to clear escaping gliders
		/** @type {boolean} */ this.clearGliders = false;

		// count of escaping gliders cleared
		/** @type {number} */ this.numClearedGliders = 0;

		// gliders in 7x7 cell array
		/** @type {Array<Array<number>>} */ this.gliderNW07x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderNW17x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderNW27x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderNW37x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderNE07x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderNE17x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderNE27x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderNE37x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSW07x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSW17x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSW27x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSW37x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSE07x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSE17x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSE27x7 = [];
		/** @type {Array<Array<number>>} */ this.gliderSE37x7 = [];

		// cell colour strings
		/** @type {Array<string>} */ this.cellColourStrings = [];

		// before and after RLE comments
		/** @type {string} */ this.beforeTitle = "";
		/** @type {string} */ this.afterTitle = "";

		// bounded grid border colour
		/** @type {number} */ this.boundedBorderColour = 255;

		// custom state colours
		/** @type {Int32Array} */ this.customColours = null;

		// bit counts for 16bit values
		/** @type {Uint8Array} */ this.bitCounts16 = Controller.bitCounts16;

		// population graph array
		/** @type {Array<Uint32Array>} */ this.popGraphData = null;
		/** @type {Array<Uint32Array>} */ this.birthGraphData = null;
		/** @type {Array<Uint32Array>} */ this.deathGraphData = null;

		// population graph entries
		/** @type {number} */ this.popGraphEntries = 0;

		// maximum data values for graph
		/** @type {number} */ this.maxPopValue = 0;
		/** @type {number} */ this.maxBirthsValue = 0;
		/** @type {number} */ this.maxDeathsValue = 0;

		// maximum value for selected graph data types
		/** @type {number} */ this.maxDataValue = 0;

		// x, y and z origin
		/** @type {number} */ this.originX = 0;
		/** @type {number} */ this.originY = 0;
		/** @type {number} */ this.originZ = 1;

		// bounded grid type
		/** @type {number} */ this.boundedGridType = -1;

		// bounded grid size
		/** @type {number} */ this.boundedGridWidth = -1;
		/** @type {number} */ this.boundedGridHeight = -1;

		// bounded grid shifts
		/** @type {number} */ this.boundedGridHorizontalShift = 0;
		/** @type {number} */ this.boundedGridVerticalShift = 0;

		// bounded grid twists
		/** @type {boolean} */ this.boundedGridHorizontalTwist = false;
		/** @type {boolean} */ this.boundedGridVerticalTwist = false;

		// bounded grid sphere axis
		/** @type {boolean} */ this.boundedGridSphereAxisTopLeft = true;

		// remove pattern radius
		/** @type {number}*/ this.removePatternRadius = ViewConstants.defaultDeleteRadius;

		// stack for boundary pattern clear
		/** @type {Array<Int32Array>} */ this.boundaryX = [];
		/** @type {Array<Int32Array>} */ this.boundaryY = [];
		/** @type {Int32Array} */ this.boundaryX[0] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, LifeConstants.removePatternBufferSize, "Life.boundaryX0", false));
		/** @type {Int32Array} */ this.boundaryY[0] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, LifeConstants.removePatternBufferSize, "Life.boundaryY0", false));

		// number of boundary pages
		/** @type {number} */ this.boundaryPages = 0;

		// boundary colour
		/** @type {number} */ this.boundaryColour = 0xffffffff;

		// boundary colour string (for hexagons and triangles)
		/** @type {string} */ this.boundaryColourString = "rgb(255,255,255)";

		// bounded grid border colour
		/** @type {number} */ this.boundedColour = 0xff808080;

		// select colour
		/** @type {string} */ this.selectColour = "rgb(0,255,0)";

		// paste colour
		/** @type {string} */ this.pasteColour = "rgb(255,0,0)";

		// selected cells colour
		/** @type {string} */ this.selectedCellsColour = "rgb(255,128,0)";

		// whether rule is _none_
		/** @type {boolean} */ this.isNone = false;

		// whether rule is Margolus
		/** @type {boolean} */ this.isMargolus = false;

		// whether rule is PCA
		/** @type {boolean} */ this.isPCA = false;

		// whether rule is Wolfram
		/** @type {number} */ this.wolframRule = -1;

		// whether neighbourhood is triangular
		/** @type {boolean} */ this.isTriangular = false;

		// triangular neighbourhood
		/** @type {number} */ this.triangularNeighbourhood = this.manager.triangularAll;

		// whether neighbourhood is hex
		/** @type {boolean} */ this.isHex = false;

		// hex neighbourhood
		/** @type {number} */ this.hexNeighbourhood = this.manager.hexAll;

		// whether to draw cells as rectangles with hexagonal or triangular grids
		/** @type {boolean} */ this.forceRectangles = false;

		// whether to display square or hex grid
		/** @type {boolean} */ this.patternDisplayMode = false;

		// whether neighbourhood is Von Neumann
		/** @type {boolean} */ this.isVonNeumann = false;

		// number of states for multi-state rules (Generations or HROT)
		/** @type {number} */ this.multiNumStates = -1;

		// whether pattern is [R]History
		/** @type {boolean} */ this.isLifeHistory = false;

		// whether pattern is [R]Super
		/** @type {boolean} */ this.isSuper = false;

		// whether pattern is [R]Extended
		/** @type {boolean} */ this.isExtended = false;

		// how many history states to draw
		/** @type {number} */ this.historyStates = 0;

		// how many alive states to draw
		/** @type {number} */ this.aliveStates = 0;

		// whether pattern is HROT
		/** @type {boolean} */ this.isHROT = false;

		// whether to draw overlay
		/** @type {boolean} */ this.drawOverlay = false;

		// create the snapshot manager
		/** @type {SnapshotManager} */ this.snapshotManager = new SnapshotManager(this.allocator, this.bitCounts16);

		// next snapshot generation target
		/** @type {number} */ this.nextSnapshotTarget = LifeConstants.snapshotInterval;

		// whether to display grid lines
		/** @type {boolean} */ this.displayGrid = false;

		// whether cells have borders
		/** @type {boolean} */ this.cellBorders = false;

		// zoom bounding box
		/** @type {BoundingBox} */ this.zoomBox = null;

		// active cell box for HROT
		/** @type {BoundingBox} */ this.HROTBox = null;

		// initial bounding box for Track speed
		/** @type {BoundingBox} */ this.initialBox = null;

		// state 6 [R]History box
		/** @type {BoundingBox} */ this.state6Box = null;

		// identify [R]History box (alive marked states and state 6)
		/** @type {BoundingBox} */ this.identifyHistoryBox = new BoundingBox(0, 0, 0, 0);

		// alive states bounding box for Super
		/** @type {BoundingBox} */ this.aliveBox = new BoundingBox(0, 0, 0, 0);

		// identify box
		/** @type {BoundingBox} */ this.identifyBox = new BoundingBox(0, 0, 0, 0);

		// history bounding box
		/** @type {BoundingBox} */ this.historyBox = null;

		// saved bounding box
		/** @type {BoundingBox} */ this.saveBox = new BoundingBox(0, 0, 0, 0);

		/** @type {BoundingBox} */ this.saveHistoryBox = new BoundingBox(0, 0, 0, 0);

		// width of life grid in pixels
		/** @type {number} */ this.width = gridWidth;

		// height of life grid in pixels
		/** @type {number} */ this.height = gridHeight;

		// number of pixels per tile (2^n)
		/** @const {number} */ this.tilePower = 4;

		// tile width in bytes
		/** @const {number} */ this.tileX = (1 << this.tilePower) >> 3;

		// tile height in pixels
		/** @const {number} */ this.tileY = 1 << this.tilePower;

		// number of tile columns
		/** @type {number} */ this.tileCols = this.width >> this.tilePower;

		// number of tile rows
		/** @type {number} */ this.tileRows = this.height >> this.tilePower;

		// snapshot for reset
		/** @type {Snapshot} */ this.resetSnapshot = null;

		// display width
		/** @type {number} */ this.displayWidth = displayWidth;

		// display height
		/** @type {number} */ this.displayHeight = displayHeight;

		// number of cells killed in the last generation by hitting the grid boundary
		/** @type {number} */ this.cellsCleared = 0;

		// last generation that cells were cleared
		/** @type {number} */ this.lastClearedGen = -1;
		/** @type {number} */ this.lastClearedNumber = 0;

		// population of grid
		/** @type {number} */ this.population = 0;

		// births in last generation
		/** @type {number} */ this.births = 0;

		// deaths in last generation
		/** @type {number} */ this.deaths = 0;

		// cell colour index for new cell
		/** @const {number} */ this.aliveStart = LifeConstants.aliveStart;

		// cell colour index for cell alive longest
		/** @const {number} */ this.aliveMax = LifeConstants.aliveMax;

		// cell colour index for cell just dead
		/** @const {number} */ this.deadStart = LifeConstants.deadStart;

		// cell colour index for cell dead longest
		/** @const {number} */ this.deadMin = LifeConstants.deadMin;

		// cell colour index for cell never occupied
		/** @const {number} */ this.unoccupied = 0;

		// last update time
		/** @type {number} */ this.lastUpdate = 0;

		// first update time
		/** @type {number} */ this.firstUpdate = 0;

		// number of layers to draw
		/** @type {number} */ this.layers = 1;

		// flag if depth shading on
		/** @type {boolean} */ this.depthOn = true;

		// flag if layers on
		/** @type {boolean} */ this.layersOn = true;

		// colour theme number
		/** @type {number} */ this.colourTheme = -1;

		// colour change
		/** @type {number} */ this.colourChange = 1;
		/** @type {number} */ this.colourChangeSteps = 30;

		// survival rule
		/** @type {number} */ this.survival = (1 << 2) | (1 << 3);

		// birth rule
		/** @type {number} */ this.birth = (1 << 3);

		// Margolus generation number (decreases with reverse playback)
		/** @type {number} */ this.counterMargolus = 0;

		// maximum Margolus generation number
		/** @type {number} */ this.maxMargolusGen = 0;

		// number of generations
		/** @type {number} */ this.counter = 0;

		// angle of rotation
		/** @type {number} */ this.angle = 0;

		// camera tilt
		/** @type {number} */ this.tilt = 0;

		// zoom factor
		/** @type {number} */ this.zoom = 6;

		// x and y pan
		/** @type {number} */ this.xOff = 0;
		/** @type {number} */ this.yOff = 0;

		// layer depth
		/** @type {number} */ this.layerDepth = 0.1;

		// current camera settings
		/** @type {number} */ this.camAngle = 0;
		/** @type {number} */ this.camZoom = 1;
		/** @type {number} */ this.camXOff = 0;
		/** @type {number} */ this.camYOff = 0;
		/** @type {number} */ this.camLayerDepth = 0.1;

		// endian flag
		/** @const {boolean} */ this.littleEndian = Supports.littleEndian;

		// list of themes
		/** @type {Array<Theme>} */ this.themes = [];

		// current colour range
		/** @type {ColourRange} */ this.deadColCurrent = null;
		/** @type {ColourRange} */ this.aliveColCurrent = null;
		/** @type {Colour} */ this.unoccupiedCurrent = null;
		/** @type {Colour} */ this.aliveGenColCurrent = null;
		/** @type {ColourRange} */ this.dyingGenColCurrent = null;
		/** @type {ColourRange} */ this.deadGenColCurrent = null;
		/** @type {Colour} */ this.unoccupiedGenCurrent = null;

		// current PCA theme
		/** @type {Array<Colour>} */ this.pcaColsCurrent = null;

		// target colour range
		/** @type {ColourRange} */ this.deadColTarget = null;
		/** @type {ColourRange} */ this.aliveColTarget = null;
		/** @type {Colour} */ this.unoccupiedTarget = null;
		/** @type {Colour} */ this.aliveGenColTarget = null;
		/** @type {ColourRange} */ this.dyingGenColTarget = null;
		/** @type {ColourRange} */ this.deadGenColTarget = null;
		/** @type {Colour} */ this.unoccupiedGenTarget = null;

		// target PCA theme
		/** @type {Array<Colour>} */ this.pcaColsTarget = null;

		// number of colour themes (will be computed when themes are added)
		/** @type {number} */ this.numThemes = -1;

		// 8 bit view of image data required if CanvasPixelArray used
		/** @type {Uint8Array} */ this.data8 = null;

		// 32 bit view of image data
		/** @type {Uint32Array} */ this.data32 = null;

		// buffers for rendering offsets
		/** @type {Uint32Array} */ this.xOffsets = null;
		/** @type {Uint32Array} */ this.xMaxOffsets = null;

		// image data
		/** @type {ImageData} */ this.imageData = null;

		// mode 7 buffer
		/** @type {Uint32Array} */ this.mode7Buffer = null;

		// drawing context
		/** @type {CanvasRenderingContext2D} */ this.context = context;

		// bit masks for width and height
		/** @type {number} */ this.widthMask = 0;
		/** @type {number} */ this.heightMask = 0;

		// life tile grid and double buffer
		/** @type {Array<Uint16Array>} */ this.tileGrid = null;
		/** @type {Array<Uint16Array>} */ this.nextTileGrid = null;

		// life grid and double buffer
		/** @type {Array<Uint8Array>} */ this.grid = null;
		/** @type {Array<Uint8Array>} */ this.nextGrid = null;

		// 16bit view of grid and double buffer
		/** @type {Array<Uint16Array>} */ this.grid16 = null;
		/** @type {Array<Uint16Array>} */ this.nextGrid16 = null;

		// tile grid of tiles that died
		/** @type {Array<Uint16Array>} */ this.diedGrid = null;

		// blank row for 16bit life grid
		///** @type {Uint16Array} */ this.blankRow16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.blankRow16", Controller.useWASM));
		/** @type {Uint16Array} */ this.blankRow16 = null;

		// blank tile row to prevent wrap
		///** @type {Uint16Array} */ this.blankTileRow = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, this.tileCols >> 4, "Life.blankTileRow", Controller.useWASM));
		/** @type {Uint16Array} */ this.blankTileRow = null;

		// blank colour row
		///** @type {Uint8Array} */ this.blankColourRow = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, this.width, "Life.blankColourRow", Controller.useWASM));
		/** @type {Uint8Array} */ this.blankColourRow = null;

		// colour grid
		/** @type {Array<Uint8Array>} */ this.colourGrid = null;

		// 16bit view of colour grid
		/** @type {Array<Uint16Array>} */ this.colourGrid16 = null;

		// 32bit view of colour grid
		/** @type {Array<Uint32Array>} */ this.colourGrid32 = null;

		// next colour grid for PCA, RuleTree, Super or Extended rules
		/** @type {Array<Uint8Array>} */ this.nextColourGrid = null;
		/** @type {Array<Uint16Array>} */ this.nextColourGrid16 = null;
		/** @type {Array<Uint32Array>} */ this.nextColourGrid32 = null;

		// safe colour grid used when in Photosensitive mode to keep the current grid static for 400ms
		/** @type {Array<Uint8Array>} */ this.safeColourGrid = null;
		/** @type {Array<Uint16Array>} */ this.safeColourGrid16 = null;
		/** @type {Array<Uint32Array>} */ this.safeColourGrid32 = null;

		// small colour grid used for zooms < 1
		/** @type {Array<Uint8Array>} */ this.smallColourGrid = null;

		// overlay grid for LifeHistory
		/** @type {Array<Uint8Array>} */ this.overlayGrid = null;

		// overlay colour grid used for zooms < 1
		/** @type {Array<Uint8Array>} */ this.smallOverlayGrid = null;

		// 16bit view of overlay grid
		/** @type {Array<Uint16Array>} */ this.overlayGrid16 = null;

		// 32bit view of overlay grid
		/** @type {Array<Uint32Array>} */ this.overlayGrid32 = null;

		// colour tile grid
		/** @type {Array<Uint16Array>} */ this.colourTileGrid = null;

		// colour tile history grid (where life has ever been)
		/** @type {Array<Uint16Array>} */ this.colourTileHistoryGrid = null;

		// state 6 grid for [R]History
		/** @type {Array<Uint16Array>} */ this.state6Mask = null;
		/** @type {Array<Uint16Array>} */ this.state6Cells = null;
		/** @type {Array<Uint16Array>} */ this.state6Alive = null;

		// state6 tile grid for [R]History
		/** @type {Array<Uint16Array>} */ this.state6TileGrid = null;

		// colour definitions
		/** @type {Uint8Array} */ this.redChannel = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, 256, "Life.redChannel", false));
		/** @type {Uint8Array} */ this.greenChannel = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, 256, "Life.greenChannel", false));
		/** @type {Uint8Array} */ this.blueChannel = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, 256, "Life.blueChannel", false));

		// pixel colours
		/** @type {Uint32Array} */ this.pixelColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, 256, "Life.pixelColours", Controller.useWASM));

		// array for next genaration extended
		/** @type {Array<number>} */ this.nextStateExtended = [0, 1, 2, 3, 4, 5, 7, 6, 8, 9, 11, 10, 13, 12, 14, 15, 16, 17, 18, 20, 19];

		// WASM version
		/** @type {Uint8Array} */ this.nextStateExtendedWASM = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, this.nextStateExtended.length, "Life.nextStateExtended", Controller.useWASM));
		this.nextStateExtendedWASM.set(this.nextStateExtended);

		// ruleLoader fast lookup
		/** @type {Uint8Array} */ this.ruleLoaderLookup = null;

		// ruletree fast partial lookup
		/** @type {Uint32Array} */ this.ruleTreePartialLookup = null;

		// ruleLoader lookup bits
		/** @type {number} */ this.ruleLoaderLookupBits = 0;

		// ruleLoader enabled
		/** @type {boolean} */ this.ruleLoaderLookupEnabled = false;

		// ruleLoader lookup generation time
		/** @type {number} */ this.ruleLoaderGenerationTime = 0;

		// ruleLoader init step for Moore @TABLE max state rules
		/** @type {number} */ this.ruleLoaderStep = -1;

		// 512 bit density all/odd generations
		/** @type {number} */ this.density = 0;
		/** @type {number} */ this.densityOdd = 0;

		// hash lookup for life generation (algorithm 6x3)
		/** @type {Uint8Array} */ this.indexLookup63 = null;
		/** @type {Uint8Array} */ this.indexLookup632 = null;

		// triangular lookup
		/** @type {Uint8Array} */ this.indexLookupTri1 = null;
		/** @type {Uint8Array} */ this.indexLookupTri2 = null;

		// Margolus lookup
		/** @type {Uint16Array} */ this.margolusLookup1 = null;
		/** @type {Uint16Array} */ this.margolusLookup2 = null;

		// Margolus reverse lookup
		/** @type {Uint16Array} */ this.margolusReverseLookup1 = null;
		/** @type {Uint16Array} */ this.margolusReverseLookup2 = null;

		// colour lookup for next generation
		/** @type {Uint16Array} */ this.colourLookup16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.aliveMax + 1) * 2) << 8, "Life.colourLookup16", Controller.useWASM));
		/** @type {Uint16Array} */ this.colourLookup17 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.aliveMax + 1) * 2) << 9, "Life.colourLookup17", false));

		// fast lookup for colour reset
		/** @type {Uint8Array} */ this.colourReset = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, 256 * 8, "Life.colourReset", false));

		// grid line colour in raw format R G B
		/** @type {number} */ this.gridLineRaw = ViewConstants.gridLineRawDefault;

		// grid line colour
		/** @type {number} */ this.gridLineColour = -1;

		// grid line bold colour in raw format R G B
		/** @type {number} */ this.gridLineBoldRaw = ViewConstants.gridLineBoldRawDefault;

		// grid line bold colour
		/** @type {number} */ this.gridLineBoldColour = -1;

		// grid line major interval and enablement
		/** @type {number} */ this.gridLineMajor = 10;
		/** @type {boolean} */ this.gridLineMajorEnabled = true;

		// user defined grid line major (to restore if theme doesn't define one)
		/** @type {number} */ this.definedGridLineMajor = 10;

		// column occupancy array for grid bounding box calculation
		/** @type {Uint16Array} */ this.columnOccupied16 = null;

		// row occupancy array for grid bounding box calculation
		/** @type {Uint16Array} */ this.rowOccupied16 = null;

		// column occupancy array for alive grid bounding box calculation
		/** @type {Uint16Array} */ this.columnAliveOccupied16 = null;

		// row occupancy array for grid alive bounding box calculation
		/** @type {Uint16Array} */ this.rowAliveOccupied16 = null;

		// current maximum grid size
		/** @type {number} */ this.maxGridSize = 8192;

		// graph default colours
		/** @type {Array<number>} */ this.graphBgDefColor = [0, 0, 48];
		/** @type {Array<number>} */ this.graphAxisDefColor = [255, 255, 255];
		/** @type {Array<number>} */ this.graphAliveDefColor = [255, 255, 255];
		/** @type {Array<number>} */ this.graphBirthDefColor = [0, 255, 0];
		/** @type {Array<number>} */ this.graphDeathDefColor = [255, 0, 0];

		// graph current colours
		/** @type {Array<number>} */ this.graphBgColor = [0, 0, 48];
		/** @type {Array<number>} */ this.graphAxisColor = [255, 255, 255];
		/** @type {Array<number>} */ this.graphAliveColor = [255, 255, 255];
		/** @type {Array<number>} */ this.graphBirthColor = [0, 255, 0];
		/** @type {Array<number>} */ this.graphDeathColor = [255, 0, 0];

		// share buffer for return values from WASM functions
		/** @type {Uint32Array} */ this.sharedBuffer = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, 64, "Life.sharedBuffer", Controller.useWASM));

		// HROT engine
		/** @type {HROT} */ this.HROT = new HROT(this.allocator, this, manager);

		// hex or triangle cell coordinates
		/** @type {Float32Array} */ this.coords = /** @type {!Float32Array} */ (this.allocator.allocate(Type.Float32, 1, "Life.coords", false));

		// hex or triangle cell colours
		/** @type {Uint32Array} */ this.cellColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, 1, "Life.cellColours", false));

		// number of hex or triangle cells
		/** @type {number} */ this.numCells = 0;
	}

	// initialise pretty rendering
	Life.prototype.initPretty = function() {
		var	/** @type {number} */ displayWidth = this.displayWidth,
			/** @type {number} */ displayHeight = this.displayHeight,
			/** @type {number} */ maxZoom = this.view.maxZoom,
			/** @type {number} */ width = (displayWidth + maxZoom + maxZoom) << 1,
			/** @type {number} */ height = (displayHeight + maxZoom + maxZoom) << 1;

		// check if pretty rendering enabled
		if (this.pretty) {
			if (this.sCanvas === null) {
				this.sCanvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
			}
			this.sCanvas.width = width;
			this.sCanvas.height = height;
			this.sContext = /** @type {!CanvasRenderingContext2D} */ (this.sCanvas.getContext("2d", {alpha: false}));
			this.sImageData = this.sContext.getImageData(0, 0, this.sCanvas.width, this.sCanvas.height);
			this.sData32 = new Uint32Array(this.sImageData.data.buffer);
		} else {
			this.sContext = null;
			this.sImageData = null;
			this.sData32 = null;
		}
	};

	// initialise snow flakes
	Life.prototype.initSnow = function() {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ d = 0,
			/** @const {number} */ maxX = this.displayWidth,
			/** @const {number} */ maxY = this.displayHeight,
			/** @const {number} */ yRange = 20 * maxY,
			/** @type {number} */ maxPos = -yRange,
			/** @type {number} */ yVal = 0,
			/** @type {Random} */ randGen = this.randGen,
			/** @type {Uint16Array} */ snowX = null,
			/** @type {Float32Array} */ snowY = null,
			/** @type {Float32Array} */ snowDY = null;

		// allocate snow flakes
		this.snowX = new Uint16Array(LifeConstants.flakes);
		this.snowY = new Float32Array(LifeConstants.flakes);
		this.snowDY = new Float32Array(LifeConstants.flakes);
		this.snowRevive = new Uint32Array(LifeConstants.flakes);
		snowX = this.snowX;
		snowY = this.snowY;
		snowDY = this.snowDY;

		// position each snowflake
		for (i = 0; i < LifeConstants.flakes; i += 1) {
			// pick integer x position
			snowX[i] = (randGen.random() * maxX) | 0;

			// pick y position as a normal distribution
			yVal = 0;
			for (j = 0; j < 10; j += 1) {
				yVal = yVal + randGen.random() * yRange;
			}
			snowY[i] = -yVal / 10;
			if (snowY[i] > maxPos) {
				maxPos = snowY[i];
			}

			// pick random delta
			snowDY[i] = randGen.random() / 5 + 0.8;

			// check whether there is space to make a big snowflake
			if (i < LifeConstants.flakes - 4) {
				if (randGen.random() < 0.2) {
					x = snowX[i];
					y = snowY[i];
					d = snowDY[i];

					// make a bigger snowflake
					i += 1;
					snowX[i] = x;
					snowY[i] = y - 2;
					snowDY[i] = d;
					i += 1;
					snowX[i] = x;
					snowY[i] = y + 2;
					snowDY[i] = d;
					if (x > 1) {
						i += 1;
						snowX[i] = x - 2;
						snowY[i] = y;
						snowDY[i] = d;
					}
					if (x < maxX - 2) {
						i += 1;
						snowX[i] = x + 2;
						snowY[i] = y;
						snowDY[i] = d;
					}
				}
			}
		}
		for (i = 0; i < LifeConstants.flakes; i += 1) {
			snowY[i] -= maxPos;
		}
	};

	// get Y zoom factor based on grid type
	/** @returns {number} */
	Life.prototype.getYFactor = function() {
		var	/** @type {number} */ result = 1;

		if (this.isTriangular) {
			result = ViewConstants.triangularYFactor;
		}

		if (this.isHex) {
			result = ViewConstants.hexagonalYFactor;
		}

		return result;
	};

	// adjust Y zoom based on grid type
	/** @returns {number} */
	Life.prototype.getYZoom = function(/** @type {number} */ zoom) {
		var	/** @type {number} */ result = zoom;

		if (this.isTriangular) {
			result *= ViewConstants.triangularYFactor;
		}

		if (this.isHex) {
			result *= ViewConstants.hexagonalYFactor;
		}

		return result;
	};

	// update and draw snowflakes
	Life.prototype.drawSnow = function() {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ r = 0,
			/** @type {number} */ numRevive = 0,
			/** @type {number} */ dx = 0,
			/** @type {number} */ lastX,
			/** @type {number} */ lastY,
			/** @type {number} */ dirY,
			/** @type {number} */ newX,
			/** @type {number} */ newY,
			/** @const {number} */ wd = this.displayWidth,
			/** @const {number} */ ht = this.displayHeight,
			/** @const {number} */ bg = this.pixelColours[0],
			/** @type {Uint32Array} */ screen = this.data32,
			/** @type {Random} */ randGen = this.randGen,
			/** @type {Uint16Array} */ snowX = this.snowX,
			/** @type {Float32Array} */ snowY = this.snowY,
			/** @type {Float32Array} */ snowDY = this.snowDY,
			/** @type {Uint32Array} */ snowRevive = this.snowRevive,
			/** @type {number} */ snowCol = this.view.snowColour,
			/** @type {number} */ yScale = 60 / Controller.refreshRate;

		// set snow colour
		if (this.littleEndian) {
			snowCol = (255 << 24) | ((snowCol & 255) << 16) | (snowCol & 0xff00) | (snowCol >> 16);
		} else {
			snowCol = (snowCol << 8) | 255;
		}

		// update each snowflake
		for (i = 0; i < LifeConstants.flakes; i += 1) {
			lastX = snowX[i];
			lastY = snowY[i];
			dirY = snowDY[i] * yScale;
			newX = lastX;
			newY = lastY;

			// check if the flake is on screen
			if (lastY >= 0 && lastY < ht - 1) {
				// check if the delta moves the flake a pixel down
				if ((lastY | 0) === ((lastY + dirY) | 0)) {
					newY = lastY + dirY;
					// draw the flake at the current position
					screen[(newY | 0) * wd + newX] = snowCol;
				} else {
					// check if the space below is free
					if (screen[((lastY + dirY) | 0) * wd + lastX] === bg) {
						newY = lastY + dirY;
					} else {
						// space not free so check bottom left or bottom right
						r = randGen.random();
						if (r < 0.05) {
							dx = 1;
							if (r < 0.025) {
								dx = -1;
							}
							if (lastX + dx >= 0 && lastX + dx < wd) {
								if (screen[((lastY + dirY) | 0) * wd + lastX + dx] === bg && screen[(lastY | 0) * wd + lastX + dx] === bg) {
									newX = lastX + dx;
									newY = lastY + dirY;
								}
							}
						}
					}
					// draw the flake at the new position
					screen[(newY | 0) * wd + newX] = snowCol;
				}
			} else if (lastY < 0) {
				// flake is above the screen so move it down
				newY = lastY + dirY;
			}

			// if the flake is off the bottom then add to revive list
			if (newY >= ht - 1) {
				// add to revive list
				snowRevive[numRevive] = i;
				numRevive += 1;
			}
			snowX[i] = newX;
			snowY[i] = newY;
		}

		// process any flakes to be revived
		if (numRevive > 5) {
			for (i = 0; i < numRevive; i += 1) {
				if (randGen.random() < 0.25) {
					j = snowRevive[i];
					newY = -(randGen.random() * ht);
					newX = (randGen.random() * wd) | 0;
					snowX[j] = newX;
					snowY[j] = newY;
					dirY = snowDY[j];

					// check whether there is space to make a big snowflake
					if (i < numRevive - 4) {
						if (randGen.random() < 0.2) {
							// make a bigger snowflake
							i += 1;
							j = snowRevive[i];
							snowX[j] = newX;
							snowY[j] = newY - 2;
							snowDY[j] = dirY;
							i += 1;
							j = snowRevive[i];
							snowX[j] = newX;
							snowY[j] = newY + 2;
							snowDY[j] = dirY;
							if (newX > 1) {
								i += 1;
								j = snowRevive[i];
								snowX[j] = newX - 2;
								snowY[j] = newY;
								snowDY[j] = dirY;
							}
							if (newX < wd - 2) {
								i += 1;
								j = snowRevive[i];
								snowX[j] = newX + 2;
								snowY[j] = newY;
								snowDY[j] = dirY;
							}
						}
					}
				}
			}
		}
	};

	// apply greyscale to a state colour
	/** @returns {number} */
	Life.prototype.applyGreyScale = function(/** @type {number} */ greyScale, /** @type {number} */ stateColour) {
		var	/** @type {number} */ result = 0,
			/** @type {number} */ r = 0,
			/** @type {number} */ g = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ l = 0;

		// get state colour r g b components
		r = (stateColour >> 16);
		g = (stateColour >> 8) & 255;
		b = stateColour & 255;

		// get greyscale level as a value from 0 to 1
		l = (greyScale & 255) / 255;

		// apply greyscale level
		r = (r * l) | 0;
		g = (g * l) | 0;
		b = (b * l) | 0;

		result = (r << 16) | (g <<  8) | b;

		return result;
	};

	// create cell icon canvases at the required size
	Life.prototype.createCellIconCanvases = function(/** @type {number} */ numIcons) {
		// create 31x31 icon canvas
		if (this.cellIconCanvas31 === null) {
			this.cellIconCanvas31 = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
		}
		this.cellIconCanvas31.width = 31;
		this.cellIconCanvas31.height = numIcons * 31;

		// create 15x15 icon canvas
		if (this.cellIconCanvas15 === null) {
			this.cellIconCanvas15 = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
		}
		this.cellIconCanvas15.width = 15;
		this.cellIconCanvas15.height = numIcons * 15;

		// create 7x7 icon canvas
		if (this.cellIconCanvas7 === null) {
			this.cellIconCanvas7 = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
		}
		this.cellIconCanvas7.width = 7;
		this.cellIconCanvas7.height = numIcons * 7;

		// get the contexts
		this.cellIconContext31 = /** @type {!CanvasRenderingContext2D} */ (this.cellIconCanvas31.getContext("2d", {"willReadFrequently": true, "alpha": false}));
		this.cellIconContext15 = /** @type {!CanvasRenderingContext2D} */ (this.cellIconCanvas15.getContext("2d", {"willReadFrequently": true, "alpha": false}));
		this.cellIconContext7 = /** @type {!CanvasRenderingContext2D} */ (this.cellIconCanvas7.getContext("2d", {"willReadFrequently": true, "alpha": false}));

		// clear the image data
		this.cellIconImageData = null;
	};

	// create PCA Icons
	Life.prototype.createPCAIcons = function(/** @type {Pattern} */ pattern) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ o = 0,
			/** @type {number} */ base = 0,
			/** @type {number} */ iconSize = 0,
			/** @const {number} */ numIcons = 15,
			/** @type {ImageData} */ data = null,
			/** @type {Uint32Array} */ data32 = null,
			/** @type {HTMLCanvasElement} */ cvs = null,
			/** @type {CanvasRenderingContext2D} */ ctx = null,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {number} */ historyStates = this.historyStates,
			/** @type {number} */ nCol = pixelColours[1 + historyStates],
			/** @type {number} */ eCol = pixelColours[2 + historyStates],
			/** @type {number} */ sCol = pixelColours[4 + historyStates],
			/** @type {number} */ wCol = pixelColours[8 + historyStates];

		// create the cell icon canvases
		this.createCellIconCanvases(numIcons);

		// create the icons at each of the three sizes
		if (pattern !== null) {
			pattern.ruleTableIcons = [];
		}

		for (j = 0; j < 3; j += 1) {
			switch (j) {
				case 0:
					cvs = this.cellIconCanvas31;
					ctx = this.cellIconContext31;
					iconSize = 31;
					break;

				case 1:
					cvs = this.cellIconCanvas15;
					ctx = this.cellIconContext15;
					iconSize = 15;
					break;

				case 2:
					cvs = this.cellIconCanvas7;
					ctx = this.cellIconContext7;
					iconSize = 7;
					break;
			}

			// fill the canvas with the first history state
			ctx.fillStyle = this.cellColourStrings[historyStates];
			ctx.fillRect(0, 0, cvs.width, cvs.height);

			data = ctx.getImageData(0, 0, cvs.width, cvs.height);
			data32 = new Uint32Array(data.data.buffer);

			// create icon for each of the 16 states
			base = 0;

			for (i = 1; i <= numIcons; i += 1) {
				// check for north
				if ((i & 1) !== 0) {
					o = 1;
					for (y = iconSize - 1; y >= ((iconSize + 1) / 2); y -= 1) {
						for (x = o; x < iconSize - o; x += 1) {
							data32[base + y * iconSize + x] = nCol;
						}
						o += 1;
					}
				}

				// check for east
				if ((i & 2) !== 0) {
					o = 1;
					for (x = 0; x <= ((iconSize - 3) / 2); x += 1) {
						for (y = o; y < iconSize - o; y += 1) {
							data32[base + y * iconSize + x] = eCol;
						}
						o += 1;
					}
				}

				// check for south
				if ((i & 4) !== 0) {
					o = 1;
					for (y = 0; y <= ((iconSize - 3) / 2); y += 1) {
						for (x = o; x < iconSize - o; x += 1) {
							data32[base + y * iconSize + x] = sCol;
						}
						o += 1;
					}
				}

				// check for west
				if ((i & 8) !== 0) {
					o = 1;
					for (x = iconSize - 1; x >= ((iconSize + 1) / 2); x -= 1) {
						for (y = o; y < iconSize - o; y += 1) {
							data32[base + y * iconSize + x] = wCol;
						}
						o += 1;
					}
				}

				// go to next icon
				base += iconSize * iconSize;
			}

			// add entry for Help
			if (pattern !== null) {
				pattern.ruleTableIcons[pattern.ruleTableIcons.length] = {builtIn: PatternConstants.ruleTableIconNone, width: iconSize, height: iconSize * numIcons, numColours: 4, colours: [], iconData: [], greyScale: false};
			}

			// write the image data back to the canvas
			ctx.putImageData(data, 0, 0);

			// save the image data
			switch (j) {
				case 0:
					this.cellIconImageData31 = data;
					break;
				case 1:
					this.cellIconImageData15 = data;
					break;
				case 2:
					this.cellIconImageData7 = data;
					break;
			}
		}

		// create the icon drawing canvas
		if (this.iconCanvas === null) {
			// create the icon canvas to be the display width and height plus one cell in each direction
			this.iconCanvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
			this.iconCanvas.width = this.displayWidth + this.view.maxZoom;
			this.iconCanvas.height = this.displayHeight + this.view.maxZoom;

			this.iconContext = /** @type {!CanvasRenderingContext2D} */ (this.iconCanvas.getContext("2d", {alpha: false}));
			this.iconImageData = this.iconContext.createImageData(this.iconCanvas.width, this.iconCanvas.height);
		}

		// mark that icons are available
		this.iconsAvailable = true;
	};

	// scale icon set and return image data
	/** @returns {ImageData} */
	Life.prototype.scaleIconSet = function(/** @type {CanvasRenderingContext2D} */ dest, /** @type {CanvasRenderingContext2D} */ source) {
		var	/** @type {number} */ destSize = dest.canvas.width,
			/** @type {number} */ sourceSize = source.canvas.width,
			/** @type {number} */ numIcons = source.canvas.height / sourceSize,
			/** @type {number} */ scaledSize = 0,
			/** @type {number} */ stateZeroCol = this.ruleTreeColours[0],
			/** @type {number} */ i = 0,
			/** @type {ImageData} */ result = null;

		// check if the size is increasing or decreasing
		if (sourceSize > destSize) {
			scaledSize = (sourceSize - 3) / 2;
		} else {
			scaledSize = sourceSize * 2;
		}

		// copy each icon at scale
		dest.imageSmoothingEnabled = false;

		// fill the destination with the state 0 colour
		dest.fillStyle = "rgb(" + ((stateZeroCol >> 16) & 255) + "," + ((stateZeroCol >> 8) & 255) + "," + (stateZeroCol & 255) + ")";
		dest.fillRect(0, 0, dest.canvas.width, dest.canvas.height);

		// draw each scaled icon
		for (i = 0; i < numIcons; i += 1) {
			dest.drawImage(source.canvas, 0, i * sourceSize, sourceSize, sourceSize, 0, i * destSize, scaledSize, scaledSize);
		}

		result = dest.getImageData(0, 0, dest.canvas.width, dest.canvas.height);

		return result;
	};

	// process icons
	Life.prototype.processIcons = function(/** @type {Array} */ icons) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ src = 0,
			/** @type {number} */ dst = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ col = 0,
			/** @type {number} */ rgb = 0,
			/** @type {number} */ red = 0,
			/** @type {number} */ green = 0,
			/** @type {number} */ blue = 0,
			/** @type {number} */ stateCol = 0,
			/** @type {number} */ stateZeroCol = this.ruleTreeColours[0],
			/** @type {number} */ iconSize = 0,
			/** @type {number} */ numIcons = 0,
			/** @type {Uint16Array} */ iconData = null,
			/** @type {Uint32Array} */ iconColours = null,
			/** @type {boolean} */ greyScale = false,
			/** @type {ImageData} */ data = null,
			/** @type {Uint32Array} */ data32 = null,
			/** @type {CanvasRenderingContext2D} */ ctx = null,
			/** @type {boolean} */ size31 = false,
			/** @type {boolean} */ size15 = false,
			/** @type {boolean} */ size7 = false,
			/** @type {boolean} */ done31 = false,
			/** @type {boolean} */ done15 = false,
			/** @type {boolean} */ done7 = false;

		// get the icon definitions
		this.ruleTableIcons = icons;

		// if no icons are specified then disable icons
		if (icons.length === 0 || this.isHex || this.isTriangular) {
			this.iconsAvailable = false;
		} else {
			// create the canvases
			this.createCellIconCanvases(icons[0].height / icons[0].width);

			// process each set of icons
			for (j = 0; j < icons.length; j += 1) {
				// get the size and number of icons
				iconSize = icons[j].width;
				numIcons = icons[j].height / iconSize;
				iconData = icons[j].iconData;
				iconColours = icons[j].colours;
				greyScale = icons[j].greyScale;

				// get the context for the correct icon size
				switch (iconSize) {
					case 31:
						ctx = this.cellIconContext31;
						size31 = true;
						break;

					case 15:
						ctx = this.cellIconContext15;
						size15 = true;
						break;

					case 7:
						ctx = this.cellIconContext7;
						size7 = true;
						break;

					default:
						// any other size is an error so disable icons
						console.error("LifeViewer: illegal icon size found");
						this.iconsAvailable = false;
						return;
				}

				// create image data buffer
				data = ctx.createImageData(ctx.canvas.width, ctx.canvas.height);
				data32 = new Uint32Array(data.data.buffer);

				// write them to the canvas
				src = 0;
				dst = 0;
				for (i = 0; i < numIcons; i += 1) {
					// check if greyscale set
					if (greyScale) {
						// lookup the state colour
						stateCol = this.ruleTreeColours[i + 1];
					}

					// process each pixel in the icon
					for (y = 0; y < iconSize; y += 1) {
						for (x = 0; x < iconSize; x += 1) {
							// get the next source pixel colour number
							col = iconData[src];
							src += 1;

							// lookup the rgb for this colour
							rgb = iconColours[col];

							// check if greyscale set
							if (greyScale) {
								// check for black pixel
								if (rgb === 0) {
									// black pixels are replaced by state 0 colour
									rgb = stateZeroCol;
								} else {
									// other pixels control current state colour transparency
									rgb = this.applyGreyScale(rgb, stateCol);
								}
							} else {
								// multi-colour icons, check for black pixel
								if (rgb === 0) {
									// black pixels are replaced by state 0 colour
									rgb = stateZeroCol;
								}
							}

							// write the rgb to the canvas
							red = rgb >> 16;
							green = (rgb >> 8) & 255;
							blue = rgb & 255;

							if (this.littleEndian) {
								data32[dst] = (255 << 24) | (blue << 16) | (green << 8) | red;
							} else {
								data32[dst] = (red << 24) | (green << 16) | (blue << 8) | 255;
							}
							dst += 1;
						}
					}
				}

				// write the image data back to the canvas
				ctx.putImageData(data, 0, 0);

				// save the image data
				switch (iconSize) {
					case 31:
						this.cellIconImageData31 = data;
						break;
					case 15:
						this.cellIconImageData15 = data;
						break;
					case 7:
						this.cellIconImageData7 = data;
						break;
				}
			}

			// create the icon drawing canvas
			if (this.iconCanvas === null) {
				this.iconCanvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas"));
				this.iconCanvas.width = this.displayWidth + this.view.maxZoom;
				this.iconCanvas.height = this.displayHeight + this.view.maxZoom;

				this.iconContext = /** @type {!CanvasRenderingContext2D} */ (this.iconCanvas.getContext("2d", {alpha: false}));
				this.iconImageData = this.iconContext.createImageData(this.iconCanvas.width, this.iconCanvas.height);
			}

			// at least one icon size has been created so check if any other sizes are missing
			if (size31 === false || size15 === false || size7 === false) {
				// check if size 31 is missing
				if (size31 === false) {
					// check if size 15 is also missing
					if (size15 === false) {
						// create size 15 from size 7 since it must exist
						this.cellIconImageData15 = this.scaleIconSet(this.cellIconContext15, this.cellIconContext7);
						done15 = true;
					}

					// create size 31 from size 15
					this.cellIconImageData31 = this.scaleIconSet(this.cellIconContext31, this.cellIconContext15);
					done31 = true;
				}

				// by now at least size 31 exists
				if (size15 === false) {
					// if size 7 exists then use it to create size 15
					if (size7) {
						// create size 15 from size 7
						this.cellIconImageData15 = this.scaleIconSet(this.cellIconContext15, this.cellIconContext7);
						done15 = true;
					} else {
						// create size 15 from size 31
						this.cellIconImageData15 = this.scaleIconSet(this.cellIconContext15, this.cellIconContext31);
						done15 = true;
					}
				}

				// by now both sizes 31 and 15 exist so create size 7 from size 15
				if (size7 === false) {
					this.cellIconImageData7 = this.scaleIconSet(this.cellIconContext7, this.cellIconContext15);
					done7 = true;
				}
			}

			// mark icons available
			this.iconsAvailable = true;
		}
	};

	// initialize oscillator search
	Life.prototype.initSearch = function(/** @type {boolean} */ on) {
		// zero array index for short array (just minimum hashes)
		this.oscLength = 0;

		// zero array index for full array (all generations)
		this.fullOscLength = 0;

		// clear temporary results
		this.identifyDeferredCounter = -1;
		this.identifyDeferredResults = [];

		// check if search has been switched on
		if (on) {
			// just switched on so check if buffers are allocated
			if (this.hashListLower === null) {
				// allocate buffers
				this.hashListLower = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, LifeConstants.maxOscillatorGens, "Life.hashListLower", false));
				this.hashListUpper = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, LifeConstants.maxOscillatorGens, "Life.hashListUpper", false));
				this.boxList = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, 2 * LifeConstants.maxOscillatorGens, "Life.boxList", false));

				this.negHashListLower = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, LifeConstants.negOscillatorGens, "Life.negHashListLower", false));
				this.negHashListUpper = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, LifeConstants.negOscillatorGens, "Life.negHashListUpper", false));
				this.negGenList = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.negOscillatorGens, "Life.negGenList", false));
				this.negPopList = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.negOscillatorGens, "Life.negPopList", false));
				this.negBoxList = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, 2 * LifeConstants.negOscillatorGens, "Life.negBoxList", false));

				this.countList = Array.matrix(Type.Uint8, this.height, this.width, LifeConstants.cellWasDead, this.allocator, "Life.countList", false);
				if (this.multiNumStates > 2 || this.isRuleTree) {
					this.initList = Array.matrix(Type.Uint8, this.height, this.width, 0, this.allocator, "Life.initList", false);
				} else {
					this.initList = null;
				}
				this.modValue = -1;
				this.modType = -1;

				// clear last strict volatility
				this.strictVol = "";
				this.cellPeriod = null;

				// create the occupancy map
				this.occMap = new Uint16Array(64 + 3);
				this.occMap[0] = LifeConstants.cellWasDead;
				this.occMap[1] = LifeConstants.cellHasChanged;
				this.occMap[2] = LifeConstants.cellHasChanged;
				this.occMap[64] = LifeConstants.cellHasChanged;
				this.occMap[65] = LifeConstants.cellWasAlive;
				this.occMap[66] = LifeConstants.cellHasChanged;

				// get start time
				this.identifyStartTime = performance.now();
			}
		} else {
			// just switched off so release buffers
			this.hashListLower = null;
			this.hashListUpper = null;
			this.boxList = null;
			this.negHashListLower = null;
			this.negHashListUpper = null;
			this.negGenList = null;
			this.negPopList = null;
			this.negBoxList = null;
			this.countList = null;
			this.initList = null;
			this.modValue = -1;
			this.modType = -1;
			this.occMap = null;
			this.occTileMap = null;
			this.occMergedTileMap = null;
		}
	};

	// ouput spaceship speed as string
	/** @returns {string} */
	Life.prototype.spaceshipSpeed = function(/** @type {number} */ period, /** @type {number} */ deltaX, /** @type {number} */ deltaY) {
		var	/** @type {string} */ message = deltaX + "," + deltaY;

		// add the speed
		message = "Spaceship (" + message + ")c";

		// add period if greather than 1
		if (period > 1) {
			message += "/" + period;
		}

		return message;
	};

	// get mod hash from 2-state pattern using Rot270
	Life.prototype.getHash2Rot270 = function(/** @type {number} */ width, /** @type {number} */ height, /** @type {number} */ modHeight, /** @type {number} */ hm1, /** @type {number} */ left, /** @type {number} */ bottom, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ iDivHeight = 0,
			/** @type {number} */ iModHeight = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ checkedFirst = false;

		// create a hash from every alive cell
		iDivHeight = left;
		hm1 += bottom;

		// find the first alive cell
		for (y = 0; y < height; y += 1) {
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			for (x = 0; x < width; x += 1) {
				if (colourGrid[hm1 - iModHeight][iDivHeight] >= aliveStart) {
					hash = (hash * factor) ^ (yPrime ^ (x * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));

					// check if this matches the first hash
					if (!checkedFirst) {
						if (hash !== firstHash1 || hash2 !== firstHash2) {
							//console.log("gen", this.counter, "xy", x, y, "hash", hash, "rot270 BAILED != ", firstHash);

							// bail out with non-matching hash value
							this.hashLower = firstHash1 + 1;
							this.hashUpper = firstHash2 + 1;
							return;
						}

						checkedFirst = true;
					}
				}

				// update index
				iModHeight += 1;
				if (iModHeight === modHeight) {
					iModHeight = 0;
					iDivHeight += 1;
				}
			}
		}

		//console.log("gen", this.counter, "rot270", hash);

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash from 2-state pattern using Rot180
	Life.prototype.getHash2Rot180 = function(/** @type {number} */ width, /** @type {number} */ height, /** @type {number} */ wm1, /** @type {number} */ hm1, /** @type {number} */ left, /** @type {number} */ bottom, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ checkedFirst = false;

		// create a hash from every alive cell
		wm1 += left;
		hm1 += bottom;

		for (y = 0; y < height; y += 1) {
			colourRow = colourGrid[hm1 - y];
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			for (x = 0; x < width; x += 1) {
				// adjust the coordinates based on the transformation
				if (colourRow[wm1 - x] >= aliveStart) {
					// update the hash
					hash = (hash * factor) ^ (yPrime ^ (x * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));

					if (!checkedFirst) {
						if (hash !== firstHash1 || hash2 !== firstHash2) {
							//console.log("gen", this.counter, "xy", x, y, "hash", hash, "rot180 BAILED != ", firstHash);

							// bail out with non-matching hash value
							this.hashLower = firstHash1 + 1;
							this.hashUpper = firstHash2 + 1;
							return;
						}

						checkedFirst = true;
					}
				}
			}
		}

		//console.log("gen", this.counter, "rot180", hash);

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash from 2-state pattern using Rot90
	Life.prototype.getHash2Rot90 = function(/** @type {number} */ width, /** @type {number} */ height, /** @type {number} */ modHeight, /** @type {number} */ wm1, /** @type {number} */ left, /** @type {number} */ bottom, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {number} */ iDivHeight = 0,
			/** @type {number} */ iModHeight = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ checkedFirst = false;

		// create a hash from every alive cell
		wm1 += left;

		// find the first alive cell and check it against the first hash
		for (y = 0; y < height; y += 1) {
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			for (x = 0; x < width; x += 1 ) {
				// adjust the coordinates based on the transformation
				if (colourGrid[iModHeight + bottom][wm1 - iDivHeight] >= aliveStart) {
					// update the hash
					hash = (hash * factor) ^ (yPrime ^ (x * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));

					if (!checkedFirst) {
						if (hash !== firstHash1 || hash2 !== firstHash2) {
							//console.log("gen", this.counter, "xy", x, y, "hash", hash, "rot90 BAILED != ", firstHash);

							// bail out with non-matching hash value
							this.hashLower = firstHash1 + 1;
							this.hashUpper = firstHash2 + 1;
							return;
						}

						checkedFirst = true;
					}
				}

				// update index
				iModHeight += 1;
				if (iModHeight === modHeight) {
					iModHeight = 0;
					iDivHeight += 1;
				}
			}
		}

		//console.log("gen", this.counter, "rot90", hash);

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash from 2-state pattern using FlipX
	Life.prototype.getHash2FlipX = function(/** @type {number} */ width, /** @type {number} */ height, /** @type {number} */ wm1, /** @type {number} */ left, /** @type {number} */ bottom, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ checkedFirst = false;

		// create a hash from every alive cell
		wm1 += left;

		for (y = 0; y < height; y += 1) {
			colourRow = colourGrid[y + bottom];
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			for (x = 0; x < width; x += 1) {
				// adjust the coordinates based on the transformation
				if (colourRow[wm1 - x] >= aliveStart) {
					// update the hash
					hash = (hash * factor) ^ (yPrime ^ (x * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));

					if (!checkedFirst) {
						if (hash !== firstHash1 || hash2 !== firstHash2) {
							//console.log("gen", this.counter, "xy", x, y, "hash", hash, "flipX BAILED != ", firstHash);

							// bail out with non-matching hash value
							this.hashLower = firstHash1 + 1;
							this.hashUpper = firstHash2 + 1;
							return;
						}

						checkedFirst = true;
					}
				}
			}
		}

		//console.log("gen", this.counter, "flipX", hash);

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash from 2-state pattern using FlipY
	Life.prototype.getHash2FlipY = function(/** @type {number} */ width, /** @type {number} */ height, /** @type {number} */ hm1, /** @type {number} */ left, /** @type {number} */ bottom, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ checkedFirst = false;

		// create a hash from every alive cell
		hm1 += bottom;

		for (y = 0; y < height; y += 1) {
			colourRow = colourGrid[hm1 - y];
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			for (x = 0; x < width; x += 1) {
				if (colourRow[x + left] >= aliveStart) {
					// update the hash
					hash = (hash * factor) ^ (yPrime ^ (x * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));

					if (!checkedFirst) {
						if (hash !== firstHash1 || hash2 !== firstHash2) {
							//console.log("gen", this.counter, "xy", x, y, "hash", hash, "flipY BAILED != ", firstHash);

							// bail out with non-matching hash value
							this.hashLower = firstHash1 + 1;
							this.hashUpper = firstHash2 + 1;
							return;
						}

						checkedFirst = true;
					}
				}
			}
		}

		//console.log("gen", this.counter, "flipY", hash);

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash from 2-state pattern using Rot90FlipX
	Life.prototype.getHash2Rot90FlipX = function(/** @type {number} */ width, /** @type {number} */ height, /** @type {number} */ modHeight, /** @type {number} */ left, /** @type {number} */ bottom, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {number} */ iDivHeight = 0,
			/** @type {number} */ iModHeight = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ checkedFirst = false;

		// create a hash from every alive cell
		iDivHeight += left;

		for (y = 0; y < height; y += 1) {
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			for (x = 0; x < width; x += 1) {
				// adjust the coordinates based on the transformation
				if (colourGrid[iModHeight + bottom][iDivHeight] >= aliveStart) {
					// update the hash
					hash = (hash * factor) ^ (yPrime ^ (x * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));

					// check if this matches the first hash
					if (!checkedFirst) {
						if (hash !== firstHash1 || hash2 !== firstHash2) {
							//console.log("gen", this.counter, "xy", x, y, "hash", hash, "rot90flipX BAILED != ", firstHash);

							// bail out with non-matching hash value
							this.hashLower = firstHash1 + 1;
							this.hashUpper = firstHash2 + 1;
							return;
						}

						checkedFirst = true;
					}
				}

				// update index
				iModHeight += 1;
				if (iModHeight === modHeight) {
					iModHeight = 0;
					iDivHeight += 1;
				}
			}
		}

		//console.log("gen", this.counter, "rot90flipX", hash);

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash from 2-state pattern using Rot90FlipY
	Life.prototype.getHash2Rot90FlipY = function(/** @type {number} */ width, /** @type {number} */ height, /** @type {number} */ modHeight, /** @type {number} */ wm1, /** @type {number} */ hm1, /** @type {number} */ left, /** @type {number} */ bottom, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {number} */ iDivHeight = 0,
			/** @type {number} */ iModHeight = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ checkedFirst = false;

		// create a hash from every alive cell
		wm1 += left;
		hm1 += bottom;

		for (y = 0; y < height; y += 1) {
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			for (x = 0; x < width; x += 1) {
				// adjust the coordinates based on the transformation
				if (colourGrid[hm1 - iModHeight][wm1 - iDivHeight] >= aliveStart) {
					// update the hash
					hash = (hash * factor) ^ (yPrime ^ (x * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));

					// check if this matches the first hash
					if (!checkedFirst) {
						if (hash !== firstHash1 || hash2 !== firstHash2) {
							//console.log("gen", this.counter, "xy", x, y, "hash", hash, "rot90flipY BAILED != ", firstHash);

							// bail out with non-matching hash value
							this.hashLower = firstHash1 + 1;
							this.hashUpper = firstHash2 + 1;
							return;
						}

						checkedFirst = true;
					}
				}

				// update index
				iModHeight += 1;
				if (iModHeight === modHeight) {
					iModHeight = 0;
					iDivHeight += 1;
				}
			}
		}

		//console.log("gen", this.counter, "rot90flipY", hash);

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash from 2-state pattern
	Life.prototype.getModHash2 = function(/** @type {BoundingBox} */ box, /** @type {number} */ transform, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @const {number} */ left = box.leftX,
			/** @const {number} */ bottom = box.bottomY,
			/** @const {number} */ right = box.rightX,
			/** @const {number} */ top = box.topY,
			/** @const {number} */ width = right - left + 1,
			/** @const {number} */ height = top - bottom + 1,
			/** @const {number} */ wm1 = width - 1,
			/** @const {number} */ hm1 = height - 1,
			/** @type {number} */ checkWidth = width,
			/** @type {number} */ checkHeight = height,
			/** @type {number} */ swap = 0;

		// if rotate is used then swap check width and height
		if (transform === LifeConstants.modRot90 || transform === LifeConstants.modRot270 || transform === LifeConstants.modRot90FlipX || transform === LifeConstants.modRot90FlipY) {
			swap = checkWidth;
			checkWidth = checkHeight;
			checkHeight = swap;
		}

		var t = performance.now();

		switch (transform) {
			case LifeConstants.modRot90:
				this.getHash2Rot90(checkWidth, checkHeight, height, wm1, left, bottom, firstHash1, firstHash2);
				break;

			case LifeConstants.modRot180:
				this.getHash2Rot180(checkWidth, checkHeight, wm1, hm1, left, bottom, firstHash1, firstHash2);
				break;

			case LifeConstants.modRot270:
				this.getHash2Rot270(checkWidth, checkHeight, height, hm1, left, bottom, firstHash1, firstHash2);
				break;

			case LifeConstants.modFlipX:
				this.getHash2FlipX(checkWidth, checkHeight, wm1, left, bottom, firstHash1, firstHash2);
				break;

			case LifeConstants.modFlipY:
				this.getHash2FlipY(checkWidth, checkHeight, hm1, left, bottom, firstHash1, firstHash2);
				break;

			case LifeConstants.modRot90FlipX:
				this.getHash2Rot90FlipX(checkWidth, checkHeight, height, left, bottom, firstHash1, firstHash2);
				break;

			case LifeConstants.modRot90FlipY:
				this.getHash2Rot90FlipY(checkWidth, checkHeight, height, wm1, hm1, left, bottom, firstHash1, firstHash2);
				break;
		}

		t = performance.now() - t;
		//console.log(t.toFixed(2) + "ms");
	};

	// get mod hash from pattern
	Life.prototype.getModHashN = function(/** @type {BoundingBox} */ box, /** @type {number} */ transform) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ left = box.leftX,
			/** @type {number} */ bottom = box.bottomY,
			/** @type {number} */ right = box.rightX,
			/** @type {number} */ top = box.topY,
			/** @type {number} */ width = right - left + 1,
			/** @type {number} */ height = top - bottom + 1,
			/** @type {number} */ wm1 = width - 1,
			/** @type {number} */ hm1 = height - 1,
			/** @type {number} */ checkWidth = width,
			/** @type {number} */ checkHeight = height,
			/** @type {number} */ swap = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ iDivHeight = 0,
			/** @type {number} */ iModHeight = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128;

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// if rotate is used then swap check width and height
		if (transform === LifeConstants.modRot90 || transform === LifeConstants.modRot270 || transform === LifeConstants.modRot90FlipX || transform === LifeConstants.modRot90FlipY) {
			swap = checkWidth;
			checkWidth = checkHeight;
			checkHeight = swap;
		}

		for (y = 0; y < checkHeight; y += 1) {
			yPrime = y * primeY;
			yPrime2 = y * primeY2;

			//var rowMsg = "";
			//var rowMsgC = "";

			for (x = 0; x < checkWidth; x += 1) {
				// adjust the coordinates based on the transformation
				switch (transform) {
				case LifeConstants.modRot270:
					cx = iDivHeight;
					cy = hm1 - iModHeight;
					break;
				case LifeConstants.modRot180:
					cx = wm1 - x;
					cy = hm1 - y;
					break;
				case LifeConstants.modRot90:
					cx = wm1 - iDivHeight;
					cy = iModHeight;
					break;
				case LifeConstants.modFlipX:
					cx = wm1 - x;
					cy = y;
					break;
				case LifeConstants.modFlipY:
					cx = x;
					cy = hm1 - y;
					break;
				case LifeConstants.modRot90FlipX:
					cx = iDivHeight;
					cy = iModHeight;
					break;
				case LifeConstants.modRot90FlipY:
					cx = wm1 - iDivHeight;
					cy = hm1 - iModHeight;
					break;
				}

				// get the cell state
				if (this.isLifeHistory) {
					if (colourGrid[cy + bottom][cx + left] >= aliveStart || overlayGrid[cy + bottom][cx + left] === state6) {
						// update the hash
						hash = (hash * factor) ^ (yPrime ^ (x * primeX));
						hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));
						if (overlayGrid[cy + bottom][cx + left] === state6) {
							hash = (hash * factor) ^ 6;
							hash2 = (hash2 * factor2) ^ 6;
						}
					}
				} else {
					if (this.isSuper) {
						state = colourGrid[cy + bottom][cx + left];
						if (state & 1 || state === 6) {
							hash = (hash * factor) ^ (yPrime ^ (x * primeX));
							hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));
							if (state === 6) {
								hash = (hash * factor) ^ state;
								hash2 = (hash2 * factor2) ^ 6;
							}
						}
					} else {
						state = colourGrid[cy + bottom][cx + left];

						//rowMsg += String(state > this.historyStates ? state - this.historyStates : 0);

						if (state > this.historyStates) {
							state -= this.historyStates;

							// adjust sub-cells for PCA rules based on transformation
							if (this.isPCA) {
								switch (transform) {
								case LifeConstants.modRot270:
									state = ((state << 1) & 15) | ((state & 8) >> 3);
									break;
								case LifeConstants.modRot180:
									state = ((state & 3) << 2) | ((state & 12) >> 2);
									break;
								case LifeConstants.modRot90:
									state = ((state >> 1) & 15) | ((state & 1) << 3);
									break;
								case LifeConstants.modFlipX:
									state = (state & 5) | ((state & 2) << 2) | ((state & 8) >> 2);
									break;
								case LifeConstants.modFlipY:
									state = (state & 10) | ((state & 1) << 2) | ((state & 4) >> 2);
									break;
								case LifeConstants.modRot90FlipX:
									state = ((state & 8) >> 3) | ((state & 4) >> 1) | ((state & 2) << 1) | ((state & 1) << 3);
									break;
								case LifeConstants.modRot90FlipY:
									state = ((state & 4) << 1) | ((state & 8) >> 1) | ((state & 1) << 1) | ((state & 2) >> 1);
									break;
								}

								// update the hash value
								hash = (hash * factor) ^ (yPrime ^ (x * primeX));
								hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));
								hash = (hash * factor) ^ state;
								hash2 = (hash2 * factor2) ^ state;
							} else {
								if (this.isRuleTree || this.isExtended) {
									// update the hash value
									hash = (hash * factor) ^ (yPrime ^ (x * primeX));
									hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));
									hash = (hash * factor) ^ state;
									hash2 = (hash2 * factor2) ^ state;
								} else {
									state = this.multiNumStates - state;
									hash = (hash * factor) ^ (yPrime ^ (x * primeX));
									hash2 = (hash2 * factor2) ^ (yPrime2 ^ (x * primeX2));
									hash = (hash * factor) ^ state;
									hash2 = (hash2 * factor2) ^ state;
								}
							}

							//rowMsgC += state;
						//} else {
							//rowMsgC += "0";
						}
					}
				}

				// update index
				iModHeight += 1;
				if (iModHeight === height) {
					iModHeight = 0;
					iDivHeight += 1;
				}
			}

			//if (checkWidth <= 4) {
				//console.log(y, rowMsg, rowMsgC);
			//}
		}

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get mod hash
	Life.prototype.getModHash = function(/** @type {BoundingBox} */ box, /** @type {number} */ transform, /** @type {boolean} */ state2, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		if (state2) {
			this.getModHash2(box, transform, firstHash1, firstHash2);
		} else {
			this.getModHashN(box, transform);
		}
	};

	// check for a specific mod transformation
	/** @returns {number} */
	Life.prototype.checkModHashType = function(/** @type {BoundingBox} */ box, /** @type {number} */ initialHashLower, /** @type {number} */ initialHashUpper, /** @type {number} */ initialMatches, /** @type {number} */ deltaX, /** @type {number} */ deltaY, /** @type {number} */ modFactor, /** @type {number} */ firstHashLower, /** @type {number} */ firstHashUpper) {
		var	/** @type {number} */ modMatch = 0,
			/** @type {boolean} */ isSpaceship = (deltaX !==0 && deltaY !== 0),
			/** @type {number} */ trans = -1;

		// get the matching hashes
		modMatch = this.checkModHash(box, initialHashLower, initialHashUpper, deltaX, deltaY, firstHashLower, firstHashUpper);

		// mask with the original matches
		modMatch &= initialMatches;

		if (modMatch) {
			// if this is FlipX then check if it is also true for FlipY
			if (modMatch & (1 << LifeConstants.modFlipX)) {
				trans = LifeConstants.modFlipX;

				// check for 90 degree rotates
				if (modMatch & (1 << LifeConstants.modFlipY)) {
					trans = LifeConstants.modFlipXorY;

					// check for Rot90
					if (modMatch & (1 << LifeConstants.modRot90)) {
						// check for Rot270
						if (modMatch & (1 << LifeConstants.modRot270)) {
							trans = LifeConstants.modFlipXorYorRotCWorCCW;
						}
					} else {
						// check for FlipDiag
						if (modMatch & (1 << LifeConstants.modRot90FlipX)) {
							if (modMatch & (1 << LifeConstants.modRot90FlipY)) {
								trans = LifeConstants.modFlipOrthorDiag;
							}
						}
					}
				} else {
					// check for 180 degree rotates
					if (modMatch & (1 << LifeConstants.modRot180)) {
						trans = LifeConstants.modFlipXorRot180;
					}
				}

				// don't allow flip unless mod is half period
				if (trans === LifeConstants.modFlipX && modFactor !== 2) {
					trans = -1;
				}
			}

			// if FlipY then check for FlipX
			if (trans === -1 && (modMatch & (1 << LifeConstants.modFlipY))) {
				trans = LifeConstants.modFlipY;

				// check for 90 degree rotates
				if (modMatch & (1 << LifeConstants.modFlipX)) {
					trans = LifeConstants.modFlipXorY;

					// check for Rot90
					if (modMatch & (1 << LifeConstants.modRot90)) {
						// check for Rot270
						if (modMatch & (1 << LifeConstants.modRot270)) {
							trans = LifeConstants.modFlipXorYorRotCWorCCW;
						}
					} else {
						// check for FlipDiag
						if (modMatch & (1 << LifeConstants.modRot90FlipX)) {
							if (modMatch & (1 << LifeConstants.modRot90FlipY)) {
								trans = LifeConstants.modFlipOrthorDiag;
							}
						}
					}
				} else {
					// check for 180 degree rotates
					if (modMatch & (1 << LifeConstants.modRot180)) {
						trans = LifeConstants.modFlipYorRot180;
					}
				}

				// don't allow flip unless mod is half period
				if (trans === LifeConstants.modFlipY && modFactor !== 2) {
					trans = -1;
				}
			}

			// if this is Rot90 then check if it also true for Rot270
			if (trans === -1 && (modMatch & (1 << LifeConstants.modRot90))) {
				trans = LifeConstants.modRot90;

				if (modMatch & (1 << LifeConstants.modRot270)) {
					trans = LifeConstants.modRotCWorCCW;

					// check for Flip X
					if (modMatch & (1 << LifeConstants.modFlipX)) {
						// check for Flip Y
						if (modMatch & (1 << LifeConstants.modFlipY)) {
							trans = LifeConstants.modFlipXorYorRotCWorCCW;
						}
					}

					// check for Rot90FlipX
					if (modMatch & (1 << LifeConstants.modRot90FlipX)) {
						// check for Rot90FlipY
						if (modMatch & (1 << LifeConstants.modRot90FlipY)) {
							trans = LifeConstants.modFlipDiagorRot90;
						}
					}
				}

				// don't allow rotate by 90 unless mod is quarter period
				if (trans === LifeConstants.modRot90 && modFactor !== 4) {
					trans = -1;
				}
			}

			// if this is Rot270 then check if it also true for Rot90
			if (trans === -1 && (modMatch & (1 << LifeConstants.modRot270))) {
				trans = LifeConstants.modRot270;

				if (modMatch & (1 << LifeConstants.modRot90)) {
					trans = LifeConstants.modRotCWorCCW;

					// check for Flip X
					if (modMatch & (1 << LifeConstants.modFlipX)) {
						// check for Flip Y
						if (modMatch & (1 << LifeConstants.modFlipY)) {
							trans = LifeConstants.modFlipXorYorRotCWorCCW;
						}
					}

					// check for Rot90FlipX
					if (modMatch & (1 << LifeConstants.modRot90FlipX)) {
						// check for Rot90FlipY
						if (modMatch & (1 << LifeConstants.modRot90FlipY)) {
							trans = LifeConstants.modFlipDiagorRot90;
						}
					}
				}

				// don't allow rotate by 90 unless mod is quarter period
				if (trans === LifeConstants.modRot270 && modFactor !== 4) {
					trans = -1;
				}
			}

			// if this is Flip / then check if it is also true for Flip \
			if (trans === -1 && (modMatch & (1 << LifeConstants.modRot90FlipX))) {
				trans = LifeConstants.modRot90FlipX;

				if (modMatch & (1 << LifeConstants.modRot90FlipY)) {
					trans = LifeConstants.modFlipDiag;
				} else {
					// check for Rot180
					if (!isSpaceship) {
						if (modMatch & (1 << LifeConstants.modRot180)) {
							trans = LifeConstants.modFlipDiagLRot180;
						}
					}
				}
			}

			// if this is Flip \ then check if it is also true for Flip /
			if (trans === -1 && (modMatch & (1 << LifeConstants.modRot90FlipY))) {
				trans = LifeConstants.modRot90FlipY;

				if (modMatch & (1 << LifeConstants.modRot90FlipX)) {
					trans = LifeConstants.modFlipDiag;
				} else {
					// check for Rot180
					if (!isSpaceship) {
						if (modMatch & (1 << LifeConstants.modRot180)) {
							trans = LifeConstants.modFlipDiagRRot180;
						}
					}
				}
			}

			// if this is Rot180 check for Flip / and Flip \
			if (trans === -1 && (modMatch & (1 << LifeConstants.modRot180))) {
				trans = LifeConstants.modRot180;

				if (modMatch & (1 << LifeConstants.modRot90FlipX)) {
					trans = LifeConstants.modFlipDiagLRot180;
				} else {
					if (modMatch & (1 << LifeConstants.modRot90FlipY)) {
						trans = LifeConstants.modFlipDiagRRot180;
					}
				}

				// don't allow rotate by 180 unless mod is half period
				if (trans === LifeConstants.modRot180 && modFactor !== 2) {
					trans = -1;
				}
			}
		}

		// check for diagonal spaceships
		if (isSpaceship) {
			if (trans === LifeConstants.modFlipDiag) {
				if (deltaX === deltaY) {
					trans = LifeConstants.modRot90FlipX;
				}
				if (deltaX === -deltaY) {
					trans = LifeConstants.modRot90FlipY;
				}
			} else {
				if ((modMatch & (1 << LifeConstants.modRot90FlipY)) && deltaX === deltaY) {
					trans = -1;
				}

				if ((modMatch & (1 << LifeConstants.modRot90FlipX)) && deltaX === -deltaY) {
					trans = -1;
				}
			}
		}

		// disable invalid Mods from the factor
		if (this.isHex || this.isTriangular) {
			if (!(modFactor === 1 || modFactor === 2 || modFactor === 3 || modFactor === 6)) {
				trans = -1;
			}
		} else {
			if (!(modFactor === 1 || modFactor === 2 || modFactor === 4)) {
				trans = -1;
			}
		}
		// return transformation
		return trans;
	};

	// check mod hashes
	/** @returns {number} */
	Life.prototype.checkModHash = function(/** @type {BoundingBox} */ box, /** @type {number} */ initialHash1, /** @type {number} */ initialHash2, /** @type {number} */ deltaX, /** @type {number} */ deltaY, /** @type {number} */ firstHash1, /** @type {number} */ firstHash2) {
		var	/** @type {number} */ trans = 0,
			/** @type {boolean} */ twoState = (this.multiNumStates <= 2 && !(this.isRuleTree || this.isLifeHistory)),
			/** @type {number} */ i = 0,
			/** @type {boolean} */ valid = true,
			/** @type {Array<number>} */ checkList = [],
			/** @type {number} */ result = 0;

		// check whether looking at oscillator or spaceship
		if (deltaX === 0 && deltaY === 0) {
			checkList = LifeConstants.modOscillator;
		} else {
			checkList = LifeConstants.modSpaceship;
		}

		// check each transformation
		for (i = 0; i < checkList.length; i += 1) {
			trans = checkList[i];
			this.getModHash(box, trans, twoState, firstHash1, firstHash2);

			//console.log(trans, LifeConstants.modTypeName[trans], hash, initialHash);

			if (this.hashLower === initialHash1 && this.hashUpper === initialHash2) {
				valid = true;

				//console.log("found", trans, LifeConstants.modTypeName[trans]);

				// if a spaceship then check valid transformations based on direction
				if (deltaX !== 0 || deltaY !== 0) {
					// check for vertical direction with Flip Y or Flip / or Flip \
					if (deltaX === 0 && (trans === LifeConstants.modFlipY || trans === LifeConstants.modRot90FlipX || trans === LifeConstants.modRot90FlipY)) {
						valid = false;
					}

					// check for horizontal direction with Flip X or Flip / or Flip \
					if (deltaY === 0 && (trans === LifeConstants.modFlipX || trans === LifeConstants.modRot90FlipX || trans === LifeConstants.modRot90FlipY)) {
						valid = false;
					}
				}

				// add to results if valid
				if (valid) {
					result |= 1 << trans;
				}
			}
		}

		return result;
	};

	// get hash from pattern
	Life.prototype.getHash = function(/** @type {BoundingBox} */ box) {
		var	/** @type {boolean} */ twoState = (this.multiNumStates <= 2 && !this.isRuleTree),
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {number} */ gridWidth = this.colourGrid[0].length,
			/** @type {number} */ timing = performance.now();

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		if (Controller.useWASM && Controller.wasmEnableGetHash && this.view.wasmEnabled) {
			if (this.isLifeHistory) {
				WASM.getHashLifeHistory(
					colourGrid.whole.byteOffset | 0,
					this.overlayGrid.whole.byteOffset | 0,
					box.bottomY | 0,
					box.leftX | 0,
					box.topY | 0,
					box.rightX | 0,
					gridWidth | 0,
					this.aliveStart | 0,
					(ViewConstants.stateMap[6] + 128) | 0,
					this.sharedBuffer.byteOffset | 0
				);
			} else if (twoState) {
				WASM.getHashTwoState(
					colourGrid.whole.byteOffset | 0,
					box.bottomY | 0,
					box.leftX | 0,
					box.topY | 0,
					box.rightX | 0,
					gridWidth | 0,
					this.aliveStart | 0,
					this.sharedBuffer.byteOffset | 0
				);
			} else if (this.isSuper) {
				WASM.getHashSuper(
					colourGrid.whole.byteOffset | 0,
					box.bottomY | 0,
					box.leftX | 0,
					box.topY | 0,
					box.rightX | 0,
					gridWidth | 0,
					this.sharedBuffer.byteOffset | 0
				);
			} else if (this.isRuleTree || this.isPCA || this.isExtended) {
				WASM.getHashRuleLoaderOrPCAOrExtended(
					colourGrid.whole.byteOffset | 0,
					box.bottomY | 0,
					box.leftX | 0,
					box.topY | 0,
					box.rightX | 0,
					gridWidth | 0,
					this.historyStates | 0,
					this.sharedBuffer.byteOffset | 0
				);
			} else {
				WASM.getHashGenerations(
					colourGrid.whole.byteOffset | 0,
					box.bottomY | 0,
					box.leftX | 0,
					box.topY | 0,
					box.rightX | 0,
					gridWidth | 0,
					this.historyStates | 0,
					this.multiNumStates | 0,
					this.sharedBuffer.byteOffset | 0
				);
			}

			//this.getHashJS(box);
			//console.log(this.hashLower, this.hashUpper);
			this.hashLower = this.sharedBuffer[0] | 0;
			this.hashUpper = this.sharedBuffer[1] | 0;
			//console.log(this.hashLower, this.hashUpper);
		} else {
			this.getHashJS(box);
		}

		timing = performance.now() - timing;
		if (Controller.wasmTiming) {
			this.view.menuManager.updateTimingItem("getHash", timing, Controller.useWASM && Controller.wasmEnableGetHash && this.view.wasmEnabled);
		}
	};

	// get first hash from pattern
	Life.prototype.getFirstHash = function(/** @type {BoundingBox} */ box) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = box.leftX,
			/** @type {number} */ y = box.bottomY,
			/** @type {number} */ right = box.rightX,
			/** @type {number} */ top = box.topY,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ yShift = 0,
			/** @type {number} */ hashX = 0,
			/** @type {number} */ hashY = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart;

		// create a hash from the first alive cell
		hashX = x;
		hashY = y;

		for (cy = y; cy <= top; cy += 1) {
			yShift = cy - hashY;
			colourRow = colourGrid[cy];
			yPrime = yShift * primeY;
			yPrime2 = yShift * primeY2;

			for (cx = x; cx <= right; cx += 1) {
				if (colourRow[cx] >= aliveStart) {

					hash = (hash * factor) ^ (yPrime ^ ((cx - hashX) * primeX));
					hash2 = (hash2 * factor2) ^ (yPrime2 ^ ((cx - hashX) * primeX2));
					this.hashLower = hash | 0;
					this.hashUpper = hash2 | 0;
					//console.log("firstHash", hash, "xy", cx - hashX, yshift);

					return;
				}
			}
		}

		this.hashLower = hash | 0;
		this.hashUpper = hash2 | 0;
	};

	// get hash from pattern
	Life.prototype.getHashJS = function(/** @type {BoundingBox} */ box) {
		var	/** @type {number} */ hash = 31415962,
			/** @const {number} */ factor = 1000003,
			/** @const {number} */ primeX = 19349663,
			/** @const {number} */ primeY = 73856093,
			/** @type {number} */ hash2 = 27182818,
			/** @const {number} */ factor2 = 3000077,
			/** @const {number} */ primeX2 = 60000047,
			/** @const {number} */ primeY2 = 45230483,
			/** @type {number} */ x = box.leftX,
			/** @type {number} */ y = box.bottomY,
			/** @type {number} */ right = box.rightX,
			/** @type {number} */ top = box.topY,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ yShift = 0,
			/** @type {number} */ yPrime = 0,
			/** @type {number} */ yPrime2 = 0,
			/** @type {number} */ state = 0,
			/** @type {boolean} */ twoState = (this.multiNumStates <= 2 && !this.isRuleTree),
			/** @type {number} */ hashX = 0,
			/** @type {number} */ hashY = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {Uint8Array} */ overlayRow = null,
			/** @type {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128;

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// create a hash from every alive cell
		hashX = x;
		hashY = y;

		for (cy = y; cy <= top; cy += 1) {
			yShift = cy - hashY;
			yPrime = (yShift * primeY);
			yPrime2 = (yShift * primeY2);

			colourRow = colourGrid[cy];
			if (this.isLifeHistory) {
				overlayRow = overlayGrid[cy];
				for (cx = x; cx <= right; cx += 1) {
					if (colourRow[cx] >= aliveStart || overlayRow[cx] === state6) {
						hash = (hash * factor) ^ (yPrime ^ ((cx - hashX) * primeX));
						hash2 = (hash2 * factor2) ^ (yPrime2 ^ ((cx - hashX) * primeX2));
						if (overlayRow[cx] === state6) {
							hash = (hash * factor) ^ 6;
							hash2 = (hash2 * factor2) ^ 6;
						}
					}
				}
			} else if (twoState) {
				for (cx = x; cx <= right; cx += 1) {
					if (colourRow[cx] >= aliveStart) {
						hash = (hash * factor) ^ (yPrime ^ ((cx - hashX) * primeX));
						hash2 = (hash2 * factor2) ^ (yPrime2 ^ ((cx - hashX) * primeX2));
					}
				}
			} else if (this.isSuper) {
				for (cx = x; cx <= right; cx += 1) {
					if ((colourRow[cx] & 1) || colourRow[cx] === 6) {
						hash = (hash * factor) ^ (yPrime ^ ((cx - hashX) * primeX));
						hash2 = (hash2 * factor2) ^ (yPrime2 ^ ((cx - hashX) * primeX2));
						if (colourRow[cx] === 6) {
							hash = (hash * factor) ^ 6;
							hash2 = (hash2 * factor2) ^ 6;
						}
					}
				}
			} else if (this.isRuleTree || this.isPCA || this.isExtended) {
				for (cx = x; cx <= right; cx += 1) {
					// get the raw state
					state = colourRow[cx];
					if (state > this.historyStates) {
						state -= this.historyStates;
						hash = (hash * factor) ^ (yPrime ^ ((cx - hashX) * primeX));
						hash2 = (hash2 * factor2) ^ (yPrime2 ^ ((cx - hashX) * primeX2));
						hash = (hash * factor) ^ state;
						hash2 = (hash2 * factor2) ^ state;
					}
				}
			} else {
				for (cx = x; cx <= right; cx += 1) {
					// get the raw state
					state = colourRow[cx];
					if (state > this.historyStates) {
						state -= this.historyStates;
						state = this.multiNumStates - state;
						hash = (hash * factor) ^ (yPrime ^ ((cx - hashX) * primeX));
						hash2 = (hash2 * factor2) ^ (yPrime2 ^ ((cx - hashX) * primeX2));
						hash = (hash * factor) ^ state;
						hash2 = (hash2 * factor2) ^ state;
					}
				}
			}
		}

		this.hashLower = hash;
		this.hashUpper = hash2;
	};

	// get greatest common divisor
	/** @returns {number} */
	Life.prototype.gcd = function(/** @type {number} */ a, /** @type {number} */ b) {
		var	/** @type {number} */ temp = 0;

		while (b !== 0) {
			temp = b;
			b = a % b;
			a = temp;
		}

		return a;
	};

	// get oblique displacement name
	/** @returns {string} */
	Life.prototype.getDisplacementName = function(/** @type {number} */ x, /** @type {number} */ y) {
		var	/** @type {string} */ result = "Oblique",
			/** @type {number} */ swap = 0,
			/** @type {number} */ d = 0;

		// order the deltas
		if (x > y) {
			swap = x;
			x = y;
			y = swap;
		}

		// find greatest common divisor
		d = this.gcd(x, y);
		x /= d;
		y /= d;

		// check for known displacements
		switch (x * 10 + y) {
		case 12:
			result += " Knightwise";
			break;
		case 13:
			result += " Camelwise";
			break;
		case 14:
			result += " Giraffewise";
			break;
		case 15:
			result += " Ibiswise";
			break;
		case 16:
			result += " Flamingowise";
			break;
		case 23:
			result += " Zebrawise";
			break;
		case 34:
			result += " Antelopewise";
			break;
		}

		return result;
	};

	// convert number to string with no more than n decimal places
	/** @returns {string} */
	Life.prototype.toPlaces = function(/** @type {number} */ value, /** @type {number} */ places) {
		var	/** @type {string} */ result = "";

		if (value === Math.floor(value)) {
			result = String(value);
		} else {
			result = value.toFixed(places);
		}

		return result;
	};

	// create cell period map
	Life.prototype.createCellPeriodMap = function(/** @type {View} */ view) {
		var	/** @type {number} */ numCols = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ row = 0,
			/** @type {number} */ row2 = 0,
			/** @type {number} */ p = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ hue = 0,
			/** @type {number} */ red = 0,
			/** @type {number} */ green = 0,
			/** @type {number} */ blue = 0,
			/** @type {string} */ rgbString = "",
			/** @type {number} */ pixCol = 0,
			/** @type {number} */ gridCol = 0xff505050,
			/** @type {number} */ boundedCol = 0xff808080,
			/** @type {number} */ cellBorderSize = this.cellBorderSize,
			/** @type {number} */ gridBorderSize = 0,
			/** @type {number} */ cellSize = 0,
			/** @type {number} */ rowWidth = 0,
			/** @type {number} */ colHeight = 0,
			/** @type {Array} */ periodCols = [],
			/** @type {boolean} */ drawGrid = false,
			/** @type {ImageData} */ data = null,
			/** @type {Uint32Array} */ data32 = null,
			/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ legendWidth = 50,
			/** @type {number} */ width = this.displayWidth - (displayScale * (legendWidth + legendWidth + 20)),
			/** @type {number} */ height = this.displayHeight - (displayScale * 90),
			/** @type {number} */ offset = 0,
			/** @type {number} */ i = 0,
			/** @type {number} */ vy = 0,
			/** @type {number} */ vo = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ ex = 0,
			/** @type {number} */ inc = 0,
			/** @type {number} */ xPos = 0,
			/** @type {Array<number>} */ subPeriodIndices = [],
			/** @type {Array<string>} */ subPeriodColours = LifeConstants.subPeriodColours,
			/** @type {number} */ cellPeriodWidth = this.origCellPeriodWidth;

		// adjust the colours based on endian
		if (!this.littleEndian) {
			gridCol = 0x505050ff;
			boundedCol = 0x808080ff;
		}

		// check for hex cells
		if (this.isHex) {
			cellPeriodWidth += Math.ceil(this.cellPeriodHeight / 2);
		}

		// default to large cell size
		this.cellPeriodCellSize = 8;
		cellSize = this.cellPeriodCellSize;

		// determine the image scale to select cell borders
		x = width / ((cellPeriodWidth + cellBorderSize + cellBorderSize) * cellSize);
		y = height / ((this.cellPeriodHeight + cellBorderSize + cellBorderSize) * cellSize);
		if (x > y) {
			s = y;
		} else {
			s = x;
		}

		// make cells integer width and enable grid if scale > 1
		if (s >= 1) {
			s |= 0;
			drawGrid = true;

			// add an extra line at the right and the bottom to complete the grid
			gridBorderSize = 1;
		} else {
			// switch to smaller cell size
			this.cellPeriodCellSize = 1;
			cellSize = 1;
		}

		// find subperiods excluding period 1 and oscillator period
		for (i = 2; i < this.popSubPeriod.length - 1; i += 1) {
			if (this.popSubPeriod[i] > 0) {
				subPeriodIndices.push(i);
			}
		}
		numCols = subPeriodIndices.length;
		this.cellPeriodNumCols = numCols;

		for (i = 0; i < subPeriodColours.length && i < subPeriodIndices.length; i += 1) {
			periodCols[subPeriodIndices[subPeriodIndices.length - 1 - i]] = subPeriodColours[i];
		}

		// generate colours for the rest
		for (i = 0; i < subPeriodIndices.length - subPeriodColours.length; i += 1) {
			hue = Math.floor(360 * (i / (subPeriodIndices.length - subPeriodColours.length)));
			periodCols[subPeriodIndices[i]] = "hsl(" + hue + ",100%," + (70 - (i & 3) * 12) + "%)";
		}

		// set colours for period 1 and oscillator period
		periodCols[0] = "black";
		periodCols[1] = "rgb(168,168,168)";
		periodCols[this.popSubPeriod.length - 1] = "rgb(255,255,255)";

		// create a colour for state 6 cells
		if (this.cellPeriodState6) {
			periodCols[-1] = "rgb(96,96,96)";
			x = -1;
		} else {
			x = 0;
		}

		// convert colours into RGB
		while (x < this.popSubPeriod.length) {
			if (x === -1 || x === 0 || this.popSubPeriod[x] > 0) {
				this.cellPeriodContext.fillStyle = periodCols[x];
				rgbString = /** @type {!string} */ (this.cellPeriodContext.fillStyle);
				red = parseInt(rgbString.substring(1, 3), 16);
				green = parseInt(rgbString.substring(3, 5), 16);
				blue = parseInt(rgbString.substring(5, 7), 16);
				if (this.littleEndian) {
					this.cellPeriodRGB[x] = (255 << 24) | (blue << 16) | (green << 8) | red;
				} else {
					this.cellPeriodRGB[x] = (red << 24) | (green << 16) | (blue << 8) | 255;
				}
			}

			x += 1;
		}

		// resize the image and canvas to fix the period map with "cellSize" cells
		rowWidth = cellSize * (cellPeriodWidth + cellBorderSize + cellBorderSize) + gridBorderSize;
		if (this.isHex) {
			if ((this.cellPeriodHeight & 1) === 0) {
				rowWidth += cellSize / 2;
			}
		}
		rowWidth |= 0;
		colHeight = cellSize * (this.cellPeriodHeight + cellBorderSize + cellBorderSize) + gridBorderSize;
		this.cellPeriodCanvas.width = rowWidth;
		this.cellPeriodCanvas.height = colHeight;

		// clear the canvas
		this.cellPeriodContext.fillStyle = "rgba(0,0,0,0)"; //"black";
		this.cellPeriodContext.fillRect(0, 0, this.cellPeriodCanvas.width, this.cellPeriodCanvas.height);

		// get the pixel data
		data = this.cellPeriodContext.getImageData(0, 0, this.cellPeriodCanvas.width, this.cellPeriodCanvas.height);
		data32 = new Uint32Array(data.data.buffer);

		// check for hex grid
		if (this.isHex) {
			// set the offset for drawing
			inc = -cellSize / 2;
			offset = (this.cellPeriodHeight >> 1) * cellSize - inc;
			if ((this.cellPeriodHeight & 1) === 0) {
				offset += inc;
			}
		}

		// draw the cells
		for (y = 0; y < this.cellPeriodHeight; y += 1) {
			for (x = 0; x < this.origCellPeriodWidth; x += 1) {
				p = this.cellPeriod[y * this.origCellPeriodWidth + x];
				pixCol = this.cellPeriodRGB[p];
				for (cy = 0; cy < cellSize; cy += 1) {
					row = ((y + cellBorderSize) * cellSize + cy) * rowWidth + ((x + cellBorderSize) * cellSize) + offset;
					for (cx = 0; cx < cellSize; cx += 1) {
						data32[(row + cx) | 0] = pixCol;
					}
				}
			}
			offset += inc;
		}

		// draw the bounded grid if required
		if (this.boundedGridType !== -1) {
			// use bounded grid colour
			pixCol = boundedCol;
		} else  {
			// draw border of black cells
			pixCol = 0xff000000;
		}

		// check for hex grid
		sx = 0;
		ex = (this.origCellPeriodWidth + 2) * cellSize;

		if (this.isHex) {
			// set the offset for drawing
			inc = -cellSize / 2;
			offset = (this.cellPeriodHeight >> 1) * cellSize - inc;
			if ((this.cellPeriodHeight & 1) === 0) {
				offset += inc;
			}
			sx = cellSize;
			ex = (this.origCellPeriodWidth + 1) * cellSize;
		}

		// draw the bounded grid or border cells
		y = 0;
		offset -= inc;
		for (cy = 0; cy < cellSize; cy += 1) {
			// draw top row
			row = cy * rowWidth + offset;
			for (x = 0; x < ex; x += 1) {
				data32[row + x] = pixCol;
			}

			// draw bottom row
			row += (this.cellPeriodHeight + 1) * cellSize * rowWidth - offset;
			for (x = sx; x < (this.origCellPeriodWidth + 2) * cellSize; x += 1) {
				data32[row + x] = pixCol;
			}
		}
		offset += inc;

		for (y = 0; y < this.cellPeriodHeight; y += 1) {
			for (cy = 0; cy < cellSize; cy += 1) {
				row = ((y + cellBorderSize) * cellSize + cy) * rowWidth + offset;
				row2 = row + cellSize * (this.origCellPeriodWidth + 1);
				for (cx = 0; cx < cellSize; cx += 1) {
					data32[row + cx] = pixCol;
					data32[row2 + cx] = pixCol;
				}
			}
			offset += inc;
		}

		// draw the grid if required
		if (drawGrid) {
			// check for hex grid
			if (this.isHex) {
				// set the offset for drawing
				offset = (this.cellPeriodHeight >> 1) * cellSize - inc + cellSize;
				if ((this.cellPeriodHeight & 1) === 0) {
					offset += inc;
				}
			}

			// draw the grid
			for (y = -1; y <= this.cellPeriodHeight + 1; y += 1) {
				offset += inc;
				row = ((y + cellBorderSize) * cellSize) * rowWidth;

				if (this.isHex && y === -1) {
					ex = (this.origCellPeriodWidth + cellBorderSize) * cellSize;
				} else {
					ex = (this.origCellPeriodWidth + cellBorderSize + cellBorderSize) * cellSize;
				}

				if (this.isHex && y === this.cellPeriodHeight) {
					sx = -inc;
				} else if (this.isHex && y === this.cellPeriodHeight + 1) {
					sx = cellSize - inc;
				} else {
					sx = 0;
				}

				for (x = sx + offset; x < ex + offset + 1; x += 1) {
					if (x >= 0) {
						data32[row + x] = gridCol;
					}
				}
				if (this.isHex && y > 0) {
					for (cx = 0; cx < -inc; cx += 1) {
						if (x + cx < rowWidth) {
							data32[row + x + cx] = gridCol;
						}
					}
				}
			}

			sx = -1;
			ex = this.origCellPeriodWidth + 1;
			if (this.isHex) {
				offset = (this.cellPeriodHeight >> 1) * cellSize - inc;
				sx += 1;
				ex += 1;
				if ((this.cellPeriodHeight & 1) === 0) {
					offset += inc;
				}
			}

			for (x = sx; x <= ex; x += 1) {
				vy = 0;
				vo = offset;
				for (y = 0; y < (this.cellPeriodHeight + cellBorderSize + cellBorderSize) * cellSize; y += 1) {
					if ((vy % cellSize) === 0) {
						vo +=inc;
					}
					vy += 1;

					xPos = ((x + cellBorderSize) * cellSize) + vo;
					if (xPos >= 0 && xPos < this.cellPeriodCanvas.width) {
						if (this.isHex && ((x === ex && y < cellSize) || (x === sx && y >= (this.cellPeriodHeight + cellBorderSize) * cellSize))) {
							// do nothing
						} else {
							data32[(y * rowWidth) + xPos] = gridCol;
						}
					}
				}
			}
		} else {
			// draw a border
			if (this.isHex) {
				offset = colHeight >> 1;
				inc = -0.5;
			}

			if (this.boundedGridType !== -1) {
				pixCol = boundedCol;
			} else {
				pixCol = gridCol;
			}

			ex = 0;
			if ((this.cellPeriodHeight & 1) !== 0) {
				ex += inc;
			}

			row = rowWidth * (colHeight - 1);
			for (x = 0; x < rowWidth - offset; x += 1) {
				data32[x + offset] = pixCol;
				data32[x + row] = pixCol;
			}

			for (y = 0; y < colHeight; y += 1) {
				data32[y * rowWidth + (offset | 0)] = pixCol;
				data32[y * rowWidth + rowWidth - 1 + (ex | 0)] = pixCol;
				offset += inc;
				ex += inc;
			}
		}

		// convert RGB array
		if (this.cellPeriodState6) {
			x = -1;
		} else {
			x = 0;
		}

		this.popSubPeriodHelp = [];
		while (x < this.popSubPeriod.length) {
			if (x === -1 || this.popSubPeriod[x] > 0) {
				pixCol = this.cellPeriodRGB[x];
				if (this.littleEndian) {
					red = pixCol & 255;
					green = (pixCol >> 8) & 255;
					blue = (pixCol >> 16) & 255;
				} else {
					red = pixCol >> 24;
					green = (pixCol >> 16) & 255;
					blue = (pixCol >> 8) & 255;
				}
				this.cellPeriodRGB[x] = (red << 16) | (green << 8) | blue;
				this.popSubPeriodHelp[this.popSubPeriodHelp.length] = x;
			}

			x += 1;
		}

		// update the image
		this.cellPeriodContext.putImageData(data, 0, 0);

		// save the cell period size
		this.cellPeriodWidth = cellPeriodWidth;

		// create the table row values for page up and page down
		view.setResultsPosition();
	};

	// create cell frequency map
	Life.prototype.createCellFrequencyMap = function(/** @type {Uint32Array} */ cellCounts, /** @type {number} */ period) {
		var	/** @type {number} */ numCols = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ row = 0,
			/** @type {number} */ row2 = 0,
			/** @type {number} */ p = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ hue = 0,
			/** @type {number} */ red = 0,
			/** @type {number} */ green = 0,
			/** @type {number} */ blue = 0,
			/** @type {string} */ rgbString = "",
			/** @type {number} */ pixCol = 0,
			/** @type {number} */ gridCol = 0xff505050,
			/** @type {number} */ boundedCol = 0xff808080,
			/** @type {number} */ cellBorderSize = this.cellBorderSize,
			/** @type {number} */ gridBorderSize = 0,
			/** @type {number} */ cellSize = 0,
			/** @type {number} */ rowWidth = 0,
			/** @type {number} */ colHeight = 0,
			/** @type {Array} */ periodCols = [],
			/** @type {boolean} */ drawGrid = false,
			/** @type {ImageData} */ data = null,
			/** @type {Uint32Array} */ data32 = null,
			/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ legendWidth = 50,
			/** @type {number} */ width = this.displayWidth - (displayScale * (legendWidth + legendWidth + 20)),
			/** @type {number} */ height = this.displayHeight - (displayScale * 90),
			/** @type {number} */ offset = 0,
			/** @type {number} */ vy = 0,
			/** @type {number} */ vo = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ ex = 0,
			/** @type {number} */ inc = 0,
			/** @type {number} */ xPos = 0,
			/** @type {Object} */ index = {},
			/** @type {Uint32Array} */ sortedCounts = new Uint32Array(cellCounts),
			/** @type {Array<number>} */ uniqueCounts = [],
			/** @type {number} */ cellPeriodWidth = this.origCellPeriodWidth;

		// adjust the colours based on endian
		if (!this.littleEndian) {
			gridCol = 0x505050ff;
			boundedCol = 0x808080ff;
		}

		// check for hex cells
		if (this.isHex) {
			cellPeriodWidth += Math.ceil(this.cellPeriodHeight / 2);
		}

		// default to large cell size
		this.cellPeriodCellSize = 8;
		cellSize = this.cellPeriodCellSize;

		// determine the image scale to select cell borders
		x = width / ((cellPeriodWidth + cellBorderSize + cellBorderSize) * cellSize);
		y = height / ((this.cellPeriodHeight + cellBorderSize + cellBorderSize) * cellSize);
		if (x > y) {
			s = y;
		} else {
			s = x;
		}

		// make cells integer width and enable grid if scale > 1
		if (s >= 1) {
			s |= 0;
			drawGrid = true;

			// add an extra line at the right and the bottom to complete the grid
			gridBorderSize = 1;
		} else {
			// switch to smaller cell size
			this.cellPeriodCellSize = 1;
			cellSize = 1;
		}

		// get the unique cell counts from the sorted cell counts list
		sortedCounts.sort();
		x = sortedCounts[0];
		if (sortedCounts.length > 1) {
			for (i = 1; i < sortedCounts.length; i += 1) {
				y = sortedCounts[i];
				if (x !== y) {
					uniqueCounts[uniqueCounts.length] = x;
					x = y;
				}
			}
		} else {
			y = x;
		}

		uniqueCounts[uniqueCounts.length] = y;
		numCols = uniqueCounts.length;

		// save the unique cell counts
		this.uniqueCellCounts = new Uint32Array(uniqueCounts);

		x = 0;
		if (uniqueCounts[x] === 0) {
			periodCols[x] = "#000000";
			x += 1;
		}

		y = uniqueCounts.length - 1;
		if (uniqueCounts[y] === period) {
			periodCols[y] = "rgb(168, 168, 168)";
			y -= 1;
		}

		// make colours for the unique cell counts
		for (i = x; i <= y; i += 1) {
			hue = Math.floor(360 * ((i - x) / (y - x + 1)));
			periodCols[i] = "hsl(" + hue + ",100%," + (70 - ((i - x) & 3) * 12) + "%)";
		}

		// create a colour for state 6 cells if present
		if (this.cellPeriodState6) {
			periodCols[-1] = "rgb(96,96,96)";
			x = -1;
		} else {
			x = 0;
		}

		// convert colours into RGB
		while (x < numCols) {
			this.cellFrequencyContext.fillStyle = periodCols[x];
			rgbString = /** @type {!string} */ (this.cellFrequencyContext.fillStyle);
			red = parseInt(rgbString.substring(1, 3), 16);
			green = parseInt(rgbString.substring(3, 5), 16);
			blue = parseInt(rgbString.substring(5, 7), 16);
			if (this.littleEndian) {
				this.cellFrequencyRGB[x] = (255 << 24) | (blue << 16) | (green << 8) | red;
			} else {
				this.cellFrequencyRGB[x] = (red << 24) | (green << 16) | (blue << 8) | 255;
			}

			x += 1;
		}

		// create the reverse lookup index
		for (i = 0; i < uniqueCounts.length; i += 1) {
			index[uniqueCounts[i]] = i;
		}

		// allocate the unique cell counts counts
		this.uniqueCellCountsCounts = new Uint32Array(uniqueCounts.length);

		// create the cell counts counts data
		for (i = 0; i < cellCounts.length; i += 1) {
			this.uniqueCellCountsCounts[index[cellCounts[i]]] += 1;
		}

		x = 0;
		for (i = 0; i < this.uniqueCellCountsCounts.length; i += 1) {
			x += this.uniqueCellCountsCounts[i];
		}
		this.cellCountsTotal = x;
		// make the first colour black if there are cells with zero count
		// resize the image and canvas to fix the period map with "cellSize" cells
		rowWidth = cellSize * (cellPeriodWidth + cellBorderSize + cellBorderSize) + gridBorderSize;
		if (this.isHex) {
			if ((this.cellPeriodHeight & 1) === 0) {
				rowWidth += cellSize / 2;
			}
		}
		rowWidth |= 0;
		colHeight = cellSize * (this.cellPeriodHeight + cellBorderSize + cellBorderSize) + gridBorderSize;
		this.cellFrequencyCanvas.width = rowWidth;
		this.cellFrequencyCanvas.height = colHeight;

		// clear the canvas
		this.cellFrequencyContext.fillStyle = "rgba(0,0,0,0)"; //"black";
		this.cellFrequencyContext.fillRect(0, 0, this.cellFrequencyCanvas.width, this.cellFrequencyCanvas.height);

		// get the pixel data
		data = this.cellFrequencyContext.getImageData(0, 0, this.cellFrequencyCanvas.width, this.cellFrequencyCanvas.height);
		data32 = new Uint32Array(data.data.buffer);

		// check for hex grid
		if (this.isHex) {
			// set the offset for drawing
			inc = -cellSize / 2;
			offset = (this.cellPeriodHeight >> 1) * cellSize - inc;
			if ((this.cellPeriodHeight & 1) === 0) {
				offset += inc;
			}
		}

		// draw the cells
		for (y = 0; y < this.cellPeriodHeight; y += 1) {
			for (x = 0; x < this.origCellPeriodWidth; x += 1) {
				if (this.cellPeriodState6 && this.cellPeriod[y * this.origCellPeriodWidth + x] === -1){
					pixCol = this.cellFrequencyRGB[-1];
				} else {
					p = cellCounts[y * this.origCellPeriodWidth + x];
					pixCol = this.cellFrequencyRGB[index[p]];
				}

				for (cy = 0; cy < cellSize; cy += 1) {
					row = ((y + cellBorderSize) * cellSize + cy) * rowWidth + ((x + cellBorderSize) * cellSize) + offset;
					for (cx = 0; cx < cellSize; cx += 1) {
						data32[(row + cx) | 0] = pixCol;
					}
				}
			}
			offset += inc;
		}

		// draw the bounded grid if required
		if (this.boundedGridType !== -1) {
			// use bounded grid colour
			pixCol = boundedCol;
		} else  {
			// draw border of black cells
			pixCol = 0xff000000;
		}

		// check for hex grid
		sx = 0;
		ex = (this.origCellPeriodWidth + 2) * cellSize;

		if (this.isHex) {
			// set the offset for drawing
			inc = -cellSize / 2;
			offset = (this.cellPeriodHeight >> 1) * cellSize - inc;
			if ((this.cellPeriodHeight & 1) === 0) {
				offset += inc;
			}
			sx = cellSize;
			ex = (this.origCellPeriodWidth + 1) * cellSize;
		}

		// draw the bounded grid or border cells
		y = 0;
		offset -= inc;
		for (cy = 0; cy < cellSize; cy += 1) {
			// draw top row
			row = cy * rowWidth + offset;
			for (x = 0; x < ex; x += 1) {
				data32[row + x] = pixCol;
			}

			// draw bottom row
			row += (this.cellPeriodHeight + 1) * cellSize * rowWidth - offset;
			for (x = sx; x < (this.origCellPeriodWidth + 2) * cellSize; x += 1) {
				data32[row + x] = pixCol;
			}
		}
		offset += inc;

		for (y = 0; y < this.cellPeriodHeight; y += 1) {
			for (cy = 0; cy < cellSize; cy += 1) {
				row = ((y + cellBorderSize) * cellSize + cy) * rowWidth + offset;
				row2 = row + cellSize * (this.origCellPeriodWidth + 1);
				for (cx = 0; cx < cellSize; cx += 1) {
					data32[row + cx] = pixCol;
					data32[row2 + cx] = pixCol;
				}
			}
			offset += inc;
		}

		// draw the grid if required
		if (drawGrid) {
			// check for hex grid
			if (this.isHex) {
				// set the offset for drawing
				offset = (this.cellPeriodHeight >> 1) * cellSize - inc + cellSize;
				if ((this.cellPeriodHeight & 1) === 0) {
					offset += inc;
				}
			}

			// draw the grid
			for (y = -1; y <= this.cellPeriodHeight + 1; y += 1) {
				offset += inc;
				row = ((y + cellBorderSize) * cellSize) * rowWidth;

				if (this.isHex && y === -1) {
					ex = (this.origCellPeriodWidth + cellBorderSize) * cellSize;
				} else {
					ex = (this.origCellPeriodWidth + cellBorderSize + cellBorderSize) * cellSize;
				}

				if (this.isHex && y === this.cellPeriodHeight) {
					sx = -inc;
				} else if (this.isHex && y === this.cellPeriodHeight + 1) {
					sx = cellSize - inc;
				} else {
					sx = 0;
				}

				for (x = sx + offset; x < ex + offset + 1; x += 1) {
					if (x >= 0) {
						data32[row + x] = gridCol;
					}
				}
				if (this.isHex && y > 0) {
					for (cx = 0; cx < -inc; cx += 1) {
						if (x + cx < rowWidth) {
							data32[row + x + cx] = gridCol;
						}
					}
				}
			}

			sx = -1;
			ex = this.origCellPeriodWidth + 1;
			if (this.isHex) {
				offset = (this.cellPeriodHeight >> 1) * cellSize - inc;
				sx += 1;
				ex += 1;
				if ((this.cellPeriodHeight & 1) === 0) {
					offset += inc;
				}
			}

			for (x = sx; x <= ex; x += 1) {
				vy = 0;
				vo = offset;
				for (y = 0; y < (this.cellPeriodHeight + cellBorderSize + cellBorderSize) * cellSize; y += 1) {
					if ((vy % cellSize) === 0) {
						vo +=inc;
					}
					vy += 1;

					xPos = ((x + cellBorderSize) * cellSize) + vo;
					if (xPos >= 0 && xPos < this.cellPeriodCanvas.width) {
						if (this.isHex && ((x === ex && y < cellSize) || (x === sx && y >= (this.cellPeriodHeight + cellBorderSize) * cellSize))) {
							// do nothing
						} else {
							data32[(y * rowWidth) + xPos] = gridCol;
						}
					}
				}
			}
		} else {
			// draw a border
			if (this.isHex) {
				offset = colHeight >> 1;
				inc = -0.5;
			}

			if (this.boundedGridType !== -1) {
				pixCol = boundedCol;
			} else {
				pixCol = gridCol;
			}

			ex = 0;
			if ((this.cellPeriodHeight & 1) !== 0) {
				ex += inc;
			}

			row = rowWidth * (colHeight - 1);
			for (x = 0; x < rowWidth - offset; x += 1) {
				data32[x + offset] = pixCol;
				data32[x + row] = pixCol;
			}

			for (y = 0; y < colHeight; y += 1) {
				data32[y * rowWidth + (offset | 0)] = pixCol;
				data32[y * rowWidth + rowWidth - 1 + (ex | 0)] = pixCol;
				offset += inc;
				ex += inc;
			}
		}

		// convert RGB array
		if (this.cellPeriodState6) {
			x = -1;
		} else {
			x = 0;
		}

		while (x < numCols) {
			pixCol = this.cellFrequencyRGB[x];
			if (this.littleEndian) {
				red = pixCol & 255;
				green = (pixCol >> 8) & 255;
				blue = (pixCol >> 16) & 255;
			} else {
				red = pixCol >> 24;
				green = (pixCol >> 16) & 255;
				blue = (pixCol >> 8) & 255;
			}
			this.cellFrequencyRGB[x] = (red << 16) | (green << 8) | blue;

			x += 1;
		}

		// update the image
		this.cellFrequencyContext.putImageData(data, 0, 0);

		// save the cell period size
		this.cellPeriodWidth = cellPeriodWidth;
		this.cellFrequencyNumCols = numCols;
	};

	// draw a right justified string
	/** @returns {number} */
	Life.prototype.drawRightString = function(/** @type {string} */ txt, /** @type {number} */ fieldWidth, /** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ offset) {
		var	/** @type {number} */ width = 0,
			/** @type {CanvasRenderingContext2D} */ ctx = this.context;

		// measure the text
		width = ctx.measureText(txt).width;

		// draw the text right aligned
		ctx.fillText(txt, x + fieldWidth - width + offset, y + offset);

		// return next field start
		return x + fieldWidth;
	};

	// create period table start and end row numbers
	Life.prototype.createPeriodTableRowNumbers = function(/** @type {MenuItem} */ label) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ rowHeight = 24 * displayScale,
			/** @type {number} */ startY = label.relY + label.height,
			/** @type {number} */ numRows = 0,
			/** @type {number} */ value = 0,
			/** @type {number} */ y = startY;

		// count number of table rows
		if (this.popSubPeriod) {
			for (i = this.popSubPeriod.length - 1; i > 0; i -= 1) {
				value = this.popSubPeriod[i];
				if (value > 0) {
					numRows += 1;
				}
			}

			// see if the table would be off the bottom of the display (add 1 for the column headings)
			y = startY + (rowHeight >> 1) + ((numRows + 1) * rowHeight);

			if (y > this.displayHeight - 2 * 80 * displayScale) {
				this.tableMaxRow = numRows - 1;
			} else {
				this.tableMaxRow = 0;
			}
		}

		// go to top
		this.tableStartRow = 0;
	};

	// create frequency table start and end row numbers
	Life.prototype.createFrequencyTableRowNumbers = function(/** @type {MenuItem} */ label) {
		var	/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ rowHeight = 24 * displayScale,
			/** @type {number} */ startY = label.relY + label.height,
			/** @type {number} */ numRows = 0,
			/** @type {number} */ y = startY;

		// get number of table rows
		if (this.uniqueCellCounts) {
			numRows = this.uniqueCellCounts.length;
			if (this.cellPeriodState6) {
				numRows += 1;
			}

			// see if the table would be off the bottom of the display (add 1 for the column headings)
			y = startY + (rowHeight >> 1) + ((numRows + 1) * rowHeight);

			if (y > this.displayHeight - 2 * 80 * displayScale) {
				this.tableMaxRow = numRows - 1;
			} else {
				this.tableMaxRow = 0;
			}
		}

		// go to top
		this.tableStartRow = 0;
	};

	// home on the cell table
	Life.prototype.cellTableHome = function() {
		if (this.tableMaxRow !== 0) {
			this.tableStartRow = 0;
		}
	};

	// end of the cell table
	Life.prototype.cellTableEnd = function() {
		if (this.tableMaxRow !== 0) {
			this.tableStartRow = this.tableMaxRow - this.tablePageSize;
		}
	};

	// page up on the cell table
	Life.prototype.cellTablePageUp = function() {
		if (this.tableStartRow > 0) {
			this.tableStartRow -= this.tablePageSize;
			if (this.tableStartRow < 0) {
				this.tableStartRow = 0;
			}
		}
	};

	// page up on the cell table
	Life.prototype.cellTablePageDown = function() {
		if (this.tableStartRow < this.tableMaxRow - this.tablePageSize) {
			this.tableStartRow += this.tablePageSize;
			if (this.tableStartRow > this.tableMaxRow - this.tablePageSize) {
				this.tableStartRow = this.tableMaxRow - this.tablePageSize;
			}
		}
	};

	// draw cell period table under and in the style of the supplied label
	Life.prototype.drawCellPeriodTable = function(/** @type {MenuItem} */ label) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ itemNum = 0,
			/** @type {number} */ maxItem = 0,
			/** @type {number} */ value = 0,
			/** @type {number} */ offset = 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ startY = label.y + label.height,
			/** @type {number} */ width = label.width,
			/** @type {number} */ alpha = label.bgAlpha,
			/** @type {string} */ bgCol = label.bgCol,
			/** @type {string} */ fgCol = label.fgCol,
			/** @type {number} */ y = startY,
			/** @type {number} */ rowHeight = 24 * displayScale,
			/** @type {number} */ boxSize = 16 * displayScale,
			/** @type {number} */ numCols = this.cellPeriodNumCols,
			/** @type {number} */ fieldWidth = (width >> 2) - 1,
			/** @type {number} */ leftX = (this.displayWidth - width) >> 1,
			/** @type {CanvasRenderingContext2D} */ ctx = this.context;

		// get number of subperiods
		if (this.popSubPeriod[this.popSubPeriod.length - 1] === 0) {
			numCols -= 1;
		}
		if (this.popSubPeriod[1] === 0) {
			numCols -= 1;
		}

		// draw background
		ctx.fillStyle = bgCol;
		ctx.globalAlpha = alpha;

		// calculate the height of the table including the column header row
		y = rowHeight;
		for (k = this.popSubPeriodHelp.length - 1; k >= 0; k -= 1) {
			i = this.view.engine.popSubPeriodHelp[k];
			value = this.popSubPeriod[i];
			if (value > 0 && y + startY < this.displayHeight - 80 * displayScale) {
				y += rowHeight;
			}
		}
		ctx.fillRect(leftX, startY, width, y);

		// draw shadow and then text
		ctx.font = ((20 * displayScale) | 0) + "px Arial";
		ctx.globalAlpha = 1;
		ctx.fillStyle = bgCol;
		y = startY + (rowHeight >> 1);

		for (j = 0; j < 2; j += 1) {
			// draw the table header
			x = leftX;
			x = this.drawRightString("Period", fieldWidth, x, y, offset);
			x = this.drawRightString("Count", fieldWidth, x, y, offset);
			x = this.drawRightString("% Total", fieldWidth, x, y, offset);
			x = this.drawRightString("% Rotor", fieldWidth, x, y, offset);
			y += rowHeight;

			// draw the table rows
			itemNum = 0;

			for (k = this.popSubPeriodHelp.length - 1; k >= 0; k -= 1) {
				i = this.view.engine.popSubPeriodHelp[k];
				value = this.popSubPeriod[i];
				if (value > 0 && (y - (rowHeight >> 1)) < this.displayHeight - 80 * displayScale) {
					if (itemNum >= (this.tableStartRow | 0)) {
						// draw row data
						x = leftX;
						x = this.drawRightString(String(i), fieldWidth, x, y, offset);
						x = this.drawRightString(String(value), fieldWidth, x, y, offset);
						x = this.drawRightString((Math.floor(10000 * value / this.popTotal) / 100).toFixed(2) + "%", fieldWidth, x, y, offset);

						if (i > 1) {
							this.drawRightString((Math.floor(10000 * value / this.popRotor) / 100).toFixed(2) + "%", fieldWidth, x, y, offset);
						}
						y += rowHeight;
					}
					itemNum += 1;
				}
			}

			// switch to drawing text
			y = startY + (rowHeight >> 1);
			offset = 0;
			ctx.fillStyle = fgCol;
		}

		// draw the legend
		maxItem = itemNum;
		itemNum = 0;
		offset = ((28 * displayScale) >> 1);
		for (k = this.popSubPeriodHelp.length - 1; k >= 0; k -= 1) {
			i = this.view.engine.popSubPeriodHelp[k];
			value = this.popSubPeriod[i];
			if (value > 0 && itemNum < maxItem) {
				if (itemNum >= (this.tableStartRow | 0)) {
					x = leftX;

					// draw box
					ctx.fillStyle = this.view.menuManager.bgCol;
					ctx.fillRect(leftX + (boxSize >> 1) + 2, y + offset + 2, boxSize, boxSize);
					ctx.fillStyle = "#" + ("000000" + this.cellPeriodRGB[i].toString(16)).slice(-6);
					ctx.fillRect(leftX + (boxSize >> 1), y + offset, boxSize, boxSize);
					y += rowHeight;
				}
				itemNum += 1;
			}
		}

		// set the page size
		if (this.tableStartRow === 0) {
			this.tablePageSize = maxItem - 1;
		}
	};

	// draw cell frequency table under and in the style of the supplied label
	Life.prototype.drawCellFrequencyTable = function(/** @type {MenuItem} */ label) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ itemNum = 0,
			/** @type {number} */ maxItem = 0,
			/** @type {number} */ value = 0,
			/** @type {number} */ offset = 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ startY = label.y + label.height,
			/** @type {number} */ width = label.width,
			/** @type {number} */ alpha = label.bgAlpha,
			/** @type {string} */ bgCol = label.bgCol,
			/** @type {string} */ fgCol = label.fgCol,
			/** @type {number} */ y = startY,
			/** @type {number} */ rowHeight = 24 * displayScale,
			/** @type {number} */ boxSize = 16 * displayScale,
			/** @type {number} */ numCols = this.cellFrequencyNumCols,
			/** @type {number} */ fieldWidth = (width >> 2) - 1,
			/** @type {number} */ leftX = (this.displayWidth - width) >> 1,
			/** @type {CanvasRenderingContext2D} */ ctx = this.context;

		// draw background
		ctx.fillStyle = bgCol;
		ctx.globalAlpha = alpha;

		// calculate the height of the table including the column header row
		y = rowHeight;
		for (i = numCols - 1; i >= 0; i -= 1) {
			if (y + startY < this.displayHeight - 80 * displayScale) {
				y += rowHeight;
			}
		}
		ctx.fillRect(leftX, startY, width, y);

		// draw shadow and then text
		ctx.font = ((20 * displayScale) | 0) + "px Arial";
		ctx.globalAlpha = 1;
		ctx.fillStyle = bgCol;
		y = startY + (rowHeight >> 1);

		for (j = 0; j < 2; j += 1) {
			// draw the table header
			x = leftX;
			x = this.drawRightString("Frequency", fieldWidth, x, y, offset);
			x = this.drawRightString("Count", fieldWidth, x, y, offset);
			x = this.drawRightString("% Total", fieldWidth, x, y, offset);
			y += rowHeight;

			// draw the table rows
			itemNum = 0;

			for (i = numCols - 1; i >= 0; i -= 1) {
				value = this.uniqueCellCounts[i];
				if ((y - (rowHeight >> 1)) < this.displayHeight - 80 * displayScale) {
					if (itemNum >= (this.tableStartRow | 0)) {
						// draw row data
						x = leftX;
						x = this.drawRightString(String(value), fieldWidth, x, y, offset);
						x = this.drawRightString(String(this.uniqueCellCountsCounts[i]), fieldWidth, x, y, offset);
						x = this.drawRightString(String((100 * this.uniqueCellCountsCounts[i] / this.cellCountsTotal).toFixed(2)) + "%", fieldWidth, x, y, offset);
						y += rowHeight;
					}
					itemNum += 1;
				}
			}

			// switch to drawing text
			y = startY + (rowHeight >> 1);
			offset = 0;
			ctx.fillStyle = fgCol;
		}

		// draw the legend
		maxItem = itemNum;
		itemNum = 0;
		offset = ((28 * displayScale) >> 1);
		for (i = numCols - 1; i >= 0; i -= 1) {
			value = this.cellFrequencyCounts[i];
			if (itemNum < maxItem) {
				if (itemNum >= (this.tableStartRow | 0)) {
					x = leftX;

					// draw box
					ctx.fillStyle = this.view.menuManager.bgCol;
					ctx.fillRect(leftX + (boxSize >> 1) + 2, y + offset + 2, boxSize, boxSize);
					ctx.fillStyle = "#" + ("000000" + this.cellFrequencyRGB[i].toString(16)).slice(-6);
					ctx.fillRect(leftX + (boxSize >> 1), y + offset, boxSize, boxSize);
					y += rowHeight;
				}
				itemNum += 1;
			}
		}

		// set the page size
		if (this.tableStartRow === 0) {
			this.tablePageSize = maxItem - 1;
		}
	};

	// draw cell period map
	Life.prototype.drawCellPeriodMap = function(/** @type {MenuItem} */ label) {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ p = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ legendWidth = 50 * displayScale,
			/** @type {number} */ rowSize = 15 * displayScale,
			/** @type {number} */ colSize = 16 * displayScale,
			/** @type {number} */ boxSize = (rowSize * 0.7) | 0,
			/** @type {number} */ width = this.displayWidth - (displayScale * (legendWidth + legendWidth + 20)),
			/** @type {number} */ height = this.displayHeight - (displayScale * 90),
			/** @type {number} */ numCols = this.cellPeriodNumCols,
			/** @type {number} */ cellSize = this.cellPeriodCellSize,
			/** @type {number} */ cellBorderSize = this.cellBorderSize,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ maxY = 0,
			/** @type {number} */ itemNum = 0,
			/** @type {number} */ legendCols = 1,
			/** @type {number} */ alpha = label.bgAlpha,
			/** @type {string} */ fgCol = label.fgCol,
			/** @type {string} */ bgCol = label.bgCol,
			/** @type {CanvasRenderingContext2D} */ ctx = this.context,
			/** @type {number} */ maxLabelWidth = 0,
			/** @type {number} */ legendBorder = displayScale * 80,
			/** @type {number} */ yFactor = this.getYFactor();

		// scale the image to fit
		x = width / ((this.cellPeriodWidth + cellBorderSize + cellBorderSize) * cellSize);
		y = height / ((this.cellPeriodHeight + cellBorderSize + cellBorderSize) * cellSize);

		if (x > y) {
			s = y;
		} else {
			s = x;
		}

		// make cells integer width if scale >= 1
		if (s >= 1) {
			s |= 0;
		}

		// recompute size based on scale factor
		x = this.cellPeriodCanvas.width * s;
		y = this.cellPeriodCanvas.height * s;

		// render the map centered on the display
		ctx.save();
		leftX = ((this.displayWidth - x) / 2);
		bottomY = ((this.displayHeight - y) / 2);
		ctx.translate(this.displayWidth >> 1, this.displayHeight >> 1);
		if (yFactor > 1 && y * yFactor > height - 160) {
			ctx.scale(s / yFactor, s);
		} else {
			ctx.scale(s, s * yFactor);
		}

		// use image smoothing for scales below 1 pixel per cell
		if (s < 1 || s * yFactor <= 1 || s / yFactor < 1) {
			ctx.imageSmoothingEnabled = true;
		} else {
			ctx.imageSmoothingEnabled = false;
		}
		ctx.drawImage(this.cellPeriodCanvas, -(this.cellPeriodCanvas.width >> 1), -(this.cellPeriodCanvas.height >> 1));
		ctx.imageSmoothingEnabled = false;
		ctx.restore();

		// draw the legend
		leftX = 55 * displayScale;
		bottomY = 180 * displayScale;
		ctx.font = ((11 * displayScale) | 0) + "px Arial";
		if (this.popSubPeriod[this.popSubPeriod.length - 1] === 0) {
			numCols -= 1;
		}
		if (this.popSubPeriod[1] === 0) {
			numCols -= 1;
		}
		if (this.cellPeriodState6) {
			numCols += 1;
		}

		// set the box width to the longest period value or the State 6 label
		y = 0;
		for (k = this.popSubPeriodHelp.length - 1; k >= 0; k -= 1) {
			x = this.popSubPeriodHelp[k];
			p = this.popSubPeriod[x];
			if (p > 0) {
				if (x > y) {
					y = x;
				}
			}
		}
		maxLabelWidth = ctx.measureText(String(y) + " ").width;
		if (this.cellPeriodState6) {
			if (this.isExtended) {
				y = ctx.measureText(LifeConstants.state3Label + " ").width;
			} else {
				y = ctx.measureText(LifeConstants.state6Label + " ").width;
			}
			if (y > maxLabelWidth) {
				maxLabelWidth = y;
			}
		}

		// check if the legend fits in one column
		y = 0;
		maxY = 0;
		for (k = this.popSubPeriodHelp.length - 1; k >= 0; k -= 1) {
			x = this.popSubPeriodHelp[k];
			p = this.popSubPeriod[x];
			if (p > 0) {
				if (y >= this.displayHeight - 2 * legendBorder) {
					if (y > maxY) {
						maxY = y;
					}
					y = 0;
					legendCols += 1;
				} else {
					y += rowSize;
				}
			}
		}

		if (legendCols > 1) {
			bottomY = legendBorder;
		} else {
			bottomY = (this.displayHeight - y) / 2;
		}

		// draw the legend box
		ctx.globalAlpha = alpha;
		ctx.fillStyle = bgCol;

		if (legendCols > 1) {
			ctx.fillRect(leftX - legendWidth - 2, bottomY - 2, boxSize + maxLabelWidth + 8 * displayScale, maxY);
		} else {
			ctx.fillRect(leftX - legendWidth - 2, bottomY - 2, boxSize + maxLabelWidth + 8 * displayScale, (numCols + 2) * rowSize + 3 * displayScale);
		}
		ctx.globalAlpha = 1;

		// draw each legend entry
		y = 0;
		itemNum = 0;

		// check for state 6 cells
		if (this.cellPeriodState6 && itemNum >= (this.tableStartRow | 0)) {
			// draw colour
			ctx.fillStyle = this.view.menuManager.bgCol;
			ctx.fillRect(leftX - legendWidth + 2, bottomY + y + 2, boxSize, boxSize);
			ctx.fillStyle = "#" + ("000000" + this.cellPeriodRGB[-1].toString(16)).slice(-6);
			ctx.fillRect(leftX - legendWidth, bottomY + y, boxSize, boxSize);

			// draw period
			if (this.isExtended) {
				ctx.fillStyle = bgCol;
				ctx.fillText(LifeConstants.state3Label, leftX - legendWidth + colSize + 2, bottomY + y + 2 + (7 * displayScale));
				ctx.fillStyle = fgCol;
				ctx.fillText(LifeConstants.state3Label, leftX - legendWidth + colSize, bottomY + y + (7 * displayScale));
			} else {
				ctx.fillStyle = bgCol;
				ctx.fillText(LifeConstants.state6Label, leftX - legendWidth + colSize + 2, bottomY + y + 2 + (7 * displayScale));
				ctx.fillStyle = fgCol;
				ctx.fillText(LifeConstants.state6Label, leftX - legendWidth + colSize, bottomY + y + (7 * displayScale));
			}

			y += rowSize;
			itemNum += 1;
		}

		for (k = this.popSubPeriodHelp.length - 1; k >= 0; k -= 1) {
			x = this.popSubPeriodHelp[k];
			p = this.popSubPeriod[x];
			if (p > 0 && y < this.displayHeight - 2 * legendBorder) {
				if (itemNum >= (this.tableStartRow | 0)) {
					// draw colour
					ctx.fillStyle = this.view.menuManager.bgCol;
					ctx.fillRect(leftX - legendWidth + 2, bottomY + y + 2 + (1 * displayScale), boxSize, boxSize);
					ctx.fillStyle = "#" + ("000000" + this.cellPeriodRGB[x].toString(16)).slice(-6);
					ctx.fillRect(leftX - legendWidth, bottomY + y + (1 * displayScale), boxSize, boxSize);

					// draw period
					ctx.fillStyle = bgCol;
					ctx.fillText(String(x), leftX - legendWidth + colSize + 2, bottomY + y + 2 + (7 * displayScale));
					ctx.fillStyle = fgCol;
					ctx.fillText(String(x), leftX - legendWidth + colSize, bottomY + y + (7 * displayScale));

					y += rowSize;
				}
				itemNum += 1;
			}
		}

		// set the page size
		if (this.tableStartRow === 0) {
			this.tablePageSize = itemNum - 1;
		}
	};

	// draw cell frequency map
	Life.prototype.drawCellFrequencyMap = function(/** @type {MenuItem} */ label) {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ displayScale = this.view.viewMenu.xScale,
			/** @type {number} */ legendWidth = 50 * displayScale,
			/** @type {number} */ rowSize = 15 * displayScale,
			/** @type {number} */ colSize = 16 * displayScale,
			/** @type {number} */ boxSize = (rowSize * 0.7) | 0,
			/** @type {number} */ width = this.displayWidth - (displayScale * (legendWidth + legendWidth + 20)),
			/** @type {number} */ height = this.displayHeight - (displayScale * 90),
			/** @type {number} */ cellSize = this.cellPeriodCellSize,
			/** @type {number} */ cellBorderSize = this.cellBorderSize,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ maxY = 0,
			/** @type {number} */ itemNum = 0,
			/** @type {number} */ legendCols = 1,
			/** @type {number} */ numCols = 0,
			/** @type {number} */ alpha = label.bgAlpha,
			/** @type {string} */ fgCol = label.fgCol,
			/** @type {string} */ bgCol = label.bgCol,
			/** @type {CanvasRenderingContext2D} */ ctx = this.context,
			/** @type {number} */ maxLabelWidth = 0,
			/** @type {number} */ legendBorder = displayScale * 80,
			/** @type {Uint32Array} */ uniqueCounts = this.uniqueCellCounts,
			/** @type {number} */ yFactor = this.getYFactor();

		// scale the image to fit
		x = width / ((this.cellPeriodWidth + cellBorderSize + cellBorderSize) * cellSize);
		y = height / ((this.cellPeriodHeight + cellBorderSize + cellBorderSize) * cellSize);

		if (x > y) {
			s = y;
		} else {
			s = x;
		}

		// make cells integer width if scale >= 1
		if (s >= 1) {
			s |= 0;
		}

		// recompute size based on scale factor
		x = this.cellFrequencyCanvas.width * s;
		y = this.cellFrequencyCanvas.height * s;

		// render the map centered on the display
		ctx.save();
		leftX = ((this.displayWidth - x) / 2);
		bottomY = ((this.displayHeight - y) / 2);
		ctx.translate(this.displayWidth >> 1, this.displayHeight >> 1);
		if (yFactor > 1 && y * yFactor > height - 160) {
			ctx.scale(s / yFactor, s);
		} else {
			ctx.scale(s, s * yFactor);
		}

		// use image smoothing for scales below 1 pixel per cell
		if (s < 1 || s * yFactor <= 1 || s / yFactor < 1) {
			ctx.imageSmoothingEnabled = true;
		} else {
			ctx.imageSmoothingEnabled = false;
		}
		ctx.drawImage(this.cellFrequencyCanvas, -(this.cellFrequencyCanvas.width >> 1), -(this.cellFrequencyCanvas.height >> 1));
		ctx.imageSmoothingEnabled = false;
		ctx.restore();

		// draw the legend
		leftX = 55 * displayScale;
		bottomY = 180 * displayScale;
		ctx.font = ((11 * displayScale) | 0) + "px Arial";

		// set the box width to the longest unique cell count value
		maxLabelWidth = ctx.measureText(String(uniqueCounts[uniqueCounts.length - 1]) + " ").width;
		if (this.cellPeriodState6) {
			if (this.isExtended) {
				y = ctx.measureText(LifeConstants.state3Label + " ").width;
			} else {
				y = ctx.measureText(LifeConstants.state6Label + " ").width;
			}
			if (y > maxLabelWidth) {
				maxLabelWidth = y;
			}
		}

		// check if the legend fits in one column
		numCols = uniqueCounts.length;
		if (this.cellPeriodState6) {
			numCols += 1;
		}

		y = 0;
		maxY = 0;
		for (x = numCols - 1; x >= 0; x -= 1) {
			if (y >= this.displayHeight - 2 * legendBorder) {
				if (y > maxY) {
					maxY = y;
				}
				y = 0;
				legendCols += 1;
			} else {
				y += rowSize;
			}
		}

		if (legendCols > 1) {
			bottomY = legendBorder;
		} else {
			bottomY = (this.displayHeight - y) / 2;
		}

		// draw the legend box
		ctx.globalAlpha = alpha;
		ctx.fillStyle = bgCol;

		if (legendCols > 1) {
			ctx.fillRect(leftX - legendWidth - 2, bottomY - 2, boxSize + maxLabelWidth + 8 * displayScale, maxY);
		} else {
			ctx.fillRect(leftX - legendWidth - 2, bottomY - 2, boxSize + maxLabelWidth + 8 * displayScale, numCols * rowSize + 3 * displayScale);
		}
		ctx.globalAlpha = 1;

		// draw each legend entry
		y = 0;
		itemNum = 0;

		// check for state 6 cells
		if (this.cellPeriodState6) {
			// draw colour
			ctx.fillStyle = this.view.menuManager.bgCol;
			ctx.fillRect(leftX - legendWidth + 2, bottomY + y + 2, boxSize, boxSize);
			ctx.fillStyle = "#" + ("000000" + this.cellPeriodRGB[-1].toString(16)).slice(-6);
			ctx.fillRect(leftX - legendWidth, bottomY + y, boxSize, boxSize);

			// draw period
			if (this.isExtended) {
				ctx.fillStyle = bgCol;
				ctx.fillText(LifeConstants.state3Label, leftX - legendWidth + colSize + 2, bottomY + y + 2 + (7 * displayScale));
				ctx.fillStyle = fgCol;
				ctx.fillText(LifeConstants.state3Label, leftX - legendWidth + colSize, bottomY + y + (7 * displayScale));
			} else {
				ctx.fillStyle = bgCol;
				ctx.fillText(LifeConstants.state6Label, leftX - legendWidth + colSize + 2, bottomY + y + 2 + (7 * displayScale));
				ctx.fillStyle = fgCol;
				ctx.fillText(LifeConstants.state6Label, leftX - legendWidth + colSize, bottomY + y + (7 * displayScale));
			}

			y += rowSize;
			itemNum += 1;
			numCols -= 1;
		}

		for (x = numCols - 1; x >= 0; x -= 1) {
			if (y < this.displayHeight - 2 * legendBorder) {
				if (itemNum >= (this.tableStartRow | 0)) {
					// draw colour
					ctx.fillStyle = this.view.menuManager.bgCol;
					ctx.fillRect(leftX - legendWidth + 2, bottomY + y + 2 + (1 * displayScale), boxSize, boxSize);
					ctx.fillStyle = "#" + ("000000" + this.cellFrequencyRGB[x].toString(16)).slice(-6);
					ctx.fillRect(leftX - legendWidth, bottomY + y + (1 * displayScale), boxSize, boxSize);

					// draw period
					ctx.fillStyle = bgCol;
					ctx.fillText(String(uniqueCounts[x]), leftX - legendWidth + colSize + 2, bottomY + y + 2 + (7 * displayScale));
					ctx.fillStyle = fgCol;
					ctx.fillText(String(uniqueCounts[x]), leftX - legendWidth + colSize, bottomY + y + (7 * displayScale));

					y += rowSize;
				}
				itemNum += 1;
			}
		}

		// set the page size
		if (this.tableStartRow === 0) {
			this.tablePageSize = itemNum - 1;
		}
	};

	// update occupancy for state 2 with tiles
	Life.prototype.updateOccupancyState2 = function(/** @type {number} */ gen, /** @type {Array<Uint16Array>} */ tileGrid) {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Array<Uint8Array>} */ countList = this.countList,
			/** @type {Uint8Array} */ countRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {Uint16Array} */ mapping = this.occMap,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX << 3,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows,

			/** @const {number} */ cellWasAlive = LifeConstants.cellWasAlive;

		// check start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileRow = tileGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// process each row
							h = bottomY;
							while (h < topY) {
								// get colour grid row
								colourRow = colourGrid[h];
								countRow = countList[h];

								// get correct starting colour index
								cr = leftX;

								// check if this is the first generation
								if (gen === 0) {
									// process each cell along the tile row
									cr = leftX;
									while (cr < leftX + xSize) {
										if (colourRow[cr] >= aliveStart) {
											countRow[cr] = cellWasAlive;
										}
										cr += 1;

										if (colourRow[cr] >= aliveStart) {
											countRow[cr] = cellWasAlive;
										}
										cr += 1;

										if (colourRow[cr] >= aliveStart) {
											countRow[cr] = cellWasAlive;
										}
										cr += 1;

										if (colourRow[cr] >= aliveStart) {
											countRow[cr] = cellWasAlive;
										}
										cr += 1;
									}
								} else {
									// process each cell along the tile row
									cr = leftX;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									cr += 1;

									countRow[cr] = mapping[(colourRow[cr] & 64) + countRow[cr]];
									//cr += 1;
								}

								// next row
								h += 1;
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// update cell occupancy for rotor and stator calculation (used when not computing strict volatility)
	Life.prototype.updateOccupancyAny = function(/** @type {BoundingBox} */ box, /** @type {number} */ gen) {
		var	/** @type {number} */ x = box.leftX,
			/** @type {number} */ y = box.bottomY,
			/** @type {number} */ right = box.rightX,
			/** @type {number} */ top = box.topY,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ count = 0,
			/** @type {number} */ countN = 0,
			/** @type {number} */ countE = 0,
			/** @type {number} */ countS = 0,
			/** @type {number} */ countW = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {Array<Uint8Array>} */ countList = this.countList,
			/** @type {Uint8Array} */ countRow = null,
			/** @type {Array<Uint8Array>} */ initList = this.initList,
			/** @type {Uint8Array} */ initRow = null,
			/** @type {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {boolean} */ twoState = (this.multiNumStates <= 2 && !this.isRuleTree),
			/** @type {boolean} */ isAlive = false,
			/** @const {number} */ cellWasAlive = LifeConstants.cellWasAlive,
			/** @const {number} */ cellWasDead = LifeConstants.cellWasDead,
			/** @const {number} */ cellHasChanged = LifeConstants.cellHasChanged;

		//var msgRow = "";

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		for (cy = y; cy <= top; cy += 1) {
			colourRow = colourGrid[cy];
			countRow = countList[cy];
			if (twoState) {
				if (gen === 0) {
					for (cx = x; cx <= right; cx += 1) {
						if (colourRow[cx] >= aliveStart) {
							countRow[cx] = cellWasAlive;
						}
					}
				} else {
					for (cx = x; cx <= right; cx += 1) {
						if (colourRow[cx] >= aliveStart) {
							// update count
							if (countRow[cx] === cellWasDead) {
								countRow[cx] = cellHasChanged;
							}
						} else {
							// update count
							if (countRow[cx] === cellWasAlive) {
								countRow[cx] = cellHasChanged;
							}
						}
					}
				}
			} else {
				if (this.isPCA) {
					//msgRow = "";

					// handle sub-cells
					if (gen === 0) {
						for (cx = x; cx <= right; cx += 1) {
							// get the 4 sub-cell states
							state = colourRow[cx];
							if (state > this.historyStates) {
								state -= this.historyStates;
							} else {
								state = 0;
							}

							// north sub-cell
							countN = 0;
							if ((state & 1) !== 0) {
								countN = cellWasAlive;
							}

							// east sub-cell
							countE = 0;
							if ((state & 2) !== 0) {
								countE = cellWasAlive;
							}

							// south sub-cell
							countS = 0;
							if ((state & 4) !== 0) {
								countS = cellWasAlive;
							}

							// west sub-cell
							countW = 0;
							if ((state & 8) !== 0) {
								countW = cellWasAlive;
							}

							// combine the counts
							countRow[cx] = countN | (countE << 2) | (countS << 4) | (countW << 6);

							//msgRow += countRow[cx] + " ";
						}

						//console.log("gen", gen, "y", cy, "data", msgRow);
					} else {
						for (cx = x; cx <= right; cx += 1) {
							// get the 4 sub-cell counts
							count = countRow[cx];

							// get the 4 sub-cell states
							state = colourRow[cx];
							if (state > this.historyStates) {
								state -= this.historyStates;
							} else {
								state = 0;
							}

							// north sub-cell
							countN = count & 3;
							if ((state & 1) !== 0) {
								// update count
								if (countN === cellWasDead) {
									countN = cellHasChanged;
								}
							} else {
								// update count
								if (countN === cellWasAlive) {
									countN = cellHasChanged;
								}
							}

							// east sub-cell
							countE = (count >> 2) & 3;
							if ((state & 2) !== 0) {
								// update count
								if (countE === cellWasDead) {
									countE = cellHasChanged;
								}
							} else {
								// update count
								if (countE === cellWasAlive) {
									countE = cellHasChanged;
								}
							}

							// south sub-cell
							countS = (count >> 4) & 3;
							if ((state & 4) !== 0) {
								// update count
								if (countS === cellWasDead) {
									countS = cellHasChanged;
								}
							} else {
								// update count
								if (countS === cellWasAlive) {
									countS = cellHasChanged;
								}
							}

							// west sub-cell
							countW = (count >> 6) & 3;
							if ((state & 8) !== 0) {
								// update count
								if (countW === cellWasDead) {
									countW = cellHasChanged;
								}
							} else {
								// update count
								if (countW === cellWasAlive) {
									countW = cellHasChanged;
								}
							}

							// combine the counts
							countRow[cx] = countN | (countE << 2) | (countS << 4) | (countW << 6);

							//msgRow += countRow[cx] + " ";
						}

						//console.log("gen", gen, "y", cy, "data", msgRow);
					}
				} else {
					if (this.isSuper) {
						initRow = initList[cy];
						for (cx = x; cx <= right; cx += 1) {
							if (gen === 0) {
								initRow[cx] = colourRow[cx] & 1;
								if (colourRow[cx] & 1) {
									countRow[cx] = cellWasAlive;
								}
							} else {
								if (colourRow[cx] & 1) {
									// update count
									if (countRow[cx] === cellWasDead) {
										countRow[cx] = cellHasChanged;
									} else {
										if ((colourRow[cx] & 1) !== initRow[cx]) {
											countRow[cx] = cellHasChanged;
										}
									}
								} else {
									// update count
									if (countRow[cx] === cellWasAlive) {
										countRow[cx] = cellHasChanged;
									}
								}
							}
						}
					} else {
						if (this.isExtended) {
							initRow = initList[cy];
							for (cx = x; cx <= right; cx += 1) {
								isAlive = colourRow[x] > 0;
								if (gen === 0) {
									if (isAlive) {
										initRow[cx] = 1;
										countRow[cx] = cellWasAlive;
									} else {
										initRow[cx] = 0;
									}
								} else {
									if (isAlive) {
										// update count
										if (countRow[cx] === cellWasDead) {
											countRow[cx] = cellHasChanged;
										} else {
											if (initRow[cx] === 0) {
												countRow[cx] = cellHasChanged;
											}
										}
									} else {
										// update count
										if (countRow[cx] === cellWasAlive) {
											countRow[cx] = cellHasChanged;
										}
									}
								}
							}
						} else {
							// multi-state
							initRow = initList[cy];
							for (cx = x; cx <= right; cx += 1) {
								if (gen === 0) {
									initRow[cx] = colourRow[cx];
									if (colourRow[cx] > this.historyStates) {
										countRow[cx] = cellWasAlive;
									}
								} else {
									if (colourRow[cx] > this.historyStates) {
										// update count
										if (countRow[cx] === cellWasDead) {
											countRow[cx] = cellHasChanged;
										} else {
											if (colourRow[cx] !== initRow[cx]) {
												countRow[cx] = cellHasChanged;
											}
										}
									} else {
										// update count
										if (countRow[cx] === cellWasAlive) {
											countRow[cx] = cellHasChanged;
										}
									}
								}
							}
						}
					}
				}
			}
		}
	};

	// update cell occupancy for rotor and stator calculation (used when not computing strict volatility)
	Life.prototype.updateOccupancy = function(/** @type {BoundingBox} */ box, /** @type {number} */ gen) {
		var	/** @type {boolean} */ twoState = ((this.multiNumStates === -1) || (this.isHROT && this.multiNumStates === 2)),
			/** @type {number} */ i = 0,
			/** @type {Uint16Array} */ mergedMap = this.occMergedTileMap.whole,
			/** @type {Uint16Array} */ colourMap = this.colourTileHistoryGrid.whole,
			/** @type {Uint16Array} */ occMap = this.occTileMap.whole;

		if (twoState) {
			// merge last tile grid with this one
			for (i = 0; i < mergedMap.length; i += 1) {
				mergedMap[i] = colourMap[i] | occMap[i];
			}

			// update the occupancy map from the merged tile grid
			this.updateOccupancyState2(gen, this.occMergedTileMap);

			// save current tile grid as previous
			occMap.set(colourMap.subarray(0));
		} else {
			this.updateOccupancyAny(box, gen);
		}
	};

	Life.prototype.updateCellCounts = function(/** @type {BoundingBox} */ extent, /** @type {Array<Uint8Array>} */ colourGrid, /** @type {Uint32Array} */ cellCounts) {
		var	/** @type {number} */ timing = performance.now();

		if (Controller.useWASM && Controller.wasmEnableUpdateCellCounts && this.view.wasmEnabled && !this.isExtended) {
			// swap grids every generation
			if (this.isSuper || this.isExtended || this.isRuleTree) {
				colourGrid = this.colourGrid;
				if ((this.counter & 1) !== 0) {
					colourGrid = this.nextColourGrid;
				}
			}

			if (this.isSuper || this.isRuleTree) {
				WASM.updateCellCountsSuperOrRuleTree(
					colourGrid.whole.byteOffset | 0,
					cellCounts.byteOffset | 0,
					extent.bottomY | 0,
					extent.leftX | 0,
					extent.topY | 0,
					extent.rightX | 0,
					colourGrid[0].length | 0
				);
			} else {
				WASM.updateCellCounts(
					colourGrid.whole.byteOffset | 0,
					cellCounts.byteOffset | 0,
					extent.bottomY | 0,
					extent.leftX | 0,
					extent.topY | 0,
					extent.rightX | 0,
					colourGrid[0].length | 0,
					this.aliveStart | 0
				);
			}
		} else {
			this.updateCellCountsJS(extent, colourGrid, cellCounts);
		}

		if (Controller.wasmTiming) {
			timing = performance.now() - timing;
			this.view.menuManager.updateTimingItem("updateCounts", timing, Controller.useWASM && Controller.wasmEnableUpdateCellCounts && this.view.wasmEnabled);
		}
	};

	// update cell counts
	Life.prototype.updateCellCountsJS = function(/** @type {BoundingBox} */ extent, /** @type {Array<Uint8Array>} */ colourGrid, /** @type {Uint32Array} */ cellCounts) {
		var	/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ ci = 0,
			/** @type {number} */ v = 0,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {Uint8Array} */ colourRow = null;

		// swap grids every generation
		if (this.isSuper || this.isExtended || this.isRuleTree) {
			colourGrid = this.colourGrid;
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		for (cy = extent.bottomY; cy <= extent.topY; cy += 1) {
			colourRow = colourGrid[cy];

			if (this.isSuper || this.isRuleTree) {
				for (cx = extent.leftX; cx <= extent.rightX; cx += 1) {
					if (colourRow[cx] & 1) {
						cellCounts[ci] += 1;
					}
					ci += 1;
				}
			} else {
				if (this.isExtended) {
					for (cx = extent.leftX; cx <= extent.rightX; cx += 1) {
						v = colourRow[cx];
						if (v) {
							switch (v) {
								case 3:
									this.population -= 1;
									break;

								case 6:
									this.births += 1;
									cellCounts[ci] += 1;
									break;

								case 7:
									this.population -= 1;
									this.deaths += 1;
									break;

								case 10:
									this.births += 1;
									cellCounts[ci] += 1;
									break;

								case 11:
									this.population -= 1;
									this.deaths += 1;
									break;

								case 12:
									this.births += 1;
									cellCounts[ci] += 1;
									break;

								case 13:
									this.population -= 1;
									this.deaths += 1;
									break;

								case 19:
									this.births += 1;
									cellCounts[ci] += 1;
									break;

								case 20:
									this.population -= 1;
									this.deaths += 1;
									break;

								default:
									cellCounts[ci] += 1;
									break;
							}
						}
						ci += 1;
					}
				} else {
					for (cx = extent.leftX; cx <= extent.rightX; cx += 1) {
						if (colourRow[cx] >= aliveStart) {
							cellCounts[ci] += 1;
						}
						ci += 1;
					}
				}
			}
		}
	};

	Life.prototype.updateOccupancyStrict = function(/** @type {BoundingBox} */ extent, /** @type {Array<Uint8Array>} */ colourGrid, /** @type {Uint16Array} */ frames, /** @type {number} */ p, /** @type {number} */ bitRowIn16Bits, /** @type {number} */ bitFrameIn16Bits, /** @type {number} */ bitStart) {
		var	/** @type {number} */ timing = performance.now();

		// swap grids every generation
		if (this.isSuper || this.isExtended || this.isRuleTree) {
			colourGrid = this.colourGrid;
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		if (Controller.useWASM && Controller.wasmEnableUpdateOccupancyStrict && this.view.wasmEnabled && !this.isExtended) {
			if (this.isSuper || this.isRuleTree) {
				WASM.updateOccupancyStrictSuperOrRuleLoader(
					colourGrid.whole.byteOffset | 0,
					frames.byteOffset | 0,
					extent.bottomY | 0,
					extent.leftX | 0,
					extent.topY | 0,
					extent.rightX | 0,
					p | 0,
					bitRowIn16Bits | 0,
					bitFrameIn16Bits | 0,
					bitStart | 0,
					colourGrid[0].length | 0
				);
			} else {
				WASM.updateOccupancyStrict(
					colourGrid.whole.byteOffset | 0,
					frames.byteOffset | 0,
					extent.bottomY | 0,
					extent.leftX | 0,
					extent.topY | 0,
					extent.rightX | 0,
					p | 0,
					bitRowIn16Bits | 0,
					bitFrameIn16Bits | 0,
					bitStart | 0,
					this.aliveStart | 0,
					colourGrid[0].length | 0
				);
			}
		} else {
			this.updateOccupancyStrictJS(extent, colourGrid, frames, p, bitRowIn16Bits, bitFrameIn16Bits, bitStart);
		}

		if (Controller.wasmTiming) {
			timing = performance.now() - timing;
			this.view.menuManager.updateTimingItem("updateOccupancy", timing, Controller.useWASM && Controller.wasmEnableUpdateOccupancyStrict && this.view.wasmEnabled);
		}
	};

	// update cell occupancy for rotor and stator calculation (used when computing strict volatility)
	Life.prototype.updateOccupancyStrictJS = function(/** @type {BoundingBox} */ extent, /** @type {Array<Uint8Array>} */ colourGrid, /** @type {Uint16Array} */ frames, /** @type {number} */ p, /** @type {number} */ bitRowIn16Bits, /** @type {number} */ bitFrameIn16Bits, /** @type {number} */ bitStart) {
		var	/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ f = 0,
			/** @type {number} */ l = 0,
			/** @type {number} */ frameBits = 0,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {Uint8Array} */ colourRow = null;

		// swap grids every generation
		if (this.isSuper || this.isExtended || this.isRuleTree) {
			colourGrid = this.colourGrid;
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// process each row of the pattern extent
		l = p * bitFrameIn16Bits;
		for (cy = extent.bottomY; cy <= extent.topY; cy += 1) {
			// get the pattern row
			colourRow = colourGrid[cy];

			// find the start of the row
			f = ((cy - extent.bottomY) * bitRowIn16Bits) + l;
			bit = bitStart;

			// check for Super or RuleTree rules
			if (this.isSuper || this.isRuleTree) {
				// process the row
				cx = extent.leftX;
				frameBits = 0;
				while (cx <= extent.rightX - 16) {
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					// not needed since last bit and reset below
					//bit >>= 1;
					cx += 1;

					// save frame bits
					if (frameBits !== 0) {
						frames[f] = frameBits;
						frameBits = 0;
					}

					// reset bit to start of word
					bit = bitStart;
					f += 1;
				}

				// process remaining cells in row
				while (cx <= extent.rightX) {
					if (colourRow[cx] & 1) {
						frameBits |= bit;
					}
					bit >>= 1;
					cx += 1;
				}
				if (frameBits !== 0) {
					frames[f] = frameBits;
				}
			} else {
				if (this.isExtended) {
					// process the row
					cx = extent.leftX;
					frameBits = 0;
					while (cx <= extent.rightX - 16) {
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						// not needed since last bit and reset below
						//bit >>= 1;
						cx += 1;

						// save frame bits
						if (frameBits !== 0) {
							frames[f] = frameBits;
							frameBits = 0;
						}

						// reset bit to start of word
						bit = bitStart;
						f += 1;
					}

					// process remaining cells in row
					while (cx <= extent.rightX) {
						if (colourRow[cx]) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
					}
					if (frameBits !== 0) {
						frames[f] = frameBits;
					}
				} else {
					// process the row
					cx = extent.leftX;
					frameBits = 0;
					while (cx <= extent.rightX - 16) {
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						// not needed since last bit and reset below
						//bit >>= 1;
						cx += 1;

						// save frame bits
						if (frameBits !== 0) {
							frames[f] = frameBits;
							frameBits = 0;
						}

						// reset bit to start of word
						bit = bitStart;
						f += 1;
					}

					// process remaining cells in row
					while (cx <= extent.rightX) {
						if (colourRow[cx] >= aliveStart) {
							frameBits |= bit;
						}
						bit >>= 1;
						cx += 1;
					}
					if (frameBits !== 0) {
						frames[f] = frameBits;
					}
				}
			}
		}
	};

	// compute cell factors
	Life.prototype.computeCellFactors = function(/** @type {Int32Array} */ cellPeriod, /** @type {number} */ period, /** @type {Uint16Array} */ frames, /** @type {Uint32Array} */ cellCounts, /** @type {number} */ boxWidth, /** @type {number} */ boxHeight, /** @type {number} */ bitFrameIn16Bits, /** @type {number} */ bitRowIn16Bits, /** @type {number} */ bitStart) {
		var	/** @type {number} */ f = 0,
			/** @type {number} */ bitRow = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ nextFrame = 0,
			/** @type {number} */ finalFrame = 0,
			/** @type {number} */ row = 0,
			/** @type {number} */ off1 = 0,
			/** @type {number} */ off2 = 0,
			/** @type {number} */ modValue = 0,

			///** @type {number} */ checks = 0,
			///** @type {number} */ modDiscards = 0,
			///** @type {number} */ matches = 0,
			///** @type {number} */ t1 = performance.now(),

			/** @type {number} */ target = 0;

		// coming in any cell that was ever occupied will have a cell period equal to the period
		// we check subperiods up to and including half the period
		// the remaining occupied cells will be oscillating at the full period

		// check each factor starting at 1
		for (f = 1; f <= (period / 2); f += 1) {
			if (period % f === 0) {
				target = period - f;
				finalFrame = target * bitFrameIn16Bits;
				nextFrame = f * bitFrameIn16Bits;
				modValue = period / f;

				//checks = 0;
				//modDiscards = 0;
				//matches = 0;

				//var t0 = performance.now();

				// check each row
				for (cy = 0; cy < boxHeight; cy += 1) {
					row = cy * boxWidth;
					bitRow = cy * bitRowIn16Bits;
					bit = bitStart;

					// check each cell along the row
					var /** @type {number} */ rowPlusCX = 0;

					for (cx = 0; cx < boxWidth; cx += 1) {
						rowPlusCX = row + cx;

						// check if the cell was ever occupied during the period
						//if (cellPeriod[row + cx] === period) {
						if (cellPeriod[rowPlusCX] === period) {

							//  cell count must be a multiple of period divided by the subperiod
							//if (cellCounts[row + cx] % modValue) {
							if (cellCounts[rowPlusCX] % modValue) {

								//modDiscards += 1;

							} else {

								//checks += 1;

								// check if the cell's evolution is identical in each subperiod run
								off1 = (cx >> 4) + bitRow;
								off2 = off1 + finalFrame;

								while (off1 < off2) {
									if ((frames[off1] ^ frames[off1 + nextFrame]) & bit) {
										// if a mismatch is found then exit the loop
										break;
									}
									off1 += bitFrameIn16Bits;
								}

								// if evolution is identical then update the subperiod for the cell
								if (off1 === off2) {
									// save the subperiod, this also prevents it from being checked again at higher subperiods
									//cellPeriod[row + cx] = f;
									cellPeriod[rowPlusCX] = f;

									//matches += 1;

								}
							}
						}

						// rotate the bit mask
						bit = ((bit >> 1) | (bit << 15)) & 65535;
					}
				}

				//t0 = performance.now() - t0;
				//var active = modDiscards + checks;
				//console.debug(f, modValue, "active", active, "discards", modDiscards, ((100 * modDiscards) / active).toFixed(1) + "%", "checks", checks, ((100 * checks) / active).toFixed(1) + "%", "matches", matches, (t0 / 1000).toFixed(1));
			}
		}

		//t1 = performance.now() - t1;
		//console.log(t1.toFixed(2) + "ms");
	};

	// add History state 6 cells to cell map
	Life.prototype.addHistoryState6ToCellMap = function(/** @type {BoundingBox} */ box, /** @type {Int32Array} */ cellPeriod, /** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {number} */ leftX = box.leftX,
			/** @type {number} */ bottomY = box.bottomY,
			/** @type {number} */ rightX = box.rightX,
			/** @type {number} */ topY = box.topY,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ row = 0,
			/** @type {Uint8Array} */ gridRow = null,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128;

		this.cellPeriodState6 = 0;
		for (y = bottomY; y <= topY; y += 1) {
			gridRow = grid[y];
			row = (y - bottomY) * (rightX - leftX + 1);
			for (x = leftX; x <= rightX; x += 1) {
				if (gridRow[x] === state6) {
					cellPeriod[row + x - leftX] = -1;
					this.cellPeriodState6 += 1;
				}
			}
		}
	};

	// add Super state 6 cells to cell map
	Life.prototype.addSuperState6ToCellMap = function(/** @type {BoundingBox} */ box, /** @type {Int32Array} */ cellPeriod, /** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {number} */ leftX = box.leftX,
			/** @type {number} */ bottomY = box.bottomY,
			/** @type {number} */ rightX = box.rightX,
			/** @type {number} */ topY = box.topY,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ row = 0,
			/** @type {Uint8Array} */ gridRow = null;

		this.cellPeriodState6 = 0;
		for (y = bottomY; y <= topY; y += 1) {
			gridRow = grid[y];
			row = (y - bottomY) * (rightX - leftX + 1);
			for (x = leftX; x <= rightX; x += 1) {
				if (gridRow[x] === 6) {
					cellPeriod[row + x - leftX] = -1;
					this.cellPeriodState6 += 1;
				}
			}
		}
	};

	// add Extended state 3 cells to cell map
	Life.prototype.addExtendedState3ToCellMap = function(/** @type {BoundingBox} */ box, /** @type {Int32Array} */ cellPeriod, /** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {number} */ leftX = box.leftX,
			/** @type {number} */ bottomY = box.bottomY,
			/** @type {number} */ rightX = box.rightX,
			/** @type {number} */ topY = box.topY,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ row = 0,
			/** @type {Uint8Array} */ gridRow = null;

		this.cellPeriodState6 = 0;
		for (y = bottomY; y <= topY; y += 1) {
			gridRow = grid[y];
			row = (y - bottomY) * (rightX - leftX + 1);
			for (x = leftX; x <= rightX; x += 1) {
				if (gridRow[x] === 3) {
					cellPeriod[row + x - leftX] = -1;
					this.cellPeriodState6 += 1;
				}
			}
		}
	};

	// compute bounding box for Identify
	/** @returns {BoundingBox} */
	Life.prototype.updateIdentifyBox = function() {
		var	/** @type {BoundingBox} */ box = (this.isHROT ? this.HROTBox : (this.isLifeHistory ? this.identifyHistoryBox : this.zoomBox)),
			/** @type {BoundingBox} */ state6Box = this.state6Box,
			/** @type {BoundingBox} */ identifyBox = this.identifyBox,
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0);

		// check for Super rules
		if (this.isSuper) {
			box = this.aliveBox;
		}

		// found bounded grids use the bounded grid extent
		if (this.boundedGridType !== -1) {
			identifyBox.leftX = Math.round((this.width - this.boundedGridWidth) / 2) + boxOffset;
			identifyBox.bottomY = Math.round((this.height - this.boundedGridHeight) / 2) + boxOffset;
			identifyBox.rightX = identifyBox.leftX + this.boundedGridWidth - 1;
			identifyBox.topY = identifyBox.bottomY + this.boundedGridHeight - 1;
		} else {
			// copy zoom box into Identify box
			identifyBox.set(box);

			// merge with state 6 box if required
			if (this.isSuper || this.isLifeHistory) {
				identifyBox.merge(state6Box);
			}
		}

		// return the bounding box
		return this.identifyBox;
	};

	// get identify maximum bounding box
	/** @returns {BoundingBox} */
	Life.prototype.getIdentifyMaxBox = function(/** @type {number} */ period) {
		var	/** @type {BoundingBox} */ result = new BoundingBox(this.width, this.height, 0, 0),
			/** @type {BoundingBox} */ extent = null,
			/** @type {number} */ i = 0,
			/** @type {number} */ p = this.fullOscLength - period,
			/** @type {number} */ lx = 0,
			/** @type {number} */ by = 0,
			/** @type {number} */ wd = 0,
			/** @type {number} */ ht = 0,
			/** @type {number} */ record = 0,
			/** @type {View} */ view = this.view;

		// check if records exist
		if (this.fullOscLength !== -1) {
			for (i = 0; i < period; i += 1) {
				// unpack width and height
				record = this.boxList[(p + i) << 1];
				wd = record >> 16;
				ht = record & 65535;

				// unpack bottom left
				record = this.boxList[((p + i) << 1) + 1];
				lx = record >> 16;
				by = record & 65535;

				// update bounding box
				if (lx < result.leftX) {
					result.leftX = lx;
				}

				if (lx + wd - 1 > result.rightX) {
					result.rightX = lx + wd - 1;
				}

				if (by < result.bottomY) {
					result.bottomY = by;
				}

				if (by + ht - 1 > result.topY) {
					result.topY = by + ht - 1;
				}
			}
		} else {
			// need to run for the period
			for (i = 0; i < period; i += 1) {
				view.computeNextGeneration();
				extent = this.updateIdentifyBox();

				if (extent.leftX < result.leftX) {
					result.leftX = extent.leftX;
				}

				if (extent.rightX > result.rightX) {
					result.rightX = extent.rightX;
				}

				if (extent.bottomY < result.bottomY) {
					result.bottomY = extent.bottomY;
				}

				if (extent.topY > result.topY) {
					result.topY = extent.topY;
				}
			}
		}

		return result;
	};


	// compute strict volatility and Mod
	Life.prototype.computeStrictVolatility = function(/** @type {number} */ period, /** @type {View} */ view, /** @type {boolean} */ isOscillator, /** @type {number} */ deltaX, /** @type {number} */ deltaY) {
		var	/** @type {number} */ p = 0,
			/** @type {number} */ bitRowIn16Bits = 0,
			/** @type {number} */ bitFrameIn16Bits = 0,
			/** @type {boolean} */ computeStrict = false,
			/** @type {Uint16Array} */ frames = null,
			/** @type {Uint32Array} */ cellCounts = null,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint32Array} */ popSubPeriod = new Uint32Array(period + 1),
			/** @type {Int32Array} */ cellPeriod = null,
			/** @type {number} */ boxWidth = 0,
			/** @type {number} */ boxHeight = 0,
			/** @type {number} */ popTotal = 0,
			/** @type {number} */ row = 0,
			/** @type {BoundingBox} */ extent = null,
			/** @type {number} */ frameTypeMSB = 0,
			/** @type {number} */ bitStart = 0,
			/** @type {number} */ hash0Lower = 0,
			/** @type {number} */ hash0Upper = 0,
			/** @type {number} */ width0 = 0,
			/** @type {number} */ height0 = 0,
			/** @type {number} */ firstHash0Lower = 0,
			/** @type {number} */ firstHash0Upper = 0,
			/** @type {number} */ hash1Lower = 0,
			/** @type {number} */ hash1Upper = 0,
			/** @type {number} */ width1 = 0,
			/** @type {number} */ height1 = 0,
			/** @type {number} */ firstHash1Lower = 0,
			/** @type {number} */ firstHash1Upper = 0,
			/** @type {number} */ hash2Lower = 0,
			/** @type {number} */ hash2Upper = 0,
			/** @type {number} */ width2 = 0,
			/** @type {number} */ height2 = 0,
			/** @type {number} */ firstHash2Lower = 0,
			/** @type {number} */ firstHash2Upper = 0,
			/** @type {number} */ checkWidth = 0,
			/** @type {number} */ checkHeight = 0,
			/** @type {number} */ checkGenDelta = 0,
			/** @type {number} */ checkHashLower = 0,
			/** @type {number} */ checkHashUpper = 0,
			/** @type {number} */ checkFirstLower = 0,
			/** @type {number} */ checkFirstUpper = 0,
			/** @type {number} */ nextHeat = 0,
			/** @type {Array<ModCheck>} */ modChecks = [],
			/** @type {number} */ modMatch = 0,
			/** @type {number} */ minPop = 0,
			/** @type {number} */ maxPop = 0,
			/** @type {number} */ minPopT = 0,
			/** @type {number} */ maxPopT = 0,
			/** @type {number} */ totalPop = 0,
			/** @type {number} */ minBoxWidth = 0,
			/** @type {number} */ minBoxHeight = 0,
			/** @type {number} */ minBoxSize = 0,
			/** @type {number} */ minBoxSizeT = 0,
			/** @type {number} */ maxBoxWidth = 0,
			/** @type {number} */ maxBoxHeight = 0,
			/** @type {number} */ maxBoxSize = 0,
			/** @type {number} */ maxBoxSizeT = 0,
			/** @type {number} */ totalBoxWidth = 0,
			/** @type {number} */ totalBoxHeight = 0,
			/** @type {BoundingBox} */ currentBox = null,
			/** @type {number} */ minX = 0,
			/** @type {number} */ maxX = 0,
			/** @type {number} */ minY = 0,
			/** @type {number} */ maxY = 0,
			/** @type {number} */ wd = 0,
			/** @type {number} */ ht = 0,

			// get available memory
			/** @type {number} */ memoryLimit = Controller.useWASM ? this.allocator.availableHeap() : LifeConstants.maxStrictMemory,

			// save the current heap pointer so we can restore to it at the end of the function
			/** @type {number} */ wasmPointer = this.allocator.getHeapPointer();

		this.identifyDetectionTime = (performance.now() - this.identifyStartTime) / 1000;

		//console.log("found period " + String(period) + " at T=" + String(this.counter) + " in " + this.identifyDetectionTime.toFixed(1) + " seconds");

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		if (isOscillator) {
			// compute bounding box extent over entire period
			extent = this.getIdentifyMaxBox(period);
		}

		// determine whether Strict Volatility can be calculated based on amount of RAM needed
		if (isOscillator && (this.multiNumStates <= 2 || this.isSuper || this.isExtended)) {
			// compute the maximum box width and height for the oscillator
			boxWidth = extent.rightX - extent.leftX + 1;
			boxHeight = extent.topY - extent.bottomY + 1;

			// compute the size of the frame row in 16 bit chunks
			bitRowIn16Bits = ((boxWidth - 1) >> 4) + 1;

			// compute the size of the frame in 16 bit chunks
			bitFrameIn16Bits = bitRowIn16Bits * boxHeight;

			// check if there is enough memory to allocate
			if (bitFrameIn16Bits * 2 * period <= memoryLimit) {
				// allocate memory for each generation in the period (allocation is one bit per cell)
				frames = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, period * bitFrameIn16Bits, "Life.bitFrames", Controller.useWASM));

				// allocate memory for per-cell period
				cellPeriod = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, boxWidth * boxHeight, "Life.cellPeriods", false));

				// allocate memory for per-cell alive count
				cellCounts = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, boxWidth * boxHeight, "Life.cellCounts", Controller.useWASM));

				// get the frame data width most significant bit number
				frameTypeMSB = (frames.BYTES_PER_ELEMENT * 8) - 1;
				bitStart = 1 << frameTypeMSB;

				// mark computing strict volatility
				computeStrict = true;
			}

			//console.log("memory", bitFrameIn16Bits * period, ((100 * bitFrameIn16Bits * period) / LifeConstants.maxStrictMemory).toFixed(1) + "%", "strict volatility", computeStrict);
		}


		// if not computing strict volatility then use other method for rotor and stator
		if (isOscillator && !computeStrict) {
			// clear the rotor/stator count map
			this.countList.whole.fill(LifeConstants.cellWasDead);

			// create the occpancy tile map
			this.occTileMap = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.occTileMap", false);
			this.occMergedTileMap = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.occMergedTileMap", false);
		}

		var t = performance.now();

		// reset heat
		this.minHeat = 16384 * 16384;
		this.maxHeat = 0;
		this.heatVal = 0;

		// save cell map for each generation in the period
		// include extra generation to check Oscillator Mod period/2
		minPop = this.population;
		maxPop = this.population;
		minPopT = 0;
		maxPopT = 0;

		minBoxWidth = this.width;
		maxBoxWidth = 0;
		minBoxHeight = this.height;
		maxBoxHeight = 0;

		minBoxSize = this.width * this.width;
		maxBoxSize = 0;

		minBoxSizeT = 0;
		maxBoxSizeT = 0;

		totalBoxWidth = 0;
		totalBoxHeight = 0;

		minX = this.width;
		maxX = 0;
		minY = this.height;
		maxY = 0;

		for (p = 0; p <= period + 1; p += 1) {
			// compute next generation and update bounding box
			view.computeNextGeneration();

			// update bounding box if not an oscillator (i.e. a moving spaceship)
			if (!isOscillator) {
				extent = this.updateIdentifyBox();
			}

			// check if life just stopped
			if (view.justDied) {
				// remember the generation that life stopped
				if (view.diedGeneration === -1) {
					view.diedGeneration = this.counter;
				}

				// exit loop
				return;
			}

			//if (p && ((p & 4095) === 0)) {
				//console.log(p, period, ((performance.now() - this.identifyStartTime) / 1000).toFixed(1) + " seconds");
			//}

			// check if grid buffer needs to grow just for spaceships (since oscillators don't move)
			// (normally this check happens at render time but we may have processed more generations than expected by that function)
			if (this.counter && this.population > 0 && !isOscillator) {
				view.middleBox.leftX = extent.leftX;
				view.middleBox.bottomY = extent.bottomY;
				view.middleBox.rightX = extent.rightX;
				view.middleBox.topY = extent.topY;
				view.checkGridSize(view, view.middleBox);
			}

			// add to the strict volatility frame if computing strict volatility
			if (isOscillator) {
				if (computeStrict) {
					if (p < period) {
						this.updateCellCounts(extent, colourGrid, cellCounts);
						this.updateOccupancyStrict(extent, colourGrid, frames, p, bitRowIn16Bits, bitFrameIn16Bits, bitStart);
					}
				} else {
					// use the original method of computing cell occupancy
					this.updateOccupancy(extent, p);
				}
			}

			//console.log(p, "gen", this.counter, extent, "width", (extent.rightX - extent.leftX + 1), "height", (extent.topY - extent.bottomY + 1));

			// save hash for first two generations
			if (p === 0) {
				this.getHash(extent);
				hash0Lower = this.hashLower;
				hash0Upper = this.hashUpper;
				width0 = (extent.rightX - extent.leftX + 1);
				height0 = (extent.topY - extent.bottomY + 1);
				this.getFirstHash(extent);
				firstHash0Lower = this.hashLower;
				firstHash0Upper = this.hashUpper;

				//console.log(p, "gen", this.counter, "hash0", hash0, "firstHash", firstHash0);

			} else {
				if (p === 1) {
					this.getHash(extent);
					hash1Lower = this.hashLower;
					hash1Upper = this.hashUpper;
					width1 = (extent.rightX - extent.leftX + 1);
					height1 = (extent.topY - extent.bottomY + 1);
					this.getFirstHash(extent);
					firstHash1Lower = this.hashLower;
					firstHash1Upper = this.hashUpper;

					//console.log(p, "gen", this.counter, "hash1", hash1, "firstHash", firstHash1);

				} else {
					if (p === 2 && this.isMargolus && hash0Lower === hash1Lower && hash0Upper === hash1Upper) {
						this.getHash(extent);
						hash2Lower = this.hashLower;
						hash2Upper = this.hashUpper
						width2 = (extent.rightX - extent.leftX + 1);
						height2 = (extent.topY - extent.bottomY + 1);
						this.getFirstHash(extent);
						firstHash2Lower = this.hashLower;
						firstHash2Upper = this.hashUpper;

						//console.log(p, "gen", this.counter, "hash2", hash2, "firstHash", firstHash2);

					}
				}
			}

			// ignore hex or triangular patterns
			if (!(this.isHex || this.isTriangular)) {
				// check if Mod has already been found
				if (this.modValue === -1) {
					// check if verifying
					while (this.modValue === -1 && modChecks.length > 0 && modChecks[0].checkGen === p) {
						// get the correct generation values
						if (this.isMargolus && hash0Lower === hash1Lower && hash0Upper === hash1Upper) {
							checkWidth = width2;
							checkHeight = height2;
							checkGenDelta = 2;
							checkHashLower = hash2Lower;
							checkHashUpper = hash2Upper;
							checkFirstLower = firstHash2Lower;
							checkFirstUpper = firstHash2Upper;
						} else {
							checkWidth = width1;
							checkHeight = height1;
							checkGenDelta = 1;
							checkHashLower = hash1Lower;
							checkHashUpper = hash1Upper;
							checkFirstLower = firstHash1Lower;
							checkFirstUpper = firstHash1Upper;
						}

						// check if the bounding box is the same size as the source
						if ((((extent.rightX - extent.leftX + 1) === checkWidth) && ((extent.topY - extent.bottomY) + 1) === checkHeight) || ((extent.rightX - extent.leftX + 1) === checkHeight) && ((extent.topY - extent.bottomY + 1) === checkWidth)) {
							// check the source generation against this one
							var t2 = performance.now();

							this.modType = this.checkModHashType(extent, checkHashLower, checkHashUpper, modChecks[0].modType, deltaX, deltaY, period / (p - checkGenDelta), checkFirstLower, checkFirstUpper);

							t2 = performance.now() - t2;
							//console.log("checkModHashType", t2.toFixed(2) + "ms");

							if (this.modType !== -1) {

								//console.log(p, "gen", this.counter, "type", this.modType, LifeConstants.modTypeName[this.modType], "verified");

								this.modValue = p - checkGenDelta;
							} else {

								//console.log(p, "gen", this.counter, "verify failed");

								this.modValue = -1;
							}
						}

						// remove the mod check
						modChecks.shift();
					}

					// check if Mod found after verification
					if (this.modValue === -1) {
						// no Mod found so check if at a subperiod
						if (p > 0 && (period % p === 0)) {
							// ensure bounding box is the same size as the source
							if ((((extent.rightX - extent.leftX + 1) === width0) && ((extent.topY - extent.bottomY) + 1) === height0) || ((extent.rightX - extent.leftX + 1) === height0) && ((extent.topY - extent.bottomY + 1) === width0)) {

								var t1 = performance.now();

								modMatch = this.checkModHash(extent, hash0Lower, hash0Upper, deltaX, deltaY, firstHash0Lower, firstHash0Upper);

								t1 = performance.now() - t1;
								//console.log("checkModHash", t1.toFixed(2) + "ms");

								if (modMatch !== 0 && !(this.isMargolus && hash0Lower === hash1Lower && hash0Upper === hash1Upper && p < 2)) {
									// potential Mod found so create verification record
									if (this.isMargolus && hash0Lower === hash1Lower && hash0Upper === hash1Upper) {
										modChecks[modChecks.length] = new ModCheck(p + 2, modMatch);
									} else {
										modChecks[modChecks.length] = new ModCheck(p + 1, modMatch);
									}

									//console.log(p, "gen", this.counter, "type", modMatch, "check at", p + (this.isMargolus && hash0 === hash1 ? 2 : 1), "delta", deltaX, deltaY);
									//for (cx = 0; cx <= LifeConstants.modRot90FlipY; cx += 1) { if ((modMatch & (1 << cx)) !== 0) { console.log(LifeConstants.modTypeName[cx]); } }

								}
							}
						}
					}
				}
			}

			// update heat, min/max population and bounding box statistics
			if (p < period) {
				nextHeat = this.births + this.deaths;
				if (nextHeat < this.minHeat) {
					this.minHeat = nextHeat;
				}

				if (nextHeat > this.maxHeat) {
					this.maxHeat = nextHeat;
				}

				this.heatVal += nextHeat;

				if (this.population < minPop) {
					minPop = this.population;
					minPopT = p;
				}

				if (this.population > maxPop) {
					maxPop = this.population;
					maxPopT = p;
				}

				totalPop += this.population;

				if (!isOscillator) {
					wd = extent.rightX - extent.leftX + 1;
					ht = extent.topY - extent.bottomY + 1;
					if (wd > totalBoxWidth) {
						totalBoxWidth = wd;
					}
					if (ht > totalBoxHeight) {
						totalBoxHeight = ht;
					}

					if (wd * ht < minBoxSize) {
						minBoxSize = wd * ht;
						minBoxWidth = wd;
						minBoxHeight = ht;
						minBoxSizeT = p;
					}

					if (wd * ht > maxBoxSize) {
						maxBoxSize = wd * ht;
						maxBoxWidth = wd;
						maxBoxHeight = ht;
						maxBoxSizeT = p;
					}
				} else {
					currentBox = this.updateIdentifyBox();
					wd = currentBox.rightX - currentBox.leftX + 1;
					ht = currentBox.topY - currentBox.bottomY + 1;

					if (currentBox.leftX < minX) {
						minX = currentBox.leftX;
					}

					if (currentBox.rightX > maxX) {
						maxX = currentBox.rightX;
					}

					if (currentBox.bottomY < minY) {
						minY = currentBox.bottomY;
					}

					if (currentBox.topY > maxY) {
						maxY = currentBox.topY;
					}

					if (wd * ht < minBoxSize) {
						minBoxSize = wd * ht;
						minBoxWidth = wd;
						minBoxHeight = ht;
						minBoxSizeT = p;
					}

					if (wd * ht > maxBoxSize) {
						maxBoxSize = wd * ht;
						maxBoxWidth = wd;
						maxBoxHeight = ht;
						maxBoxSizeT = p;
					}
				}
			}
		}

		t = performance.now() - t;
		//console.log("computed cell map for each generation in " + (t / 1000).toFixed(1) + " seconds");
		t = performance.now();

		// compute strict volatility
		if (computeStrict) {
			// set any cell that was alive to maximum period
			var t = performance.now();

			for (cy = 0; cy < boxHeight; cy += 1) {
				// get the next row offsets
				row = cy * boxWidth;
				for (cx = 0; cx < boxWidth; cx += 1) {
					// skip empty blocks of cells
					if (cellCounts[row + cx]) {
						cellPeriod[row + cx] = period;
						popTotal += 1;
					}
				}
			}

			t = performance.now() - t;
			//console.log("initialize periods", t.toFixed(2) + "ms");
			t = performance.now();

			// calculate the factors of the period (subperiods)
			this.computeCellFactors(cellPeriod, period, frames, cellCounts, boxWidth, boxHeight, bitFrameIn16Bits, bitRowIn16Bits, bitStart);

			t = performance.now() - t;
			//console.log("calculated cell factors in " + (t / 1000).toFixed(1) + " seconds");
			t = performance.now();

			// count up the subperiod populations
			for (cy = 0; cy < boxHeight; cy += 1) {
				row = cy * boxWidth;
				for (cx = 0; cx < boxWidth; cx += 1) {
					p = cellPeriod[row + cx];
					if (p) {
						popSubPeriod[p] += 1;
					}
				}
			}
			t = performance.now() - t;
			//console.log("subperiod count", t.toFixed(2) + "ms");

			// for [R]History and [R]Super add state 6 cells to map and for [R]Extended add state 3 cells
			if (isOscillator) {
				this.cellPeriodState6 = 0;

				if (this.isLifeHistory) {
					this.addHistoryState6ToCellMap(extent, cellPeriod, this.overlayGrid);
				}

				if (this.isSuper) {
					this.addSuperState6ToCellMap(extent, cellPeriod, colourGrid);
				}

				if (this.isExtended) {
					this.addExtendedState3ToCellMap(extent, cellPeriod, colourGrid);
				}
			}

			// display integer strict volatility without decimal places
			p = popSubPeriod[period] / popTotal;
			this.strictVol = this.toPlaces(p, 2);
			this.popSubPeriod = popSubPeriod;
			this.popTotal = popTotal;
			this.cellPeriod = cellPeriod;
			this.cellPeriodWidth = boxWidth;
			this.origCellPeriodWidth = boxWidth;
			this.cellPeriodHeight = boxHeight;
			this.cellFrequencyCounts = cellCounts;

			// create the cell period map
			t = performance.now();
			this.createCellPeriodMap(view);
			t = performance.now() - t;
			//console.log("created period map in", (t / 1000).toFixed(1) + " seconds");

			t = performance.now();
			this.createCellFrequencyMap(cellCounts, period);
			t = performance.now() - t;
			//console.log("created frequency map in", (t / 1000).toFixed(1) + " seconds");
		} else {
			this.popSubPeriod = null;
			this.popSubPeriodHelp = [];
		}

		// save statistics
		this.identifyMinPop = minPop;
		this.identifyMinPopT = minPopT;
		this.identifyMaxPop = maxPop;
		this.identifyMaxPopT = maxPopT;
		this.identifyAveragePop = totalPop / period;

		this.identifyMinBoxWidth = minBoxWidth;
		this.identifyMinBoxHeight = minBoxHeight;
		this.identifyMinBoxSizeT = minBoxSizeT;

		this.identifyMaxBoxWidth = maxBoxWidth;
		this.identifyMaxBoxHeight = maxBoxHeight;
		this.identifyMaxBoxSizeT = maxBoxSizeT;


		if (isOscillator) {
			totalBoxWidth = maxX - minX + 1;
			totalBoxHeight = maxY - minY + 1;
		}

		this.identifyMinX = minX;
		this.identifyMinY = minY;
		this.identifyFullWidth = totalBoxWidth;
		this.identifyFullHeight = totalBoxHeight;

		// save elapsed time
		this.identifyElapsedTime = ((performance.now() - this.identifyStartTime) / 1000);

		//console.log("identification complete in " + this.identifyElapsedTime.toFixed(1) + " seconds");

		// restore heap pointer (freeing up memory allocated)
		this.allocator.setHeapPointer(wasmPointer);
	};

	// return identify results
	Life.prototype.identifyResults = function(/** @type {View} */ view, /** @type {string} */ message, /** @type {number} */ period, /** @type {number} */ deltaX, /** @type {number} */ deltaY, /** @type {number} */ boxWidth, /** @type {number} */ boxHeight) {
		// simple version of speed
		var	/** @type {string} */ simpleSpeed = "",

			// generation
			/** @type {string} */ genMessage = String(this.counter - period),

			// type
			/** @type {string} */ type = "",

			// direction
			/** @type {string} */ direction = "",

			// slope
			/** @type {string} */ slope = "",
			/** @type {number} */ divisor = 0,
			/** @type {number} */ dDeltaX = 0,
			/** @type {number} */ dDeltaY = 0,
			/** @type {number} */ dPeriod = 0,
			/** @type {number} */ maxDelta = 0,
			/** @type {number} */ minDelta = 0,
			/** @type {number} */ deltaXOrig = 0,
			/** @type {number} */ deltaYOrig = 0,

			// max and min population
			/** @type {number} */ min = 16384 * 16384 + 1,
			/** @type {number} */ max = 0,
			/** @type {number} */ avg = 0,
			/** @type {number} */ total = 0,
			/** @type {number} */ minT = 0,
			/** @type {number} */ maxT = 0,
			/** @type {string} */ popResult = "",

			// bounding box
			/** @type {number} */ minX = 16384,
			/** @type {number} */ maxX = 0,
			/** @type {number} */ minY = 16384,
			/** @type {number} */ maxY = 0,
			/** @type {string} */ boxResult = "",
			/** @type {number} */ minBoxSize = 16384 * 16884,
			/** @type {number} */ maxBoxSize = 0,
			/** @type {number} */ minBoxWidth = 0,
			/** @type {number} */ minBoxHeight = 0,
			/** @type {number} */ maxBoxWidth = 0,
			/** @type {number} */ maxBoxHeight = 0,

			// record range
			/** @type {number} */ last = 0,

			// heat
			/** @type {number} */ avgHeat = 0,
			/** @type {string} */ heat = "",

			// volatility
			/** @type {string} */ volatility = "",
			/** @type {string} */ strict = "",

			// rotor and stator
			/** @type {Array<Uint8Array>} */ countList = this.countList,
			/** @type {Uint8Array} */ countRow = null,
			/** @type {number} */ count = 0,
			/** @type {number} */ rotor = 0,
			/** @type {number} */ stator = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {string} */ activeResult = "",

			// mod value
			/** @type {string} */ modResult = "",

			// temperature
			/** @type {string} */ tempResult = "",

			// density (of still life)
			/** @type {string} */ densityResult = "";

		// only use one generation for still life
		if (period > 0) {
			genMessage += " and " + this.counter;
		}

		// check for movement
		deltaXOrig = deltaX;
		deltaYOrig = deltaY;
		deltaX = Math.abs(deltaX);
		deltaY = Math.abs(deltaY);

		// if a spaceship then compute simpified speed
		if (deltaX > 0 || deltaY > 0) {
			type = "Spaceship";
			if (deltaX === 0 || deltaY === 0) {
				slope = "0";
			} else {
				if (deltaX > deltaY) {
					maxDelta = deltaX;
					minDelta = deltaY;
				} else {
					maxDelta = deltaY;
					minDelta = deltaX;
				}
				divisor = this.gcd(maxDelta, minDelta);
				slope = String(maxDelta / divisor);
				if (minDelta / divisor !== 1) {
					slope += "/" + String(minDelta / divisor);
				}
			}
			if ((deltaX === deltaY) || (deltaX === 0) || (deltaY === 0)) {
				if ((deltaX === deltaY) && (deltaX !== 0)) {
					direction = "Diagonal";
				} else {
					direction = "Orthogonal";
				}
				if (deltaX === 0) {
					deltaX = deltaY;
				}
				if (deltaX > 1) {
					if (period === 1) {
						simpleSpeed = deltaX + "c";
					} else {
						divisor = this.gcd(deltaX, period);
						dDeltaX = deltaX / divisor;
						dPeriod = period / divisor;
						if (divisor !== 1) {
							if (dDeltaX === 1) {
								simpleSpeed = "c";
								if (dPeriod > 1) {
									simpleSpeed += "/" + dPeriod;
								 }
								 simpleSpeed += " | " + deltaX + "c/" + period;
							} else {
								simpleSpeed = dDeltaX + "c";
								if (dPeriod > 1) {
									simpleSpeed += "/" + dPeriod;
								 }
								 simpleSpeed += " | " + deltaX + "c/" + period;
							}
						} else {
							simpleSpeed = deltaX + "c/" + period;
						}
					}
				} else {
					// delta = 1
					simpleSpeed = "c";
					if (period > 1) {
						simpleSpeed += "/" + period;
					}
				}
			} else {
				divisor = this.gcd(deltaX, this.gcd(deltaY, period));
				dDeltaX = deltaX / divisor;
				dDeltaY = deltaY / divisor;
				dPeriod = period / divisor;
				if (divisor !== 1) {
					if (dDeltaX > dDeltaY) {
						simpleSpeed = "(" + dDeltaX + "," + dDeltaY + ")c";
					} else {
						simpleSpeed = "(" + dDeltaY + "," + dDeltaX + ")c";
					}
					if (dPeriod > 1) {
						simpleSpeed += "/" + dPeriod;
					}
					if (deltaX > deltaY) {
						simpleSpeed += " | (" + deltaX + "," + deltaY + ")c";
					} else {
						simpleSpeed += " | (" + deltaY + "," + deltaX + ")c";
					}
					if (period > 1) {
						simpleSpeed += "/" + period;
					}
				} else {
					if (deltaX > deltaY) {
						simpleSpeed = "(" + deltaX + "," + deltaY + ")c";
					} else {
						simpleSpeed = "(" + deltaY + "," + deltaX + ")c";
					}
					if (period > 1) {
						simpleSpeed += "/" + period;
					}
				}
				direction = this.getDisplacementName(deltaX, deltaY);
			}

			// compute Mod for spaceship
			this.computeStrictVolatility(period, view, false, deltaXOrig, deltaYOrig);
		} else {
			if (period === 1 || (period === 2 && this.isMargolus && this.hashListLower[0] === this.hashListLower[1] && this.hashListUpper[0] === this.hashListUpper[1])) {
				type = "Still Life";
			} else {
				// oscillator
				type = "Oscillator";

				// compute strict volatility and Mod for oscillator
				this.computeStrictVolatility(period, view, true, 0, 0);
			}
		}

		// check if Life died during volatility and mod calculation
		if (this.population === 0) {
			if (this.isPCA || this.isMargolus) {
				return ["Life ended at generation " + (this.counterMargolus + view.genOffset), "Empty"];
			} else {
				return ["Life ended at generation " + (this.counter + view.genOffset), "Empty"];
			}
		}

		// set the last record
		last = period;

		// compute the min and max population
		minT = 0;
		maxT = 0;
		this.identifyPopWithTMessage = "";

		if (type === "Still Life") {
			popResult = String(this.population);
		} else {
			min = this.identifyMinPop;
			minT = this.identifyMinPopT;

			max = this.identifyMaxPop;
			maxT = this.identifyMaxPopT;

			avg = this.identifyAveragePop;

			if (min === max) {
				// output the latest
				popResult = String(max);
			} else {
				// otherwise output min, max and average
				popResult = String(min) + " | " + String(max) + " | " + this.toPlaces(avg, 1);

				// save extended message with generations for Help->Identify
				this.identifyPopWithTMessage = String(min) + " @ T" + minT + " | " + String(max) + " @ T" + maxT;
			}
		}

		// compute the bounding box
		minT = 0;
		maxT = 0;
		this.identifyBoxWithTMessage = "";

		if (type !== "Still Life") {
			minBoxWidth = this.identifyMinBoxWidth;
			minBoxHeight = this.identifyMinBoxHeight;
			minT = this.identifyMinBoxSizeT;

			maxBoxWidth = this.identifyMaxBoxWidth;
			maxBoxHeight = this.identifyMaxBoxHeight;
			maxT = this.identifyMaxBoxSizeT;

			boxWidth = this.identifyFullWidth;
			boxHeight = this.identifyFullHeight;

			minBoxSize = minBoxWidth * minBoxHeight;
			maxBoxSize = maxBoxWidth * maxBoxHeight;

			minX = this.identifyMinX;
			maxX = minX + boxWidth;
			minY = this.identifyMinY;
			maxY = minY + boxHeight;
		}

		boxResult = String(boxWidth + " \u00D7 " + boxHeight + " = " + (boxWidth * boxHeight));
		if (!(minBoxWidth === maxBoxWidth && minBoxHeight === maxBoxHeight)) {
			this.identifyBoxWithTMessage = String(minBoxWidth + " \u00D7 " + minBoxHeight + " = " + minBoxSize + " @ T" + minT + " | " + maxBoxWidth + " \u00D7 " + maxBoxHeight + " = " + maxBoxSize + " @ T" + maxT);
		}

		// compute the heat
		if (period > 0) {
			// if min and max are the same then just output one
			if (this.minHeat === this.maxHeat) {
				avgHeat = this.minHeat;
				heat = String(avgHeat.toFixed(1));
			} else {
				// output min, max and average
				avgHeat = this.heatVal / last;
				heat = this.toPlaces(this.minHeat, 1) + " | " + this.toPlaces(this.maxHeat, 1) + " | " + this.toPlaces(avgHeat, 1);
			}
		}

		// compute volatility
		if (type === "Oscillator") {
			// if strict volatility calculated then get rotor and stator from there
			if (this.popSubPeriod) {
				rotor = this.popTotal - this.popSubPeriod[1];
				stator = this.popSubPeriod[1];
			} else {
				// otherwise get it from the count list
				for (y = minY; y <= maxY; y += 1) {
					countRow = countList[y];
					if (this.isPCA) {
						// handle PCA sub-cells
						for (x = minX; x <= maxX; x += 1) {
							count = countRow[x];

							// north sub-cell
							if ((count & 3) === LifeConstants.cellHasChanged) {
								rotor += 1;
							} else {
								if ((count & 3) === LifeConstants.cellWasAlive) {
									stator += 1;
								}
							}

							// east sub-cell
							count >>= 2;
							if ((count & 3) === LifeConstants.cellHasChanged) {
								rotor += 1;
							} else {
								if ((count & 3) === LifeConstants.cellWasAlive) {
									stator += 1;
								}
							}

							// south sub-cell
							count >>= 2;
							if ((count & 3) === LifeConstants.cellHasChanged) {
								rotor += 1;
							} else {
								if ((count & 3) === LifeConstants.cellWasAlive) {
									stator += 1;
								}
							}

							// west sub-cell
							count >>= 2;
							if ((count & 3) === LifeConstants.cellHasChanged) {
								rotor += 1;
							} else {
								if ((count & 3) === LifeConstants.cellWasAlive) {
									stator += 1;
								}
							}
						}
					} else {
						for (x = minX; x <= maxX; x += 1) {
							if (countRow[x] === LifeConstants.cellHasChanged) {
								rotor += 1;
							} else {
								if (countRow[x] === LifeConstants.cellWasAlive) {
									stator += 1;
								}
							}
						}
					}
				}
			}

			volatility = this.toPlaces(rotor / (rotor + stator), 2);
			strict = this.strictVol;
			this.popRotor = rotor;
		}

		// check if all cells were in rotor
		if (rotor === boxWidth * boxHeight) {
			this.identifyAllCells = true;
		} else {
			this.identifyAllCells = false;
		}

		// temperature
		if (type === "Oscillator") {
			if (rotor === 0) {
				tempResult = this.toPlaces(avgHeat / (rotor + stator), 2) + " | " + this.toPlaces(0, 2);
			} else {
				tempResult = this.toPlaces(avgHeat / (rotor + stator), 2) + " | " + this.toPlaces(avgHeat / rotor, 2);
			}
		}

		// active cells
		activeResult = String(rotor + " | " + stator + " | " + (rotor + stator));

		// mod value
		if (!(this.isHex || this.isTriangular)) {
			if (this.modValue <= 0) {
				this.modValue = period;
				modResult = String(this.modValue);
			} else {
				// check the mod is a divisor of the period
				if (period % this.modValue === 0 && period !== this.modValue) {
					modResult = String(this.modValue) + " (" + LifeConstants.modTypeName[this.modType] + ")";
				} else {
					modResult = String(this.modValue);
				}
			}
		}

		// calculate still life density
		if (type === "Still Life") {
			total = this.population / (boxWidth * boxHeight);
			if (this.isPCA) {
				total /= 4;
			}

			if (total === 1) {
				densityResult = String(total);
			} else {
				densityResult = total.toFixed(3);
			}
		}

		// return the result
		return [message, type, direction, simpleSpeed, boxResult, genMessage, popResult, slope, period, heat, volatility, strict, modResult, activeResult, tempResult, densityResult];
	};

	// add new identify record to short list
	Life.prototype.addNegIdentifyRecord = function(/** @type {number} */ hashLower, /** @type {number} */ hashUpper, /** @type {number} */ boxSize, /** @type {number} */ leftX, /** @type {number} */ bottomY) {
		// create the new record
		this.negHashListLower[this.oscLength] = hashLower;
		this.negHashListUpper[this.oscLength] = hashUpper;
		this.negGenList[this.oscLength] = this.counter;
		this.negPopList[this.oscLength] = this.population;
		this.negBoxList[this.oscLength << 1] = boxSize;
		this.negBoxList[(this.oscLength << 1) + 1] = (leftX << 16) | bottomY;
	};

	// add new identify record to full list
	Life.prototype.addFullIdentifyRecord = function(/** @type {number} */ hashLower, /** @type {number} */ hashUpper, /** @type {number} */ boxSize, /** @type {number} */ leftX, /** @type {number} */ bottomY) {
		// create the new record
		this.hashListLower[this.fullOscLength] = hashLower;
		this.hashListUpper[this.fullOscLength] = hashUpper;
		this.boxList[this.fullOscLength << 1] = boxSize;
		this.boxList[(this.fullOscLength << 1) + 1] = (leftX << 16) | bottomY;
	};

	// return true if pattern is empty, stable, oscillating or a spaceship
	/** @returns {Array} */
	Life.prototype.oscillating = function(/** @type {View} */ view) {
		// get bounding box
		var	/** @type {BoundingBox} */ box = this.updateIdentifyBox(),
			/** @type {number} */ leftX = box.leftX,
			/** @type {number} */ bottomY = box.bottomY,
			/** @type {number} */ rightX = box.rightX,
			/** @type {number} */ topY = box.topY,
			/** @type {number} */ boxWidth = rightX - leftX + 1,
			/** @type {number} */ boxHeight = topY - bottomY + 1,

			// whether to save results
			/** @type {boolean} */ saveResults = false,

			// merge size into one value
			/** @type {number} */ boxSize = (boxWidth << 16) | boxHeight,

			// merge location into one value
			/** @type {number} */ boxLocation = (leftX << 16) | bottomY,

			// hash value of current pattern
			/** @type {number} */ hashLower = 0,
			/** @type {number} */ hashUpper = 0,
			/** @type {number} */ currentValueLower = 0,
			/** @type {number} */ currentValueUpper = 0,

			// period
			/** @type {number} */ period = 0,

			// flag to quit loop
			/** @type {boolean} */ quitLoop = false,
			/** @type {boolean} */ quit = false,

			// hash entry index
			/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ maxI = this.oscLength,
			/** @type {number} */ nextI = 0,

			// movement vector
			/** @type {number} */ deltaX = 0,
			/** @type {number} */ deltaY = 0,
			/** @type {number} */ deltaX2 = 0,
			/** @type {number} */ deltaY2 = 0,

			// message
			/** @type {string} */ message = "",

			// result
			/** @type {Array} */ result = [];

		// check for deferred results
		if (this.identifyDeferredCounter !== -1) {
			this.identifyDeferredCounter -= 1;

			if (this.identifyDeferredCounter === -1) {
				this.identifyMessage = this.identifyDeferredResults[0];
				this.identifyPeriod = this.identifyDeferredResults[1];
				this.identifyDeltaX = this.identifyDeferredResults[2];
				this.identifyDeltaY = this.identifyDeferredResults[3];
				this.identifyBoxWidth = this.identifyDeferredResults[4];
				this.identifyBoxHeight = this.identifyDeferredResults[5];
				result = this.identifyResults(view, this.identifyDeferredResults[0], this.identifyDeferredResults[1], this.identifyDeferredResults[2], this.identifyDeferredResults[3], this.identifyDeferredResults[4], this.identifyDeferredResults[5]);
			}

			return result;
		}

		// check for cells killed at grid boundary
		if (this.cellsCleared !== 0) {
			result = ["Cells Killed", "Empty"];
			boxWidth = 0;
			boxHeight = 0;
			quit = true;
		}

		// check buffer
		if (this.oscLength < LifeConstants.negOscillatorGens) {
			// check population
			if (this.population === 0) {
				result = ["Empty pattern", "Empty"];
				boxWidth = 0;
				boxHeight = 0;
				quit = true;
			} else {
				// get the hash of the current pattern
				this.getHash(box);
				hashLower = this.hashLower;
				hashUpper = this.hashUpper;

				// search hash list for match
				quitLoop = false;

				// get the start item
				i = 0;

				// search the hash list
				while (i < maxI && !quitLoop) {
					currentValueLower = this.negHashListLower[i];
					currentValueUpper = this.negHashListUpper[i];
					if (hashLower > currentValueLower) {
						// hash value greater so try next
						i += 1;
					} else {
						if (hashLower < currentValueLower) {
							// hash value less so truncate list here
							this.oscLength = i;

							quitLoop = true;
						} else {
							if (hashLower === currentValueLower && hashUpper === currentValueUpper) {
								j = i << 1;

								// hash matched so check population and bounding box
								if ((this.population === this.negPopList[i]) && boxSize === this.negBoxList[j]) {
									saveResults = true;
									period = this.counter - this.negGenList[i];

									if (this.negBoxList[j + 1] === boxLocation) {
										// check for odd period Margolus
										if ((period & 1) === 1 && this.isMargolus) {
											saveResults = false;
										} else {
											// pattern hasn't moved
											if (period === 1 || (period === 2 && this.isMargolus && this.negHashListLower[0] === this.negHashListLower[1] && this.negHashListUpper[0] === this.negHashListUpper[1])) {
												message = "Still Life";
											} else {
												message = "Oscillator period " + period;
											}
										}
									} else {
										// pattern is moving so attempt to verify spaceship
										saveResults = false;

										if (i < this.oscLength - 1) {
											// check if the next record is also a hash match
											nextI = i + 1;

											if (hashLower === this.negHashListLower[nextI] && hashUpper === this.negHashListUpper[nextI]) {
												// check if it has the same population and bounding box
												if ((this.population === this.negPopList[nextI]) && boxSize === this.negBoxList[nextI << 1]) {
													// check if the periods are the same
													if (this.negGenList[nextI] - this.negGenList[i] === this.counter - this.negGenList[nextI]) {
														// finally check the deltas
														deltaX = (this.negBoxList[(nextI << 1) + 1] >> 16) - (this.negBoxList[(i << 1) + 1] >> 16);
														deltaY = (this.negBoxList[(nextI << 1) + 1] & 65535) - (this.negBoxList[(i << 1) + 1] & 65535);

														deltaX2 = leftX - (this.negBoxList[(nextI << 1) + 1] >> 16);
														deltaY2 = bottomY - (this.negBoxList[(nextI << 1) + 1] & 65535);

														if (deltaX === deltaX2 && deltaY === deltaY2) {
															period = this.counter - this.negGenList[nextI];
															message = this.spaceshipSpeed(period, deltaX, deltaY);
															saveResults = true;
														}
													}
												}
											}
										}
									}

									// check if defering for spaceship verification
									if (saveResults) {
										// result found
										quitLoop = true;
										quit = true;

										// create the results
										this.identifyDeferredResults = [message, period, deltaX, deltaY, boxWidth, boxHeight];
										this.identifyDeferredCounter = Math.round(LifeConstants.identifyNotificationDuration * Controller.refreshRate / 60);
										view.menuManager.notification.notify(message, 15, 40, 15, true);
										view.menuManager.notification.notify("Calculating...", 15, 40, 15, false);
									} else {
										// try next
										i += 1;
									}
								} else {
									// false positive so try next
									i += 1;
								}
							} else {
								// try next
								i += 1;
							}
						}
					}
				}

				// add to the lists
				if (!quit) {
					// create the new records
					this.addNegIdentifyRecord(hashLower, hashUpper, boxSize, leftX, bottomY);

					// check if the full buffer is full
					if (this.fullOscLength >= 0) {
						// add the record
						this.addFullIdentifyRecord(hashLower, hashUpper, boxSize, leftX, bottomY);

						// see if the buffer is now full
						this.fullOscLength += 1;
						if (this.fullOscLength === LifeConstants.maxOscillatorGens) {
							this.fullOscLength = -1;
							this.identifyLastSnapshotTime = performance.now();
							view.saveIdentifySnapshot();
						}
					}

					// check for short buffer full
					this.oscLength += 1;
					if (this.oscLength === LifeConstants.negOscillatorGens) {
						this.oscLength = 0;
						result = [LifeConstants.bufferFullMessage];
					}
				}
			}
		}

		// save checkpoint to clipboard periodically
		if (this.fullOscLength === -1 && ((performance.now() - this.identifyLastSnapshotTime) / 1000) > LifeConstants.identifySnapshotInterval) {
			this.identifyLastSnapshotTime = performance.now();
			view.saveIdentifySnapshot();
		}

		// return results
		return result;
	};

	// draw triangle cells in selection
	Life.prototype.drawTriangleCellsInSelection = function(/** @type {number} */ leftX, /** @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY, /** @type {number} */ xOff, /** @type {number} */ yOff, /** @type {Uint8Array} */ cells) {
		var	/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ m = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @const {number} */ w2 = this.width / 2,
			/** @const {number} */ h2 = this.height / 2,
			/** @type {number} */ state = 0,
			/** @type {Float32Array} */ coords = this.coords,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @type {number} */ yOff1 = this.height / 2 - this.yOff - this.originY + 0.5,
			/** @type {number} */ xOff1 = this.width / 2 - this.xOff - this.originX + 0.5,
			/** @type {number} */ zoom = this.zoom * this.originZ,
			/** @type {number} */ displayY = 0,
			/** @type {number} */ displayX = 0,
			/** @type {number} */ xOffset = 0,
			/** @type {number} */ swap = 0,
			/** @type {number} */ oddEven = 0;

		// order coordinates
		if (leftX > rightX) {
			swap = rightX;
			rightX = leftX;
			leftX = swap;
		}
		if (bottomY > topY) {
			swap = topY;
			topY = bottomY;
			bottomY = swap;
		}
		leftX += xOff;
		rightX += xOff;
		bottomY += yOff;
		topY += yOff;

		// check if buffers have been allocated
		if (colours.length !== LifeConstants.coordBufferSize) {
			this.coords = /** @type {!Float32Array} */ (this.allocator.allocate(Type.Float32, 12 * LifeConstants.coordBufferSize, "Life.coords", false));
			this.cellColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.coordBufferSize, "Life.cellColours", false));
			coords = this.coords;
			colours = this.cellColours;
		}

		// create triangles
		this.context.lineWidth = LifeConstants.triangleLineWidth;
		this.context.lineCap = "butt";
		this.context.lineJoin = "bevel";
		j = 0;
		k = 0;
		for (y = bottomY; y <= topY; y += 1) {
			// clip y to window
			displayY = (((y - h2) + yOff1) * zoom) + halfDisplayHeight;
			if (displayY >= -zoom && displayY < this.displayHeight + zoom) {
				cy = (y - h2);
				xOffset = xOff1 - w2;
				for (x = leftX; x <= rightX; x += 1) {
					if (cells[m] > 0) {
						displayX = ((x + xOffset) * zoom) + halfDisplayWidth;
						if (displayX >= -zoom && displayX < this.displayWidth + zoom * 2) {
							// encode coordinate index into the colour state so it can be sorted later
							colours[j] = (state << LifeConstants.coordBufferBits) + k;
							cx = x - w2 + 0.5;
							oddEven = ((x + y) & 1);
							coords[k] = cx - 1;
							coords[k + 1] = cy + oddEven;
							coords[k + 2] = cx + 1;
							coords[k + 3] = cy + oddEven;
							coords[k + 4] = cx;
							coords[k + 5] = cy + 1 - oddEven;
							k += 6;
							j += 1;

							// check if buffer is full
							if (j === LifeConstants.coordBufferSize) {
								// draw buffer
								this.numCells = j;
								this.drawTriangleCells(true, false, true);

								// clear buffer
								j = 0;
								k = 0;
							}
						}
					}
					// next cell
					m += 1;
				}
			} else {
				// next row
				m += (rightX - leftX + 1);
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawTriangleCells(true, false, true);

			// clear buffer
			j = 0;
			k = 0;
		}
	};

	// draw triangle selection
	Life.prototype.drawTriangleSelection = function(/** @type {number} */ leftX, /** @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY, /** @type {number} */ xOff, /** @type {number} */ yOff) {
		var	/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @const {number} */ w2 = this.width / 2,
			/** @const {number} */ h2 = this.height / 2,
			/** @type {number} */ state = 0,
			/** @type {Float32Array} */ coords = this.coords,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @type {number} */ yOff1 = this.height / 2 - this.yOff - this.originY + 0.5,
			/** @type {number} */ xOff1 = this.width / 2 - this.xOff - this.originX + 0.5,
			/** @type {number} */ zoom = this.zoom * this.originZ,
			/** @type {number} */ displayY = 0,
			/** @type {number} */ displayX = 0,
			/** @type {number} */ xOffset = 0,
			/** @type {number} */ swap = 0,
			/** @type {number} */ oddEven = 0;

		// order coordinates
		if (leftX > rightX) {
			swap = rightX;
			rightX = leftX;
			leftX = swap;
		}
		if (bottomY > topY) {
			swap = topY;
			topY = bottomY;
			bottomY = swap;
		}
		leftX += xOff;
		rightX += xOff;
		bottomY += yOff;
		topY += yOff;

		// check if buffers have been allocated
		if (colours.length !== LifeConstants.coordBufferSize) {
			this.coords = /** @type {!Float32Array} */ (this.allocator.allocate(Type.Float32, 12 * LifeConstants.coordBufferSize, "Life.coords", false));
			this.cellColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.coordBufferSize, "Life.cellColours", false));
			coords = this.coords;
			colours = this.cellColours;
		}

		// create triangles
		this.context.lineWidth = LifeConstants.triangleLineWidth;
		this.context.lineCap = "butt";
		this.context.lineJoin = "bevel";
		j = 0;
		k = 0;
		for (y = bottomY; y <= topY; y += 1) {
			// clip y to window
			displayY = (((y - h2) + yOff1) * zoom) + halfDisplayHeight;
			if (displayY >= -zoom && displayY < this.displayHeight + zoom) {
				cy = (y - h2);
				xOffset = xOff1 - w2;
				for (x = leftX; x <= rightX; x += 1) {
					displayX = ((x + xOffset) * zoom) + halfDisplayWidth;
					if (displayX >= -zoom && displayX < this.displayWidth + zoom * 2) {
						// encode coordinate index into the colour state so it can be sorted later
						colours[j] = (state << LifeConstants.coordBufferBits) + k;
						cx = x - w2 + 0.5;
						oddEven = ((x + y) & 1);
						coords[k] = cx - 1;
						coords[k + 1] = cy + oddEven;
						coords[k + 2] = cx + 1;
						coords[k + 3] = cy + oddEven;
						coords[k + 4] = cx;
						coords[k + 5] = cy + 1 - oddEven;
						k += 6;
						j += 1;

						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw buffer
							this.numCells = j;
							this.drawTriangleCells(true, false, true);

							// clear buffer
							j = 0;
							k = 0;
						}
					}
				}
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawTriangleCells(true, false, true);

			// clear buffer
			j = 0;
			k = 0;
		}
	};

	// draw triangles
	Life.prototype.drawTriangles = function() {
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {Uint8Array} */ overlayRow = null,
			/** @type {BoundingBox} */ zoomBox = this.historyBox,
			/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @const {number} */ w2 = this.width / 2,
			/** @const {number} */ h2 = this.height / 2,
			/** @type {number} */ state = 0,
			/** @type {number} */ overState = 0,
			/** @type {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @type {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @type {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128,
			/** @type {number} */ aliveStart = this.aliveStart,
			/** @type {Float32Array} */ coords = this.coords,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @type {number} */ leftX = zoomBox.leftX,
			/** @type {number} */ rightX = zoomBox.rightX,
			/** @type {number} */ bottomY = zoomBox.bottomY,
			/** @type {number} */ topY = zoomBox.topY,
			/** @type {number} */ yOff = this.height / 2 - this.yOff - this.originY + 0.5,
			/** @type {number} */ xOff = this.width / 2 - this.xOff - this.originX + 0.5,
			/** @type {number} */ zoom = this.zoom * this.originZ,
			/** @type {number} */ displayY = 0,
			/** @type {number} */ displayX = 0,
			/** @type {number} */ xOffset = 0,
			/** @type {number} */ oddEven = 0,
			/** @type {boolean} */ drawFilledCellBorders = !this.displayGrid && !this.cellBorders,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @type {number} */ maxGridSize = this.maxGridSize;

		// switch buffers if required
		if ((this.isSuper || this.isExtended || this.isRuleTree) && ((this.counter & 1) !== 0)) {
			colourGrid = this.nextColourGrid;
		}

		// check if buffers have been allocated
		if (colours.length !== LifeConstants.coordBufferSize) {
			this.coords = /** @type {!Float32Array} */ (this.allocator.allocate(Type.Float32, 12 * LifeConstants.coordBufferSize, "Life.coords", false));
			this.cellColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.coordBufferSize, "Life.cellColours", false));
			coords = this.coords;
			colours = this.cellColours;
		}

		// use bounded grid if defined
		if (this.boundedGridType !== -1) {
			if (this.boundedGridWidth !== 0) {
				// set width to included bounded grid cells
				leftX = Math.round((this.width - this.boundedGridWidth) / 2) - 1;
				rightX = leftX + this.boundedGridWidth - 1 + 2;
			} else {
				// infinite width so set to grid width
				leftX = 0;
				rightX = this.width - 1;
			}

			if (this.boundedGridHeight !== 0) {
				// set height to included bounded grid cells
				bottomY = Math.round((this.height - this.boundedGridHeight) / 2) - 1;
				topY = bottomY + this.boundedGridHeight - 1 + 2;
			} else {
				// infinite height to set to grid height
				bottomY = 0;
				topY = this.height - 1;
			}
		}

		// create triangles from live cells
		this.context.lineWidth = LifeConstants.triangleLineWidth;
		this.context.lineCap = "butt";
		this.context.lineJoin = "bevel";

		j = 0;
		k = 0;

		// draw each row
		for (y = bottomY; y <= topY; y += 1) {
			// clip y to window
			displayY = (((y - h2) + yOff) * zoom) + halfDisplayHeight;
			if (displayY >= -zoom && displayY < this.displayHeight + zoom) {
				colourRow = colourGrid[y];
				if (overlayGrid) {
					overlayRow = overlayGrid[y];
				}
				cy = (y - h2);
				xOffset = xOff - w2;

				// draw each cell in the row
				for (x = leftX - 1; x <= rightX + 1; x += 1) {
					displayX = ((x + xOffset) * zoom) + halfDisplayWidth;
					if (displayX >= -zoom && displayX <= this.displayWidth + zoom * 2) {
						state = colourRow[x];
						if (overlayRow) {
							overState = overlayRow[x];
							if (overState === state4 || overState === state6) {
								if (state >= aliveStart) {
									state = state3;
								} else {
									state = overState;
								}
							} else {
								if (overState === state3 || overState === state5) {
									if (state < aliveStart) {
										state = state4;
									} else {
										state = overState;
									}
								}
							}
						}

						if (state > 0) {
							// encode coordinate index into the colour state so it can be sorted later
							colours[j] = (state << LifeConstants.coordBufferBits) + k;
							cx = x - w2 + 0.5;
							oddEven = ((x + y) & 1);
							coords[k] = cx - 1;
							coords[k + 1] = cy + oddEven;
							coords[k + 2] = cx + 1;
							coords[k + 3] = cy + oddEven;
							coords[k + 4] = cx;
							coords[k + 5] = cy + 1 - oddEven;
							k += 6;
							j += 1;
						}

						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw buffer
							this.numCells = j;
							this.drawTriangleCells(true, drawFilledCellBorders, false);

							// draw cell borders if enabled and grid lines disabled
							if (this.cellBorders && !this.displayGrid) {
								this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
								this.drawTriangleCells(false, false, false);
							}

							// clear buffer
							j = 0;
							k = 0;
						}
					}
				}
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawTriangleCells(true, drawFilledCellBorders, false);

			// draw cell borders if enabled and grid lines disabled
			if (this.cellBorders && !this.displayGrid) {
				this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
				this.drawTriangleCells(false, false, false);
			}

			// clear buffer
			j = 0;
			k = 0;
		}

		// draw off grid cells
		j = 0;
		k = 0;

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}
		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// create cell coordinates for window
		bottomY = ((-halfDisplayHeight / zoom) - yOff + h2) | 0;
		topY = ((halfDisplayHeight / zoom) - yOff + h2) | 0;

		// draw each row
		for (y = bottomY; y <= topY; y += 1) {
			// clip y to window
			displayY = (((y - h2) + yOff) * zoom) + halfDisplayHeight;
			if (displayY >= -zoom && displayY < this.displayHeight + zoom) {
				cy = (y - h2);
				xOffset = xOff - w2;
				leftX = ((-halfDisplayWidth / zoom) - xOffset - zoom) | 0;
				rightX = ((halfDisplayWidth / zoom) - xOffset + zoom) | 0;

				// draw each cell on the row
				for (x = leftX - 1; x <= rightX + 1; x += 1) {
					displayX = ((x + xOffset) * zoom) + halfDisplayWidth;
					if (displayX >= -zoom && displayX <= this.displayWidth + zoom * 2) {
						// check if the cell is on the grid
						if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
							// encode coordinate index into the colour state so it can be sorted later
							colours[j] = (256 << LifeConstants.coordBufferBits) + k;
							cx = x - w2 + 0.5;
							oddEven = ((x + y) & 1);
							coords[k] = cx - 1;
							coords[k + 1] = cy + oddEven;
							coords[k + 2] = cx + 1;
							coords[k + 3] = cy + oddEven;
							coords[k + 4] = cx;
							coords[k + 5] = cy + 1 - oddEven;
							k += 6;
							j += 1;
						}

						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw buffer
							this.numCells = j;
							this.drawTriangleCells(true, drawFilledCellBorders, false);

							// draw cell borders if enabled and grid lines disabled
							if (this.cellBorders && !this.displayGrid) {
								this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
								this.drawTriangleCells(false, false, false);
							}

							// clear buffer
							j = 0;
							k = 0;
						}
					}
				}
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawTriangleCells(true, drawFilledCellBorders, false);

			// draw cell borders if enabled and grid lines disabled
			if (this.cellBorders && !this.displayGrid) {
				this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
				this.drawTriangleCells(false, false, false);
			}

			// clear buffer
			j = 0;
			k = 0;
		}

		// draw grid if enabled
		if (this.displayGrid) {
			// set grid line colour
			this.context.strokeStyle = "rgb(" + (this.gridLineRaw >> 16) + "," + ((this.gridLineRaw >> 8) & 255) + "," + (this.gridLineRaw & 255) + ")";
			this.context.lineWidth = 1;

			// create cell coordinates for window
			bottomY = ((-halfDisplayHeight / zoom) - yOff + h2) | 0;
			topY = ((halfDisplayHeight / zoom) - yOff + h2) | 0;

			j = 0;

			// draw each row
			for (y = bottomY; y <= topY; y += 1) {
				// clip y to window
				displayY = (((y - h2) + yOff) * zoom) + halfDisplayHeight;
				if (displayY >= -zoom && displayY < this.displayHeight + zoom) {
					cy = (y - h2);
					xOffset = xOff - w2;
					leftX = ((-halfDisplayWidth / zoom) - xOffset - zoom) | 0;
					rightX = ((halfDisplayWidth / zoom) - xOffset + zoom) | 0;

					// draw each cell on the row
					for (x = leftX - 1; x <= rightX + 1; x += 1) {
						displayX = ((x + xOffset) * zoom) + halfDisplayWidth;
						if (displayX >= -zoom && displayX < this.displayWidth + zoom * 2) {
							cx = x - w2 + 0.5;
							oddEven = ((x + y) & 1);
							coords[k] = cx - 1;
							coords[k + 1] = cy + oddEven;
							coords[k + 2] = cx + 1;
							coords[k + 3] = cy + oddEven;
							coords[k + 4] = cx;
							coords[k + 5] = cy + 1 - oddEven;
							k += 6;
							j += 1;
						}
						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw and clear buffer
							this.numCells = j;
							this.drawTriangleCells(false, false, false);
							j = 0;
							k = 0;
						}
					}
				}
			}

			// draw any remaining cells
			this.numCells = j;
			if (j > 0) {
				this.drawTriangleCells(false, false, false);
			}
		}
	};

	// draw triangle cells
	Life.prototype.drawTriangleCells = function(/** @type {boolean} */ filled, /** @type {boolean} */ borderWhenFilled, /** @type {boolean} */ drawingSelection) {
		var	/** @type {number} */ i = 0,
			/** @type {CanvasRenderingContext2D} */ context = this.context,
			/** @type {number} */ xOff = this.width / 2 - this.xOff - this.originX,
			/** @type {number} */ yOff = this.height / 2 - this.yOff - this.originY,
			/** @const {number} */ xzoom = this.zoom * this.originZ,
			/** @const {number} */ yzoom = this.getYZoom(this.zoom) * this.originZ,
			/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ cx2 = 0,
			/** @type {number} */ cy2 = 0,
			/** @type {number} */ coord = 0,
			/** @type {number} */ cx0 = 0,
			/** @type {number} */ cy0 = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ lastState = -1,
			/** @type {Float32Array} */ coords = this.coords,
			/** @const {number} */ numCoords = this.numCells,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @const {number} */ mask = (1 << LifeConstants.coordBufferBits) - 1;

		// if triangles are filled then sort by colour
		// check for sort function since IE doesn't have it
		if (filled && !drawingSelection && colours.sort) {
			// ensure unused buffer is at end
			state = (LifeConstants.coordBufferSize << LifeConstants.coordBufferBits) + 256;
			for (i = numCoords; i < LifeConstants.coordBufferSize; i += 1) {
				colours[i] = state;
			}
			colours.sort();
		}

		// draw each triangle
		context.beginPath();
		if (!filled) {
			// if drawing the grid then draw in order since all triangles are the grid colour
			coord = 0;
			state = 0;
			lastState = 0;
		}

		for (i = 0; i < numCoords; i += 1) {
			// get next triangle offset
			if (filled) {
				state = colours[i];
				coord = state & mask;
				state = state >> LifeConstants.coordBufferBits;
			}

			// get triangle start position
			cx0 = coords[coord];
			cy0 = coords[coord + 1];
			coord += 2;

			// draw the triangle
			y = ((cy0 + yOff) * yzoom) + halfDisplayHeight;
			x = ((cx0 + xOff) * xzoom) + halfDisplayWidth;
			cy2 = (coords[coord + 1] - cy0) * yzoom;
			cx2 = (coords[coord] - cx0) * xzoom;
			coord += 2;

			// set line colour
			if (state !== lastState) {
				lastState = state;
				if (i > 0) {
					// draw the batch of cells at the current colour
					if (filled) {
						// draw border if required
						if (borderWhenFilled) {
							context.stroke();
						}
						// fill the cells
						context.fill();
					} else {
						context.stroke();
					}
					// start a new batch
					context.beginPath();
				}
				if (filled) {
					// set the new cell colours
					if (!drawingSelection) {
						context.fillStyle = this.cellColourStrings[state];
						if (borderWhenFilled) {
							context.strokeStyle = this.cellColourStrings[state];
						}
					}
				}
			}

			// draw triangle
			context.moveTo(x | 0, y | 0);
			context.lineTo((cx2 + x) | 0, (cy2 + y) | 0);
			context.lineTo((cx2 + x) | 0, y | 0);
			cy2 = (coords[coord + 1] - cy0) * yzoom;
			cx2 = (coords[coord] - cx0) * xzoom;
			context.lineTo((cx2 + x) | 0, (cy2 + y) | 0);
			coord += 2;
		}

		// draw the final batch of cells
		if (filled) {
			// draw cell borders if required
			if (borderWhenFilled) {
				context.stroke();
			}
			// fill cells
			context.fill();
		} else {
			context.stroke();
		}
	};

	// draw hex cells in selection
	Life.prototype.drawHexCellsInSelection = function(/** @type {number} */ leftX, /** @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY, /** @type {number} */ xOff, /** @type {number} */ yOff, /** @type {Uint8Array} */ cells) {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ m = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @const {number} */ w2 = this.width / 2 - ViewConstants.sqrt3 / 4,
			/** @const {number} */ h2 = this.height / 2,
			/** @const {number} */ pi3 = Math.PI / 3,
			/** @const {number} */ radius = 1 / ViewConstants.sqrt3,
			/** @const {Array<number>} */ xa = [],
			/** @const {Array<number>} */ ya = [],
			/** @type {number} */ state = 0,
			/** @type {number} */ xa0 = 0,
			/** @type {number} */ ya0 = 0,
			/** @type {number} */ xa1 = 0,
			/** @type {number} */ ya1 = 0,
			/** @type {number} */ xa2 = 0,
			/** @type {number} */ ya2 = 0,
			/** @type {number} */ xa3 = 0,
			/** @type {number} */ ya3 = 0,
			/** @type {number} */ xa4 = 0,
			/** @type {number} */ ya4 = 0,
			/** @type {number} */ xa5 = 0,
			/** @type {number} */ ya5 = 0,
			/** @type {Float32Array} */ coords = this.coords,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ xOff1 = this.width / 2 - this.xOff - this.originX,
			/** @type {number} */ yOff1 = this.height / 2 - this.yOff - this.originY,
			/** @const {number} */ hexAdjust = -(this.height >> 2),
			/** @type {number} */ displayX = 0,
			/** @type {number} */ displayY = 0,
			/** @type {number} */ xzoom = this.zoom * this.originZ,
			/** @type {number} */ yzoom = this.getYZoom(this.zoom) * this.originZ,
			/** @type {number} */ swap = 0;

		// adjust for hex
		xOff1 += yOff1 / 2;
		xOff1 = xOff1 + hexAdjust + 0.5;
		yOff1 = yOff1 + 0.5;

		// order coordinates
		if (leftX > rightX) {
			swap = rightX;
			rightX = leftX;
			leftX = swap;
		}
		if (bottomY > topY) {
			swap = topY;
			topY = bottomY;
			bottomY = swap;
		}
		leftX += xOff;
		rightX += xOff;
		bottomY += yOff;
		topY += yOff;

		// check if buffers have been allocated
		if (colours.length !== LifeConstants.coordBufferSize) {
			this.coords = /** @type {!Float32Array} */ (this.allocator.allocate(Type.Float32, 12 * LifeConstants.coordBufferSize, "Life.coords", false));
			this.cellColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.coordBufferSize, "Life.cellColours", false));
			coords = this.coords;
			colours = this.cellColours;
		}

		// create hexagon coordinates
		k = pi3 / 2;
		for (j = 0; j <= 5; j += 1) {
			xa[j] = Math.cos(k) * radius;
			ya[j] = Math.sin(k) * radius * 1.16;
			k += pi3;
		}
		xa0 = xa[0];
		ya0 = ya[0];
		xa1 = xa[1];
		ya1 = ya[1];
		xa2 = xa[2];
		ya2 = ya[2];
		xa3 = xa[3];
		ya3 = ya[3];
		xa4 = xa[4];
		ya4 = ya[4];
		xa5 = xa[5];
		ya5 = ya[5];

		// create hexagons from selection box
		this.context.lineWidth = 1;
		this.context.lineCap = "round";
		this.context.lineJoin = "round";
		j = 0;
		k = 0;
		for (y = bottomY; y <= topY; y += 1) {
			cy = y - h2;
			displayY = (cy + yOff1) * yzoom + halfDisplayHeight;
			// clip to display
			if (displayY >= -yzoom && displayY < this.displayHeight + yzoom) {
				for (x = leftX; x <= rightX; x += 1) {
					if (cells[m] > 0) {
						cx = x - w2;
						displayX = (cx + xOff1 - (cy + yOff1) / 2) * xzoom + halfDisplayWidth;
						// clip to display
						if (displayX >= -xzoom && displayX < this.displayWidth + xzoom) {
							// encode coordinate index into the colour state so it can be sorted later
							colours[j] = (state << LifeConstants.coordBufferBits) + k;
							coords[k] = xa0 + cx;
							coords[k + 1] = ya0 + cy;
							coords[k + 2] = xa1 + cx;
							coords[k + 3] = ya1 + cy;
							coords[k + 4] = xa2 + cx;
							coords[k + 5] = ya2 + cy;
							coords[k + 6] = xa3 + cx;
							coords[k + 7] = ya3 + cy;
							coords[k + 8] = xa4 + cx;
							coords[k + 9] = ya4 + cy;
							coords[k + 10] = xa5 + cx;
							coords[k + 11] = ya5 + cy;
							k += 12;
							j += 1;

							// check if buffer is full
							if (j === LifeConstants.coordBufferSize) {
								// draw buffer
								this.numCells = j;
								this.drawHexCells(true, false, false, true);

								// clear buffer
								j = 0;
								k = 0;
							}
						}
					}
					// next cell
					m += 1;
				}
			} else {
				// next row
				m += (rightX - leftX + 1);
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawHexCells(true, false, false, true);

			// clear buffer
			j = 0;
			k = 0;
		}
	};

	// draw hex selection
	Life.prototype.drawHexSelection = function(/** @type {number} */ leftX, /** @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY, /** @type {number} */ xOff, /** @type {number} */ yOff) {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @const {number} */ w2 = this.width / 2 - ViewConstants.sqrt3 / 4,
			/** @const {number} */ h2 = this.height / 2,
			/** @const {number} */ pi3 = Math.PI / 3,
			/** @const {number} */ radius = 1 / ViewConstants.sqrt3,
			/** @const {Array<number>} */ xa = [],
			/** @const {Array<number>} */ ya = [],
			/** @type {number} */ state = 0,
			/** @type {number} */ xa0 = 0,
			/** @type {number} */ ya0 = 0,
			/** @type {number} */ xa1 = 0,
			/** @type {number} */ ya1 = 0,
			/** @type {number} */ xa2 = 0,
			/** @type {number} */ ya2 = 0,
			/** @type {number} */ xa3 = 0,
			/** @type {number} */ ya3 = 0,
			/** @type {number} */ xa4 = 0,
			/** @type {number} */ ya4 = 0,
			/** @type {number} */ xa5 = 0,
			/** @type {number} */ ya5 = 0,
			/** @type {Float32Array} */ coords = this.coords,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ xOff1 = this.width / 2 - this.xOff - this.originX,
			/** @type {number} */ yOff1 = this.height / 2 - this.yOff - this.originY,
			/** @const {number} */ hexAdjust = -(this.height >> 2),
			/** @type {number} */ displayX = 0,
			/** @type {number} */ displayY = 0,
			/** @type {number} */ xzoom = this.zoom * this.originZ,
			/** @type {number} */ yzoom = this.getYZoom(this.zoom) * this.originZ,
			/** @type {number} */ swap = 0;

		// adjust for hex
		xOff1 += yOff1 / 2;
		xOff1 = xOff1 + hexAdjust + 0.5;
		yOff1 = yOff1 + 0.5;

		// order coordinates
		if (leftX > rightX) {
			swap = rightX;
			rightX = leftX;
			leftX = swap;
		}
		if (bottomY > topY) {
			swap = topY;
			topY = bottomY;
			bottomY = swap;
		}
		leftX += xOff;
		rightX += xOff;
		bottomY += yOff;
		topY += yOff;

		// check if buffers have been allocated
		if (colours.length !== LifeConstants.coordBufferSize) {
			this.coords = /** @type {!Float32Array} */ (this.allocator.allocate(Type.Float32, 12 * LifeConstants.coordBufferSize, "Life.coords", false));
			this.cellColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.coordBufferSize, "Life.cellColours", false));
			coords = this.coords;
			colours = this.cellColours;
		}

		// create hexagon coordinates
		k = pi3 / 2;
		for (j = 0; j <= 5; j += 1) {
			xa[j] = Math.cos(k) * radius;
			ya[j] = Math.sin(k) * radius * 1.16;
			k += pi3;
		}
		xa0 = xa[0];
		ya0 = ya[0];
		xa1 = xa[1];
		ya1 = ya[1];
		xa2 = xa[2];
		ya2 = ya[2];
		xa3 = xa[3];
		ya3 = ya[3];
		xa4 = xa[4];
		ya4 = ya[4];
		xa5 = xa[5];
		ya5 = ya[5];

		// create hexagons from selection box
		this.context.lineWidth = 1;
		this.context.lineCap = "round";
		this.context.lineJoin = "round";
		j = 0;
		k = 0;
		for (y = bottomY; y <= topY; y += 1) {
			cy = y - h2;
			displayY = (cy + yOff1) * yzoom + halfDisplayHeight;

			// clip to display
			if (displayY >= -yzoom && displayY < this.displayHeight + yzoom) {
				/* TBD
				// adjust to draw hexagonal area
				var midY = (topY - bottomY) >> 1;
				var curY = y - bottomY;
				if (curY < midY) {
					leftOff = 0;
					rightOff = curY - midY;
				} else {
					leftOff = curY - midY;
					rightOff = 0;
				}

				for (x = leftX + leftOff; x <= rightX + rightOff; x += 1) {
					...
				}
				*/

				for (x = leftX; x <= rightX; x += 1) {
					cx = x - w2;
					displayX = (cx + xOff1 - (cy + yOff1) / 2) * xzoom + halfDisplayWidth;
					// clip to display
					if (displayX >= -xzoom && displayX < this.displayWidth + xzoom) {
						// encode coordinate index into the colour state so it can be sorted later
						colours[j] = (state << LifeConstants.coordBufferBits) + k;
						coords[k] = xa0 + cx;
						coords[k + 1] = ya0 + cy;
						coords[k + 2] = xa1 + cx;
						coords[k + 3] = ya1 + cy;
						coords[k + 4] = xa2 + cx;
						coords[k + 5] = ya2 + cy;
						coords[k + 6] = xa3 + cx;
						coords[k + 7] = ya3 + cy;
						coords[k + 8] = xa4 + cx;
						coords[k + 9] = ya4 + cy;
						coords[k + 10] = xa5 + cx;
						coords[k + 11] = ya5 + cy;
						k += 12;
						j += 1;

						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw buffer
							this.numCells = j;
							this.drawHexCells(true, false, false, true);

							// clear buffer
							j = 0;
							k = 0;
						}
					}
				}
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawHexCells(true, false, false, true);

			// clear buffer
			j = 0;
			k = 0;
		}
	};

	// draw hexagons
	Life.prototype.drawHexagons = function() {
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {Uint8Array} */ overlayRow = null,
			/** @type {BoundingBox} */ zoomBox = this.historyBox,
			/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ k = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @const {number} */ w2 = this.width / 2 - ViewConstants.sqrt3 / 4,
			/** @const {number} */ h2 = this.height / 2,
			/** @const {number} */ pi3 = Math.PI / 3,
			/** @const {number} */ radius = 1 / ViewConstants.sqrt3,
			/** @const {Array<number>} */ xa = [],
			/** @const {Array<number>} */ ya = [],
			/** @type {number} */ state = 0,
			/** @type {number} */ overState = 0,
			/** @type {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @type {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @type {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128,
			/** @type {number} */ aliveStart = this.aliveStart,
			/** @type {number} */ xa0 = 0,
			/** @type {number} */ ya0 = 0,
			/** @type {number} */ xa1 = 0,
			/** @type {number} */ ya1 = 0,
			/** @type {number} */ xa2 = 0,
			/** @type {number} */ ya2 = 0,
			/** @type {number} */ xa3 = 0,
			/** @type {number} */ ya3 = 0,
			/** @type {number} */ xa4 = 0,
			/** @type {number} */ ya4 = 0,
			/** @type {number} */ xa5 = 0,
			/** @type {number} */ ya5 = 0,
			/** @type {Float32Array} */ coords = this.coords,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @type {number} */ leftX = zoomBox.leftX,
			/** @type {number} */ rightX = zoomBox.rightX,
			/** @type {number} */ bottomY = zoomBox.bottomY,
			/** @type {number} */ topY = zoomBox.topY,
			/** @type {number} */ yOff = this.height / 2 - this.yOff - this.originY + 0.5,
			/** @type {number} */ xOff = this.width / 2 - this.xOff - this.originX + 0.5 -(this.height >> 2) + yOff / 2,
			/** @type {number} */ xzoom = this.zoom * this.originZ,
			/** @type {number} */ yzoom = this.getYZoom(this.zoom) * this.originZ,
			/** @type {number} */ displayY = 0,
			/** @type {number} */ displayX = 0,
			/** @type {number} */ xOffset = 0,
			/** @type {boolean} */ drawFilledCellBorders = !this.displayGrid && !this.cellBorders,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @type {number} */ maxGridSize = this.maxGridSize;

		// switch buffers if required
		if ((this.isSuper || this.isExtended || this.isRuleTree) && ((this.counter & 1) !== 0)) {
			colourGrid = this.nextColourGrid;
		}

		// check if buffers have been allocated
		if (colours.length !== LifeConstants.coordBufferSize) {
			this.coords = /** @type {!Float32Array} */ (this.allocator.allocate(Type.Float32, 12 * LifeConstants.coordBufferSize, "Life.coords", false));
			this.cellColours = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, LifeConstants.coordBufferSize, "Life.cellColours", false));
			coords = this.coords;
			colours = this.cellColours;
		}

		// use bounded grid if defined
		if (this.boundedGridType !== -1) {
			if (this.boundedGridWidth !== 0) {
				// set width to included bounded grid cells
				leftX = Math.round((this.width - this.boundedGridWidth) / 2) - 1;
				rightX = leftX + this.boundedGridWidth - 1 + 2;
			} else {
				// infinite width so set to grid width
				leftX = 0;
				rightX = this.width - 1;
			}

			if (this.boundedGridHeight !== 0) {
				// set height to included bounded grid cells
				bottomY = Math.round((this.height - this.boundedGridHeight) / 2) - 1;
				topY = bottomY + this.boundedGridHeight - 1 + 2;
			} else {
				// infinite height to set to grid height
				bottomY = 0;
				topY = this.height - 1;
			}
		}

		// create hexagon coordinates
		k = pi3 / 2;
		for (j = 0; j <= 5; j += 1) {
			xa[j] = Math.cos(k) * radius;
			ya[j] = Math.sin(k) * radius * 1.16;
			k += pi3;
		}
		xa0 = xa[0];
		ya0 = ya[0];
		xa1 = xa[1];
		ya1 = ya[1];
		xa2 = xa[2];
		ya2 = ya[2];
		xa3 = xa[3];
		ya3 = ya[3];
		xa4 = xa[4];
		ya4 = ya[4];
		xa5 = xa[5];
		ya5 = ya[5];

		// create hexagons from live cells
		this.context.lineWidth = 1;
		this.context.lineCap = "round";
		this.context.lineJoin = "round";
		j = 0;
		k = 0;

		if (bottomY > 0) {
			bottomY -= 1;
		}

		if (topY < this.height - 1) {
			topY += 1;
		}

		for (y = bottomY; y <= topY; y += 1) {
			// clip y to window
			displayY = ((y + yOff - h2) * yzoom) + halfDisplayHeight;
			if (displayY >= -yzoom && displayY < this.displayHeight + yzoom) {
				colourRow = colourGrid[y];
				if (overlayGrid) {
					overlayRow = overlayGrid[y];
				}
				cy = y - h2;
				xOffset = xOff - w2 - ((cy + yOff) / 2);
				for (x = leftX; x <= rightX; x += 1) {
					displayX = ((x + xOffset) * xzoom) + halfDisplayWidth;
					if (displayX >= -xzoom && displayX < this.displayWidth + xzoom) {
						state = colourRow[x];
						if (overlayRow) {
							overState = overlayRow[x];
							if (overState === state4 || overState === state6) {
								if (state >= aliveStart) {
									state = state3;
								} else {
									state = overState;
								}
							} else {
								if (overState === state3 || overState === state5) {
									if (state < aliveStart) {
										state = state4;
									} else {
										state = overState;
									}
								}
							}
						}
						if (state > 0) {
							// encode coordinate index into the colour state so it can be sorted later
							colours[j] = (state << LifeConstants.coordBufferBits) + k;
							cx = x - w2;
							coords[k] = xa0 + cx;
							coords[k + 1] = ya0 + cy;
							coords[k + 2] = xa1 + cx;
							coords[k + 3] = ya1 + cy;
							coords[k + 4] = xa2 + cx;
							coords[k + 5] = ya2 + cy;
							coords[k + 6] = xa3 + cx;
							coords[k + 7] = ya3 + cy;
							coords[k + 8] = xa4 + cx;
							coords[k + 9] = ya4 + cy;
							coords[k + 10] = xa5 + cx;
							coords[k + 11] = ya5 + cy;
							k += 12;
							j += 1;
						}
						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw buffer
							this.numCells = j;
							this.drawHexCells(true, drawFilledCellBorders, false, false);

							// draw cell borders if enabled and grid lines disabled
							if (this.cellBorders && !this.displayGrid) {
								this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
								this.drawHexCells(false, false, false, false);
							}

							// clear buffer
							j = 0;
							k = 0;
						}
					}
				}
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawHexCells(true, drawFilledCellBorders, false, false);

			// draw cell borders if enabled and grid lines disabled
			if (this.cellBorders && !this.displayGrid) {
				this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
				this.drawHexCells(false, false, false, false);
			}

			// clear buffer
			j = 0;
			k = 0;
		}

		// draw off grid cells
		j = 0;
		k = 0;

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}
		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// create cell coordinates for window
		bottomY = (((-halfDisplayHeight / yzoom) - yOff + h2) | 0) - 1;
		topY = (((halfDisplayHeight / yzoom) - yOff + h2) | 0) + 1;

		for (y = bottomY; y <= topY; y += 1) {
			// clip y to window
			displayY = ((y + yOff - h2) * yzoom) + halfDisplayHeight;
			if (displayY >= -yzoom && displayY < this.displayHeight + yzoom) {
				cy = y - h2;
				xOffset = xOff - w2 - ((cy + yOff) / 2);
				leftX = ((-halfDisplayWidth / xzoom) - xOffset - xzoom) | 0;
				rightX = ((halfDisplayWidth / xzoom) - xOffset + xzoom) | 0;
				for (x = leftX; x <= rightX; x += 1) {
					displayX = ((x + xOffset) * xzoom) + halfDisplayWidth;
					if (displayX >= -xzoom && displayX < this.displayWidth + xzoom) {
						// check if the cell is on the grid
						if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
							// encode coordinate index into the colour state so it can be sorted later
							colours[j] = (256 << LifeConstants.coordBufferBits) + k;
							cx = x - w2;
							coords[k] = xa0 + cx;
							coords[k + 1] = ya0 + cy;
							coords[k + 2] = xa1 + cx;
							coords[k + 3] = ya1 + cy;
							coords[k + 4] = xa2 + cx;
							coords[k + 5] = ya2 + cy;
							coords[k + 6] = xa3 + cx;
							coords[k + 7] = ya3 + cy;
							coords[k + 8] = xa4 + cx;
							coords[k + 9] = ya4 + cy;
							coords[k + 10] = xa5 + cx;
							coords[k + 11] = ya5 + cy;
							k += 12;
							j += 1;
						}
						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw buffer
							this.numCells = j;
							this.drawHexCells(true, drawFilledCellBorders, false, false);

							// draw cell borders if enabled and grid lines disabled
							if (this.cellBorders && !this.displayGrid) {
								this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
								this.drawHexCells(false, false, false, false);
							}

							// clear buffer
							j = 0;
							k = 0;
						}
					}
				}
			}
		}

		// draw any remaining cells
		this.numCells = j;
		if (j > 0) {
			// draw buffer
			this.drawHexCells(true, drawFilledCellBorders, false, false);

			// draw cell borders if enabled and grid lines disabled
			if (this.cellBorders && !this.displayGrid) {
				this.context.strokeStyle = "rgb(" + this.redChannel[0] + "," + this.blueChannel[0] + "," + this.greenChannel[0] + ")";
				this.drawHexCells(false, false, false, false);
			}

			// clear buffer
			j = 0;
			k = 0;
		}

		// draw grid if enabled
		if (this.displayGrid) {
			// set grid line colour
			this.context.strokeStyle = "rgb(" + (this.gridLineRaw >> 16) + "," + ((this.gridLineRaw >> 8) & 255) + "," + (this.gridLineRaw & 255) + ")";
			this.context.lineWidth = 1;
			j = 0;

			// create cell coordinates for window
			bottomY = ((-halfDisplayHeight / yzoom) - yOff + h2) | 0;
			topY = ((halfDisplayHeight / yzoom) - yOff + h2) | 0;

			for (y = bottomY; y <= topY + 1; y += 1) {
				// clip y to window
				displayY = ((y + yOff - h2) * yzoom) + halfDisplayHeight;
				if (displayY >= -yzoom && displayY < this.displayHeight + yzoom) {
					cy = y - h2;
					xOffset = xOff - w2 - ((cy + yOff) / 2);
					leftX = ((-halfDisplayWidth / xzoom) - xOffset - xzoom) | 0;
					rightX = ((halfDisplayWidth / xzoom) - xOffset + xzoom) | 0;
					for (x = leftX; x <= rightX; x += 1) {
						displayX = ((x + xOffset) * xzoom) + halfDisplayWidth;
						if (displayX >= -xzoom && displayX < this.displayWidth + xzoom) {
							cx = x - w2;
							coords[k] = xa0 + cx;
							coords[k + 1] = ya0 + cy;
							coords[k + 2] = xa1 + cx;
							coords[k + 3] = ya1 + cy;
							coords[k + 4] = xa2 + cx;
							coords[k + 5] = ya2 + cy;
							coords[k + 6] = xa3 + cx;
							coords[k + 7] = ya3 + cy;
							k += 8;
							j += 1;
						}
						// check if buffer is full
						if (j === LifeConstants.coordBufferSize) {
							// draw and clear buffer
							this.numCells = j;
							this.drawHexCells(false, false, true, false);
							j = 0;
							k = 0;
						}
					}
				}
			}

			// draw any remaining cells
			this.numCells = j;
			if (j > 0) {
				this.drawHexCells(false, false, true, false);
			}
		}
	};

	// draw hex cells
	Life.prototype.drawHexCells = function(/** @type {boolean} */ filled, /** @type {boolean} */ borderWhenFilled, /** @type {boolean} */ gridLines, /** @type {boolean} */ drawingSelection) {
		var	/** @type {number} */ i = 0,
			/** @type {CanvasRenderingContext2D} */ context = this.context,
			/** @type {number} */ xOff = this.width / 2 - this.xOff - this.originX,
			/** @type {number} */ yOff = this.height / 2 - this.yOff - this.originY,
			/** @const {number} */ xzoom = this.zoom * this.originZ,
			/** @const {number} */ yzoom = this.getYZoom(this.zoom) * this.originZ,
			/** @const {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @const {number} */ halfDisplayHeight = this.displayHeight / 2,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ cx = 0,
			/** @type {number} */ cy = 0,
			/** @type {number} */ cx2 = 0,
			/** @type {number} */ cy2 = 0,
			/** @const {number} */ hexAdjust = -(this.height >> 2),
			/** @type {number} */ coord = 0,
			/** @type {number} */ cx0 = 0,
			/** @type {number} */ cy0 = 0,
			/** @type {number} */ target = 0,
			/** @type {number} */ batch = 12,
			/** @type {number} */ state = 0,
			/** @type {number} */ lastState = -1,
			/** @type {Float32Array} */ coords = this.coords,
			/** @const {number} */ numCoords = this.numCells,
			/** @type {Uint32Array} */ colours = this.cellColours,
			/** @const {number} */ mask = (1 << LifeConstants.coordBufferBits) - 1;

		// adjust for hex
		xOff += yOff / 2;
		xOff = xOff + hexAdjust + 0.5;
		yOff = yOff + 0.5;

		// if hexagons are filled then sort by colour
		// check for sort function since IE doesn't have it
		if (filled && !drawingSelection && colours.sort) {
			// ensure unused buffer is at end
			state = (LifeConstants.coordBufferSize << LifeConstants.coordBufferBits) + 256;
			colours.fill(state, numCoords);
			colours.sort();
		}

		// draw each hexagon
		context.beginPath();
		if (!filled) {
			// if drawing the grid then only three line segments are needed
			if (gridLines) {
				batch = 8;
			} else {
				batch = 12;
			}
			target = batch;
			coord = 0;
			state = 0;
			lastState = 0;
		}
		for (i = 0; i < numCoords; i += 1) {
			// get next hexagon offset
			if (filled) {
				state = colours[i];
				coord = state & mask;
				target = coord + batch;
				state = state >> LifeConstants.coordBufferBits;
			}

			// get hexagon start position
			cx0 = coords[coord];
			cy0 = coords[coord + 1];
			cy = cy0 + yOff;
			cx = cx0 + xOff - cy / 2;
			coord += 2;

			// draw the hexagon
			y = (cy * yzoom) + halfDisplayHeight;
			x = (cx * xzoom) + halfDisplayWidth;
			cy2 = (coords[coord + 1] - cy0) * yzoom;
			cx2 = (coords[coord] - cx0) * xzoom;

			coord += 2;

			// set line colour
			if (state !== lastState) {
				lastState = state;
				if (i > 0) {
					if (filled) {
						// draw border if required
						if (borderWhenFilled) {
							context.stroke();
						}
						// fill the cells
						context.fill();
					} else {
						context.stroke();
					}
					// start a new batch
					context.beginPath();
				}
				if (filled) {
					// set the new cell colours unless drawing selection box
					if (!drawingSelection) {
						context.fillStyle = this.cellColourStrings[state];
						if (borderWhenFilled) {
							context.strokeStyle = this.cellColourStrings[state];
						}
					}
				}
			}

			// draw hexagon
			context.moveTo((x | 0) + 0.5, (y | 0) + 0.5);
			context.lineTo(((cx2 + x) | 0) + 0.5, ((cy2 + y) | 0) + 0.5);
			while (coord < target) {
				cy2 = (coords[coord + 1] - cy0) * yzoom;
				cx2 = (coords[coord] - cx0) * xzoom;
				coord += 2;
				context.lineTo(((cx2 + x) | 0) + 0.5, ((cy2 + y) | 0) + 0.5);
			}
			target += batch;
		}
		if (filled) {
			// draw cell borders if required
			if (borderWhenFilled) {
				context.stroke();
			}
			// fill cells
			context.fill();
		} else {
			context.stroke();
		}
	};

	// convert grid to RLE
	/** @returns {string} */
	Life.prototype.asRLE = function(/** @type {View} */ view, /** @type {Life} */ me, /** @type {number} */ inputStates, /** @type {number} */ outputStates, /** @type {Array<number>} */ mapping, /** @type {boolean} */ useAlias, /** @type {number} */ options) {
		var	/** @type {string} */ rle = "",
			/** @type {BoundingBox} */ zoomBox = (me.isLifeHistory ? me.historyBox : (me.isHROT ? me.HROTBox : me.zoomBox)),
			/** @type {number} */ leftX = zoomBox.leftX,
			/** @type {number} */ rightX = zoomBox.rightX,
			/** @type {number} */ topY = zoomBox.topY,
			/** @type {number} */ bottomY = zoomBox.bottomY,
			/** @type {number} */ width = rightX - leftX + 1,
			/** @type {number} */ height = topY - bottomY + 1,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ swap = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ last = 0,
			/** @type {number} */ count = 0,
			/** @type {number} */ rowCount = 0,
			/** @type {number} */ lastLength = 0,
			/** @const {number} */ charsPerRow = 69,
			/** @const {Array<string>} */ outputState = [],
			/** @const {number} */ asciiA = String("A").charCodeAt(0),
			/** @const {number} */ asciiP = String("p").charCodeAt(0),
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {number} */ col = 0,
			/** @type {number} */ xOff = view.panX - view.xOffset,
			/** @type {number} */ yOff = view.panY - view.yOffset,
			/** @type {string} */ ruleName = "",
			/** @type {BoundingBox} */ selBox = view.selectionBox,
			/** @type {boolean} */ blanks = (options & LifeConstants.rleBlanks) !== 0,
			/** @type {boolean} */ noNewLines = (options & LifeConstants.rleNoNewLines) !== 0,
			/** @type {boolean} */ noHeader = (options & LifeConstants.rleNoHeader) !== 0,
			/** @type {boolean} */ addComments = (options & LifeConstants.rleComments) !== 0;

		// ensure states are at least 2
		if (inputStates < 2) {
			inputStates = 2;
		}
		if (outputStates < 2) {
			outputStates = 2;
		}

		// check for [R]History
		if (inputStates === outputStates && me.isLifeHistory) {
			inputStates = 7;
			outputStates = 7;
		}

		// check for selection
		if (view.isSelection) {
			leftX = selBox.leftX + xOff;
			bottomY = selBox.bottomY + yOff;
			rightX = selBox.rightX + xOff;
			topY = selBox.topY + yOff;

			// order selection
			if (leftX > rightX) {
				swap = leftX;
				leftX = rightX;
				rightX = swap;
			}
			if (bottomY > topY) {
				swap = bottomY;
				bottomY = topY;
				topY = swap;
			}

			// compute selection size
			width = rightX - leftX + 1;
			height = topY - bottomY + 1;

			// adjust if CXRLE Pos defined
			if (view.posDefined) {
				if (me.boundedGridType !== -1) {
					leftX -= Math.floor(view.specifiedWidth / 2) - view.posXOffset * 2;
					rightX -= Math.floor(view.specifiedWidth / 2) - view.posXOffset * 2;
					bottomY -= Math.floor(view.specifiedHeight / 2) - view.posYOffset * 2;
					topY -= Math.floor(view.specifiedHeight / 2 ) - view.posYOffset * 2;
				} else {
					leftX += view.posXOffset * 2;
					rightX += view.posXOffset * 2;
					bottomY += view.posYOffset * 2;
					topY += view.posYOffset * 2;
				}
			}

		}

		// check for triangular rules
		if (me.isTriangular) {
			if (leftX > 0 && ((leftX & 1) !== 0)) {
				leftX -= 1;
				width += 1;
			}
			if ((bottomY > 0) && ((bottomY & 1) !== 0)) {
				bottomY -= 1;
				height += 1;
			}
		}

		// check for Margolus rules
		if (me.isMargolus) {
			if (leftX > 0 && ((leftX & 1) === 0)) {
				leftX -= 1;
				width += 1;
			}
			if ((bottomY > 0) && ((bottomY & 1) === 0)) {
				bottomY -= 1;
				height += 1;
			}
		}

		// populate mapping if not provided
		if (mapping.length === 0) {
			for (x = 0; x < inputStates; x += 1) {
				mapping[x] = x;
			}
		}

		// populate output states
		if (outputStates <= 2) {
			outputState[0] = "b";
			outputState[1] = "o";
		} else {
			outputState[0] = ".";
			for (x = 0; x < outputStates - 1; x += 1) {
				if (x >= 24) {
					outputState[x + 1] = String.fromCharCode(asciiP + ((x / 24) | 0) - 1) + String.fromCharCode(asciiA + (x % 24));
				} else {
					outputState[x + 1] = String.fromCharCode(asciiA + x);
				}
			}
		}

		// output comments if requested
		if (addComments) {
			rle += me.beforeTitle;
		}

		// output header
		if (!noHeader) {
			ruleName = ((useAlias && view.patternAliasName !== "") ? view.patternAliasName : view.patternRuleName);
			if (ruleName === "Conway's Life") {
				ruleName = "Life";
			}
			if (ruleName === "B3/S23History") {
				ruleName = "LifeHistory";
			}
			rle += "x = " + width + ", y = " + height + ", rule = " + ruleName;
			rle += view.patternBoundedGridDef;
			rle += "\n";
		}
		lastLength = rle.length;

		// output pattern
		y = bottomY;
		while (y <= topY) {
			x = leftX;
			// check for 2 state pattern
			if (inputStates <= 2 && !this.isRuleTree) {
				// use fast lookup
				colourRow = colourGrid[y];
				col = colourRow[x];
				if (col <= this.deadStart || col === this.boundedBorderColour) {
					last = 0;
				} else {
					last = 1;
				}
			} else {
				// not 2 state so use full lookup
				last = me.getState(x, y, false);
			}
			count = 1;
			x += 1;
			while (x <= rightX + 1) {
				if (x > rightX) {
					state = -1;
				} else {
					// check for 2 state pattern
					if (inputStates <= 2 && !this.isRuleTree) {
						// use fast lookup
						col = colourRow[x];
						if (col <= this.deadStart || col === this.boundedBorderColour) {
							state = 0;
						} else {
							state = 1;
						}
					} else {
						// not 2 state so use full lookup
						state = me.getState(x, y, false);
					}
				}
				if (state !== last) {
					// output end of previous row(s)
					if ((!(state === -1 && last === 0) || blanks) && rowCount > 0) {
						if (rowCount > 1) {
							rle += rowCount;
						}
						rle += "$";

						if (!noNewLines) {
							if (rle.length - lastLength >= charsPerRow) {
								rle += "\n";
								lastLength = rle.length;
							}
						}
						rowCount = 0;
					}
					// check if run is alive or dead
					if ((last > 0) || blanks) {
						if (count > 1) {
							rle += count;
						}
						rle += outputState[mapping[last]];
					} else if (x <= rightX) {
						if (count > 1) {
							rle += count;
						}
						rle += outputState[mapping[last]];
					}

					if (!noNewLines) {
						if (rle.length - lastLength >= charsPerRow) {
							rle += "\n";
							lastLength = rle.length;
						}
					}
					count = 1;
					last = state;
				} else {
					count += 1;
				}
				x += 1;
			}
			// end of row
			rowCount += 1;
			y += 1;
		}
		rle += "!\n";

		// add final comments if requested
		if (addComments) {
			rle += me.afterTitle;
		}

		// return the RLE
		return rle;
	};

	// return the weighted neighbour count
	/** @returns {number} */
	Life.prototype.neighbourCount = function(/** @type {number} */ x, /** @type {number} */ y) {
		var	/** @type {number} */ result = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ top = colourGrid[y - 1],
			/** @type {Uint8Array} */ mid = colourGrid[y],
			/** @type {Uint8Array} */ bot = colourGrid[y + 1],
			/** @const {number} */ aliveStart = LifeConstants.aliveStart;

		result = 64 + (top[x - 1] >= aliveStart ? 1 : 0) + (top[x] >= aliveStart ? 5 : 0) + (top[x + 1] >= aliveStart ? 1 : 0) + (mid[x - 1] >= aliveStart ? 5 : 0) + (mid[x + 1] >= aliveStart ? 5 : 0) + (bot[x - 1] >= aliveStart ? 1 : 0) + (bot[x] >= aliveStart ? 5 : 0) + (bot[x + 1] >= aliveStart ? 1 : 0);

		return result;
	};

	// update neighbour counts around the given cell
	Life.prototype.updateNeighbourCounts = function(/** @type {number} */ x, /** @type {number} */ y) {
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ row = null;

		// top row
		y -= 1;
		row = colourGrid[y];

		if (row[x - 1] >= this.aliveStart) {
			row[x - 1] = this.neighbourCount(x - 1, y);
		}

		if (row[x] >= this.aliveStart) {
			row[x] = this.neighbourCount(x, y);
		}

		if (row[x + 1] >= this.aliveStart) {
			row[x + 1] = this.neighbourCount(x + 1, y);
		}

		// middle row
		y += 1;
		row = colourGrid[y];

		if (row[x - 1] >= this.aliveStart) {
			row[x - 1] = this.neighbourCount(x - 1, y);
		}

		if (row[x] >= this.aliveStart) {
			row[x] = this.neighbourCount(x, y);
		}

		if (row[x + 1] >= this.aliveStart) {
			row[x + 1] = this.neighbourCount(x + 1, y);
		}

		// bottom row
		y += 1;
		row = colourGrid[y];

		if (row[x - 1] >= this.aliveStart) {
			row[x - 1] = this.neighbourCount(x - 1, y);
		}

		if (row[x] >= this.aliveStart) {
			row[x] = this.neighbourCount(x, y);
		}

		if (row[x + 1] >= this.aliveStart) {
			row[x + 1] = this.neighbourCount(x + 1, y);
		}
	};

	// set state (2 state patterns without bounded grid or [R]History)
	/** @returns {number} */
	Life.prototype.setState2 = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ state, /** @type {boolean} */ deadZero) {
		var	/** @type {Array<Uint16Array>} */ grid = this.grid16,
			/** @type {Array<Uint16Array>} */ tileGrid = this.tileGrid,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox,
			/** @type {number} */ cellAsBit = 0,
			/** @type {number} */ cellAsTileBit = 0,
			/** @type {boolean} */ growX = false,
			/** @type {boolean} */ growY = false,

			// whether cell should be alive in bit grid
			/** @type {boolean} */ bitAlive = false,

			// whether the cell is on the grid
			/** @type {boolean} */ onGrid = true,

			// whether a cell was or became LifeHistory state6
			/** @type {number} */ result = 0,

			// left target for next tile
			/** @type {number} */ leftTarget = (this.isTriangular ? 1 : 0),

			// right target to next tile
			/** @type {number} */ rightTarget = (this.isTriangular ? 14 : 15),

			// x coordinate for boundary check
			/** @type {number} */ cx = 0;

		// check if the cell is on the grid
		if (!((x === (x & this.widthMask)) && (y === (y & this.heightMask)))) {
			onGrid = false;

			// check if grid can grow in the required direction
			growX = (this.width < this.maxGridSize) && (x !== (x & this.widthMask));
			growY = (this.height < this.maxGridSize) && (y !== (y & this.heightMask));

			// attempt to grow the grid
			while (growX || growY) {
				if (growX || growY) {
					this.growGrid(growX, growY, true);
				}
				if (growX) {
					x += this.width >> 2;
				}
				if (growY) {
					y += this.height >> 2;
				}

				growX = (this.width < this.maxGridSize) && (x !== (x & this.widthMask));
				growY = (this.height < this.maxGridSize) && (y !== (y & this.heightMask));
			}

			// check if the cell is on the expanded grid
			if ((x === (x & this.widthMask)) && (y === (y & this.heightMask))) {
				// cell on expanded grid
				onGrid = true;

				// grid has changed so lookup again
				grid = this.grid16;
				tileGrid = this.tileGrid;
				colourGrid = this.colourGrid;
				colourTileGrid = this.colourTileGrid;
				colourTileHistoryGrid = this.colourTileHistoryGrid;
			}
		}

		// draw if on the grid
		if (onGrid) {
			// get correct grid for current generation
			if ((this.counter & 1) !== 0) {
				grid = this.nextGrid16;
				tileGrid = this.nextTileGrid;
			}

			// get the cell as a bit mask and tile mask
			cellAsBit = 1 << (~x & 15);
			cellAsTileBit = 1 << (~(x >> 4) & 15);
			bitAlive = ((state & 1) === 1);

			// draw alive or dead
			if (bitAlive) {
				// adjust population if cell was dead
				if ((grid[y][x >> 4] & cellAsBit) === 0) {
					this.population += 1;
				}

				// set cell
				switch (this.cellRenderer) {
					case LifeConstants.shaderCellAge:
						colourGrid[y][x] = this.aliveStart;
						break;

					case LifeConstants.shaderRainbow:
						colourGrid[y][x] = ((x + y) & 127) + 64;
						break;

					case LifeConstants.shaderNeighbourCount:
						colourGrid[y][x] = this.aliveStart;
						this.updateNeighbourCounts(x, y);
						break;

					case LifeConstants.shaderBasic:
						colourGrid[y][x] = this.aliveStart;
						break;
				}

				// update tile grids
				colourTileGrid[y >> 4][x >> 8] |= cellAsTileBit;
				colourTileHistoryGrid[y >> 4][x >> 8] |= cellAsTileBit;
				grid[y][x >> 4] |= cellAsBit;
				tileGrid[y >> 4][x >> 8] |= cellAsTileBit;

				// check left boundary
				cx = x & 15;
				if ((x > 0) && (cx <= leftTarget)) {
					x -= (cx + 1);
					tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					x += (cx + 1);
				} else {
					// check right boundary
					if ((x < this.width - 1) && (cx >= rightTarget)) {
						x += (16 - cx);
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						x -= (16 - cx);
					}
				}

				// check bottom boundary
				if ((y > 0) && ((y & 15) === 0)) {
					y -= 1;
					tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					if ((x > 0) && (cx <= leftTarget)) {
						x -= (cx + 1);
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						x += (cx + 1);
					} else {
						if ((x < this.width - 1) && (cx >= rightTarget)) {
							x += (16 - cx);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x -= (16 - cx);
						}
					}
					y += 1;
				} else {
					// check top boundary
					if ((y < this.height - 1) && ((y & 15) === 15)) {
						y += 1;
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						if ((x > 0) && (cx <= leftTarget)) {
							x -= (cx + 1);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x += (cx + 1);
						} else {
							if ((x < this.width - 1) && (cx >= rightTarget)) {
								x += (16 - cx);
								tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
								x -= (16 - cx);
							}
						}
						y -= 1;
					}
				}
			} else {
				// adjust population if cell was alive
				if ((grid[y][x >> 4] & cellAsBit) !== 0) {
					this.population -= 1;
				}

				// clear cell
				if (deadZero) {
					colourGrid[y][x] = this.unoccupied;
				} else {
					colourGrid[y][x] = this.deadStart;
				}

				if (this.cellRenderer === LifeConstants.shaderNeighbourCount) {
					this.updateNeighbourCounts(x, y);
				}

				// update tile grids
				colourTileGrid[y >> 4][x >> 8] |= cellAsTileBit;
				colourTileHistoryGrid[y >> 4][x >> 8] |= cellAsTileBit;
				grid[y][x >> 4] &= ~cellAsBit;
			}

			// if the state is not dead (or history) then update bounding box
			if (state > 0) {
				// check for initial cell
				if (this.population === 1) {
					zoomBox.leftX = x;
					zoomBox.rightX = x;
					zoomBox.bottomY = y;
					zoomBox.topY = y;
				} else {
					if (x < zoomBox.leftX) {
						zoomBox.leftX = x;
					}
					if (x > zoomBox.rightX) {
						zoomBox.rightX = x;
					}
					if (y < zoomBox.bottomY) {
						zoomBox.bottomY = y;
					}
					if (y > zoomBox.topY) {
						zoomBox.topY = y;
					}
				}
				if (x < historyBox.leftX) {
					historyBox.leftX = x;
				}
				if (x > historyBox.rightX) {
					historyBox.rightX = x;
				}
				if (y < historyBox.bottomY) {
					historyBox.bottomY = y;
				}
				if (y > historyBox.topY) {
					historyBox.topY = y;
				}
			} else {
				// cell cleared so only shrink if the cell was on the boundary of the bounding box
				if (x === zoomBox.leftX || x === zoomBox.rightX || y === zoomBox.topY || y === zoomBox.bottomY) {
					// mark shrink needed
					this.shrinkNeeded = true;
				}
			}
		}

		// return whether LifeHistory state 6 changed
		return result;
	};

	// set state (2 state [R]History patterns without bounded grid)
	/** @returns {number} */
	Life.prototype.setState2History = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ state, /** @type {boolean} */ deadZero) {
		var	/** @type {Array<Uint16Array>} */ grid = this.grid16,
			/** @type {Array<Uint16Array>} */ tileGrid = this.tileGrid,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox,
			/** @type {number} */ cellAsBit = 0,
			/** @type {number} */ cellAsTileBit = 0,
			/** @type {boolean} */ growX = false,
			/** @type {boolean} */ growY = false,
			/** @type {boolean} */ wasAlive = false,

			// current cell state
			/** @type {number} */ current = 0,

			// whether cell should be alive in bit grid
			/** @type {boolean} */ bitAlive = false,

			// whether the cell is on the grid
			/** @type {boolean} */ onGrid = true,

			// whether a cell was or became LifeHistory state6
			/** @type {number} */ result = 0,

			// left target for next tile
			/** @type {number} */ leftTarget = (this.isTriangular ? 1 : 0),

			// right target to next tile
			/** @type {number} */ rightTarget = (this.isTriangular ? 14 : 15),

			// x coordinate for boundary check
			/** @type {number} */ cx = 0;

		// check if the cell is on the grid
		if (!((x === (x & this.widthMask)) && (y === (y & this.heightMask)))) {
			onGrid = false;

			// check if grid can grow in the required direction
			growX = (this.width < this.maxGridSize) && (x !== (x & this.widthMask));
			growY = (this.height < this.maxGridSize) && (y !== (y & this.heightMask));

			// attempt to grow the grid
			while (growX || growY) {
				if (growX || growY) {
					this.growGrid(growX, growY, true);
				}
				if (growX) {
					x += this.width >> 2;
				}
				if (growY) {
					y += this.height >> 2;
				}

				growX = (this.width < this.maxGridSize) && (x !== (x & this.widthMask));
				growY = (this.height < this.maxGridSize) && (y !== (y & this.heightMask));
			}

			// check if the cell is on the expanded grid
			if ((x === (x & this.widthMask)) && (y === (y & this.heightMask))) {
				// cell on expanded grid
				onGrid = true;

				// grid has changed so lookup again
				grid = this.grid16;
				tileGrid = this.tileGrid;
				colourGrid = this.colourGrid;
				colourTileGrid = this.colourTileGrid;
				colourTileHistoryGrid = this.colourTileHistoryGrid;
				overlayGrid = this.overlayGrid;
			}
		}

		// draw if on the grid
		if (onGrid) {
			// get correct grid for current generation
			if ((this.counter & 1) !== 0) {
				grid = this.nextGrid16;
				tileGrid = this.nextTileGrid;
			}

			// get the cell as a bit mask and tile mask
			cellAsBit = 1 << (~x & 15);
			cellAsTileBit = 1 << (~(x >> 4) & 15);
			bitAlive = ((state & 1) === 1);

			// draw alive or dead
			if (bitAlive) {
				// adjust population if cell was dead
				if ((grid[y][x >> 4] & cellAsBit) === 0) {
					this.population += 1;
				}

				// set cell
				colourGrid[y][x] = this.aliveStart;

				// update tile grids
				colourTileGrid[y >> 4][x >> 8] |= cellAsTileBit;
				colourTileHistoryGrid[y >> 4][x >> 8] |= cellAsTileBit;
				grid[y][x >> 4] |= cellAsBit;
				tileGrid[y >> 4][x >> 8] |= cellAsTileBit;

				// check left boundary
				cx = x & 15;
				if ((x > 0) && (cx <= leftTarget)) {
					x -= (cx + 1);
					tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					x += (cx + 1);
				} else {
					// check right boundary
					if ((x < this.width - 1) && (cx >= rightTarget)) {
						x += (16 - cx);
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						x -= (16 - cx);
					}
				}

				// check bottom boundary
				if ((y > 0) && ((y & 15) === 0)) {
					y -= 1;
					tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					if ((x > 0) && (cx <= leftTarget)) {
						x -= (cx + 1);
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						x += (cx + 1);
					} else {
						if ((x < this.width - 1) && (cx >= rightTarget)) {
							x += (16 - cx);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x -= (16 - cx);
						}
					}
					y += 1;
				} else {
					// check top boundary
					if ((y < this.height - 1) && ((y & 15) === 15)) {
						y += 1;
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						if ((x > 0) && (cx <= leftTarget)) {
							x -= (cx + 1);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x += (cx + 1);
						} else {
							if ((x < this.width - 1) && (cx >= rightTarget)) {
								x += (16 - cx);
								tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
								x -= (16 - cx);
							}
						}
						y -= 1;
					}
				}
			} else {
				// adjust population if cell was alive
				if ((grid[y][x >> 4] & cellAsBit) !== 0) {
					this.population -= 1;
					wasAlive = true;
				}

				// clear cell
				if (deadZero) {
					colourGrid[y][x] = this.unoccupied;
				} else {
					colourGrid[y][x] = this.deadStart;
				}

				// update tile grids
				colourTileGrid[y >> 4][x >> 8] |= cellAsTileBit;
				colourTileHistoryGrid[y >> 4][x >> 8] |= cellAsTileBit;
				grid[y][x >> 4] &= ~cellAsBit;
			}

			// check for LifeHistory
			if (overlayGrid) {
				// check if the cell used to be or has become state 6
				current = overlayGrid[y][x];
				if (current > 0) {
					current = ViewConstants.stateMap.indexOf(current - 128);
				}
				if ((state === 6 && current !== 6) || (state !== 6 && current === 6)) {
					result = 1;
				}

				// update colour grid if history state
				if (state === 2) {
					colourGrid[y][x] = LifeConstants.deadMin;
				}

				// update overlay grid if not
				if (state === 0) {
					overlayGrid[y][x] = 0;
				} else {
					overlayGrid[y][x] = ViewConstants.stateMap[state] + 128;
				}
			}

			// if the state is not dead (or history) then update bounding box
			if (state > 0) {
				// check for initial cell
				if (this.population === 1) {
					zoomBox.leftX = x;
					zoomBox.rightX = x;
					zoomBox.bottomY = y;
					zoomBox.topY = y;
				} else {
					if (x < zoomBox.leftX) {
						zoomBox.leftX = x;
					}
					if (x > zoomBox.rightX) {
						zoomBox.rightX = x;
					}
					if (y < zoomBox.bottomY) {
						zoomBox.bottomY = y;
					}
					if (y > zoomBox.topY) {
						zoomBox.topY = y;
					}
				}

				if (x < historyBox.leftX) {
					historyBox.leftX = x;
				}
				if (x > historyBox.rightX) {
					historyBox.rightX = x;
				}
				if (y < historyBox.bottomY) {
					historyBox.bottomY = y;
				}
				if (y > historyBox.topY) {
					historyBox.topY = y;
				}
			} else {
				// state cleared so only shrink if the cell was on the boundary of the bounding box
				if (x === zoomBox.leftX || x === zoomBox.rightX || y === zoomBox.topY || y === zoomBox.bottomY) {
					// mark shrink needed
					this.shrinkNeeded = true;
				}
			}
		}

		// return whether LifeHistory state 6 changed
		return result;
	};

	// set state (any pattern)
	/** @returns {number} */
	Life.prototype.setStateAny = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ state, /** @type {boolean} */ deadZero) {
		var	/** @type {Array<Uint16Array>} */ grid = this.grid16,
			/** @type {Array<Uint16Array>} */ tileGrid = this.tileGrid,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ HROTBox = this.HROTBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox,
			/** @type {number} */ cellAsBit = 0,
			/** @type {number} */ cellAsTileBit = 0,
			/** @type {number} */ initialState = state,

			// multi-state alive cell
			/** @type {number} */ aliveState = 0,

			// current cell state
			/** @type {number} */ current = 0,

			// whether cell should be alive in bit grid
			/** @type {boolean} */ bitAlive = false,

			// whether a cell was or became LifeHistory state6
			/** @type {number} */ result = 0,

			// left target for next tile
			/** @type {number} */ leftTarget = (this.isTriangular ? 1 : 0),

			// right target to next tile
			/** @type {number} */ rightTarget = (this.isTriangular ? 14 : 15),

			// x coordinate for boundary check
			/** @type {number} */ cx = 0;

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// check for PCA
		if (this.isPCA || this.isRuleTree) {
			current = colourGrid[y][x];
			if (deadZero && state === 0) {
				colourGrid[y][x] = 0;
			} else {
				colourGrid[y][x] = this.historyStates + state;
			}
			colourTileHistoryGrid[y >> 4][x >> 8] = 65535;
			if (this.isRuleTree) {
				if ((this.counter & 1) !== 0) {
					grid = this.nextGrid16;
					tileGrid = this.nextTileGrid;
				}

				// get the tile mask
				cellAsTileBit = 1 << (~(x >> 4) & 15);

				// mark the tile
				tileGrid[y >> 4][x >> 8] |= cellAsTileBit;
			}

			// update population
			if (state === 0) {
				if (current > this.historyStates) {
					if (this.isPCA) {
						this.population -= this.bitCounts16[current - this.historyStates];
					} else {
						this.population -= 1;
					}
				}
			} else {
				if (this.isPCA) {
					if (current <= this.historyStates) {
						this.population += this.bitCounts16[state];
					} else {
						this.population -= this.bitCounts16[current - this.historyStates];
						this.population += this.bitCounts16[state];
					}
				} else {
					tileGrid[y >> 4][x >> 8] |= cellAsTileBit;

					if (current === 0) {
						this.population += 1;
					}

					// check left boundary
					cx = x & 15;
					if ((x > 0) && (cx <= leftTarget)) {
						x -= (cx + 1);
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						x += (cx + 1);
					} else {
						// check right boundary
						if ((x < this.width - 1) && (cx >= rightTarget)) {
							x += (16 - cx);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x -= (16 - cx);
						}
					}

					// check bottom boundary
					if ((y > 0) && ((y & 15) === 0)) {
						y -= 1;
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						if ((x > 0) && (cx <= leftTarget)) {
							x -= (cx + 1);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x += (cx + 1);
						} else {
							if ((x < this.width - 1) && (cx >= rightTarget)) {
								x += (16 - cx);
								tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
								x -= (16 - cx);
							}
						}
						y += 1;
					} else {
						// check top boundary
						if ((y < this.height - 1) && ((y & 15) === 15)) {
							y += 1;
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							if ((x > 0) && (cx <= leftTarget)) {
								x -= (cx + 1);
								tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
								x += (cx + 1);
							} else {
								if ((x < this.width - 1) && (cx >= rightTarget)) {
									x += (16 - cx);
									tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
									x -= (16 - cx);
								}
							}
							y -= 1;
						}
					}
				}
			}
		} else {
			// get correct grid for current generation
			if ((this.counter & 1) !== 0) {
				grid = this.nextGrid16;
				tileGrid = this.nextTileGrid;
			}

			// get the cell as a bit mask and tile mask
			cellAsBit = 1 << (~x & 15);
			cellAsTileBit = 1 << (~(x >> 4) & 15);

			// check for multi-state rules
			if (!this.isHROT) {
				if (this.isExtended) {
					bitAlive = state > 0;
					current = colourGrid[y][x];
				} else {
					if (this.multiNumStates <= 2 || this.isSuper) {
						// 2-state
						bitAlive = ((state & 1) === 1);
					} else {
						// generations
						bitAlive = (state === this.multiNumStates - 1);
					}
				}

				// check for [R]Super
				if (this.isSuper) {
					// check if the cell used to be or has become state 6
					current = colourGrid[y][x];
					if ((state === 6 && current !== 6) || (state !== 6 && current === 6)) {
						result = 1;
					}
				}

				// draw alive or dead
				if (bitAlive) {
					// adjust population if cell was dead
					if (this.isExtended) {
						if (!current) {
							this.population += 1;
						}
					} else {
						if ((grid[y][x >> 4] & cellAsBit) === 0) {
							this.population += 1;
						}
					}

					// set cell
					if (this.cellRenderer === LifeConstants.shaderRainbow) {
						colourGrid[y][x] = ((x + y) & 127) + 64;
					} else {
						if (this.cellRenderer === LifeConstants.shaderNeighbourCount) {
							colourGrid[y][x] = this.aliveStart;
							this.updateNeighbourCounts(x, y);
						} else {
							if (this.isExtended || this.isSuper) {
								colourGrid[y][x] = state;
							} else {
								colourGrid[y][x] = this.aliveStart;
							}
						}
					}

					// update tile grids
					colourTileGrid[y >> 4][x >> 8] |= cellAsTileBit;
					colourTileHistoryGrid[y >> 4][x >> 8] |= cellAsTileBit;
					grid[y][x >> 4] |= cellAsBit;
					tileGrid[y >> 4][x >> 8] |= cellAsTileBit;

					// check left boundary
					cx = x & 15;
					if ((x > 0) && (cx <= leftTarget)) {
						x -= (cx + 1);
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						x += (cx + 1);
					} else {
						// check right boundary
						if ((x < this.width - 1) && (cx >= rightTarget)) {
							x += (16 - cx);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x -= (16 - cx);
						}
					}

					// check bottom boundary
					if ((y > 0) && ((y & 15) === 0)) {
						y -= 1;
						tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
						if ((x > 0) && (cx <= leftTarget)) {
							x -= (cx + 1);
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							x += (cx + 1);
						} else {
							if ((x < this.width - 1) && (cx >= rightTarget)) {
								x += (16 - cx);
								tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
								x -= (16 - cx);
							}
						}
						y += 1;
					} else {
						// check top boundary
						if ((y < this.height - 1) && ((y & 15) === 15)) {
							y += 1;
							tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
							if ((x > 0) && (cx <= leftTarget)) {
								x -= (cx + 1);
								tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
								x += (cx + 1);
							} else {
								if ((x < this.width - 1) && (cx >= rightTarget)) {
									x += (16 - cx);
									tileGrid[y >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
									x -= (16 - cx);
								}
							}
							y -= 1;
						}
					}
				} else {
					// adjust population if cell was alive
					if (this.isExtended) {
						if (current > 0) {
							this.population -= 1;
						}
					} else {
						if ((grid[y][x >> 4] & cellAsBit) !== 0) {
							this.population -= 1;
						}
					}

					// clear cell
					if (this.isExtended || this.isSuper) {
						colourGrid[y][x] = state;
					} else {
						if (deadZero) {
							colourGrid[y][x] = this.unoccupied;
						} else {
							colourGrid[y][x] = this.deadStart;
						}
					}

					if (this.cellRenderer === LifeConstants.shaderNeighbourCount) {
						this.updateNeighbourCounts(x, y);
					}

					// update tile grids
					colourTileGrid[y >> 4][x >> 8] |= cellAsTileBit;
					colourTileHistoryGrid[y >> 4][x >> 8] |= cellAsTileBit;
					grid[y][x >> 4] &= ~cellAsBit;
				}

				// check for [R]History
				if (this.isLifeHistory && overlayGrid) {
					// check if the cell used to be or has become state 6
					current = overlayGrid[y][x];
					if (current > 0) {
						current = ViewConstants.stateMap.indexOf(current - 128);
					}
					if ((state === 6 && current !== 6) || (state !== 6 && current === 6)) {
						result = 1;
					}

					// update colour grid if history state
					if (state === 2) {
						colourGrid[y][x] = LifeConstants.deadMin;
					} else {
						// update overlay grid if not
						if (state === 0) {
							overlayGrid[y][x] = 0;
						} else {
							overlayGrid[y][x] = ViewConstants.stateMap[state] + 128;
						}
					}
				}

				// check for generations style rule
				if (this.multiNumStates > 2 && !(this.isSuper || this.isExtended)) {
					// write the correct state to the colour grid
					if (state > 0) {
						state = this.historyStates + state;
					}
					colourGrid[y][x] = state;
				}
			} else {
				// update population for HROT
				current = colourGrid[y][x];
				if (this.multiNumStates === 2) {
					aliveState = this.aliveStart;
					if (state === 1) {
						state = this.aliveStart;
					}
					if (state === 0 && current >= aliveState) {
						if (deadZero) {
							state = 0;
						} else {
							state = this.historyStates;
						}
					}
					colourGrid[y][x] = state;
				} else {
					aliveState = this.multiNumStates - 1 + this.historyStates;
					if (state > 0) {
						state = this.historyStates + state;
					}
					colourGrid[y][x] = state;
				}

				// update tile grids
				colourTileGrid[y >> 4][x >> 8] |= cellAsTileBit;
				colourTileHistoryGrid[y >> 4][x >> 8] |= cellAsTileBit;

				if (current < aliveState && state >= aliveState) {
					this.population += 1;
				} else {
					if (current >= aliveState && state < aliveState) {
						this.population -= 1;
					}
				}
			}
		}

		// if the state is not dead (or history) then update bounding box
		if (initialState > 0) {
			// check for initial cell
			if (this.population === 1) {
				// use shrink since there might be Generations dying cells
				this.shrinkNeeded = true;
			}
			if (x < zoomBox.leftX) {
				zoomBox.leftX = x;
			}
			if (x > zoomBox.rightX) {
				zoomBox.rightX = x;
			}
			if (y < zoomBox.bottomY) {
				zoomBox.bottomY = y;
			}
			if (y > zoomBox.topY) {
				zoomBox.topY = y;
			}
			if (x < historyBox.leftX) {
				historyBox.leftX = x;
			}
			if (x > historyBox.rightX) {
				historyBox.rightX = x;
			}
			if (y < historyBox.bottomY) {
				historyBox.bottomY = y;
			}
			if (y > historyBox.topY) {
				historyBox.topY = y;
			}

			// if the state is alive then update HROT alive bounding box
			if (this.isHROT && state === this.multiNumStates - 1 + this.historyStates) {
				if (this.population === 1) {
					HROTBox.leftX = x;
					HROTBox.rightX = x;
					HROTBox.bottomY = y;
					HROTBox.topY = y;
				} else {
					if (x < HROTBox.leftX) {
						HROTBox.leftX = x;
					}
					if (x > HROTBox.rightX) {
						HROTBox.rightX = x;
					}
					if (y < HROTBox.bottomY) {
						HROTBox.bottomY = y;
					}
					if (y > HROTBox.topY) {
						HROTBox.topY = y;
					}
				}
			}
		} else {
			// state cleared only shrink if the cell was on the boundary of the bounding box
			if (x === zoomBox.leftX || x === zoomBox.rightX || y === zoomBox.topY || y === zoomBox.bottomY) {
				// mark shrink needed
				this.shrinkNeeded = true;
			}
		}

		// return whether LifeHistory or [R]Super state 6 changed
		return result;
	};

	// dispatcher for getState
	/** @returns {number} */
	Life.prototype.getState = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {boolean} */ rawRequested) {
		switch (this.stateMode) {
		case LifeConstants.mode2:
			return this.getState2(x, y, rawRequested);
		case LifeConstants.mode2Table:
			return this.getState2(x, y, true);
		case LifeConstants.mode2History:
			return this.getState2History(x, y, rawRequested);
		default:
			return this.getStateAny(x, y, rawRequested);
		}
	};

	// dispatcher for setState
	/** @returns {number} */
	Life.prototype.setState = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ state, /** @type {boolean} */ deadZero) {
		switch (this.stateMode) {
		case LifeConstants.mode2:
			return this.setState2(x, y, state, deadZero);
		case LifeConstants.mode2Table:
			return this.setStateAny(x, y, state, deadZero);
		case LifeConstants.mode2History:
			return this.setState2History(x, y, state, deadZero);
		default:
			return this.setStateAny(x, y, state, deadZero);
		}
	};

	// setup dynamic calls for performance
	Life.prototype.setupDynamicCalls = function() {
		// pick optimized get and set cell calls based on pattern type and bounded grid
		if (this.multiNumStates === -1 && this.boundedGridType === -1 && !this.isNone) {
			if (this.isLifeHistory) {
				this.stateMode = LifeConstants.mode2History;
			} else {
				this.stateMode = LifeConstants.mode2;
			}
		} else {
			if (this.isRuleTree && this.multiNumStates === 2) {
				this.stateMode = LifeConstants.mode2Table;
			} else {
				this.stateMode = LifeConstants.modeAny;
			}
		}
	};

	// allocate or clear graph data
	Life.prototype.allocateGraphData = function(/** @type {boolean} */ allocate) {
		var	/** @const {number} */ entries = 1 << LifeConstants.popChunkPower;

		if (allocate) {
			this.popGraphData = Array.matrix(Type.Uint32, 1, entries, 0, this.allocator, "Life.popGraphData", false);
			this.birthGraphData = Array.matrix(Type.Uint32, 1, entries, 0, this.allocator, "Life.birthGraphData", false);
			this.deathGraphData = Array.matrix(Type.Uint32, 1, entries, 0, this.allocator, "Life.deathGraphData", false);
			this.popGraphEntries = entries;
		} else {
			this.popGraphData = Array.matrix(Type.Uint32, 0, 0, 0, this.allocator, "Life.popGraphData", false);
			this.birthGraphData = Array.matrix(Type.Uint32, 0, 0, 0, this.allocator, "Life.birthGraphData", false);
			this.deathGraphData = Array.matrix(Type.Uint32, 0, 0, 0, this.allocator, "Life.deathGraphData", false);
			this.popGraphEntries = 0;
		}
	};

	// get state (2 state patterns without bounded grid or [R]History)
	/** @returns {number} */
	Life.prototype.getState2 = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {boolean} */ rawRequested) {
		// result
		var	/** @type {number} */ result = 0,
			/** @type {number} */ col = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid;

		// check if coordinates are on the grid
		if ((x === (x & this.widthMask)) && (y === (y & this.heightMask))) {
			// check which grid is needed
			if (this.isRuleTree) {
				// swap grids every generation
				if ((this.counter & 1) !== 0) {
					colourGrid = this.nextColourGrid;
				}
			}

			// get the colour grid result
			col = colourGrid[y][x];
			if (rawRequested) {
				result = col;
			} else {
				if (col <= this.deadStart) {
					result = 0;
				} else {
					result = 1;
				}
			}
		}

		// return the result
		return result;
	};

	// get state (2 state [R]History patterns without bounded grid)
	/** @returns {number} */
	Life.prototype.getState2History = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {boolean} */ rawRequested) {
		// result
		var	/** @type {number} */ result = 0,
			/** @type {number} */ col = 0,
			/** @type {number} */ over = 0,

			// get states 3, 4, 5 and 6
			/** @const {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @const {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @const {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @const {number} */ state6 = ViewConstants.stateMap[6] + 128;

		// check if coordinates are on the grid
		if ((x === (x & this.widthMask)) && (y === (y & this.heightMask))) {
			// get the colour grid result
			col = this.colourGrid[y][x];

			// check if raw data requested
			if (rawRequested) {
				result = col;
			} else {
				// check for the overlay grid
				if (this.overlayGrid) {
					// get the overlay colour
					over = this.overlayGrid[y][x];

					// states 4 and 6
					if (over === state4 || over === state6) {
						// if alive cell then use state 3
						if (col >= this.aliveStart) {
							over = state3;
						}
						result = ViewConstants.stateMap.indexOf(over - 128);
					} else {
						// states 3 and 5
						if (over === state3 || over === state5) {
							// if dead cell then use state 4
							if (col < this.aliveStart) {
								over = state4;
							}
							result = ViewConstants.stateMap.indexOf(over - 128);
						} else {
							if (col === this.unoccupied) {
								result = 0;
							} else {
								if (col <= this.deadStart) {
									result = 2;
								} else {
									result = 1;
								}
							}
						}
					}
				} else {
					// no overlay grid
					if (col <= this.deadStart) {
						result = 0;
					} else {
						result = 1;
					}
				}
			}
		}

		// return the result
		return result;
	};

	// get state (any pattern)
	/** @returns {number} */
	Life.prototype.getStateAny = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {boolean} */ rawRequested) {
		// result
		var	/** @type {number} */ result = 0,
			/** @type {number} */ col = 0,
			/** @type {number} */ over = 0,

			// get states 3, 4, 5 and 6
			/** @const {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @const {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @const {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @const {number} */ state6 = ViewConstants.stateMap[6] + 128,

			// bounded grid top left
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),
			/** @type {number} */ leftX = Math.round((this.width - this.boundedGridWidth) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - this.boundedGridHeight) / 2) + boxOffset,

			// bounded grid bottom right
			/** @type {number} */ rightX = leftX + this.boundedGridWidth - 1,
			/** @type {number} */ topY = bottomY + this.boundedGridHeight - 1,

			// colour grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid;

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// check if coordinates are on the grid
		if ((x === (x & this.widthMask)) && (y === (y & this.heightMask))) {
			// get the colour grid result
			col = colourGrid[y][x];

			// check if raw data requested or Generations or HROT rule used
			if (rawRequested || this.multiNumStates > 2) {
				// check if state is not dead
				if (this.multiNumStates > 2 && col > 0 && !this.isNone) {
					// check for history states
					if (col <= this.historyStates) {
						result = 0;
					} else {
						// check for bounded grid cylinders
						if (this.boundedGridType !== -1) {
							if (this.boundedGridWidth === 0) {
								leftX = 0;
								rightX = this.width - 1;
							}
							if (this.boundedGridHeight === 0) {
								bottomY = 0;
								topY = this.height - 1;
							}
						}

						// check for bounded grid border cell
						if (this.boundedGridType !== -1 && col === this.boundedBorderColour && (!(x >= leftX && x <= rightX && y >= bottomY && y <= topY))) {
							result = 0;
						} else {
							if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
								result = col - this.historyStates;
							} else {
								result = this.multiNumStates + this.historyStates - col;
							}
						}
					}
				} else {
					result = col;
				}
			} else {
				// check for the overlay grid
				if (this.overlayGrid) {
					// check for bounded grid
					if (this.boundedGridType !== -1 && col === this.boundedBorderColour && (!(x >= leftX && x <= rightX && y >= bottomY && y <= topY))) {
						result = 0;
					} else {
						// get the overlay colour
						over = this.overlayGrid[y][x];

						// states 4 and 6
						if (over === state4 || over === state6) {
							// if alive cell then use state 3
							if (col >= this.aliveStart) {
								over = state3;
							}
							result = ViewConstants.stateMap.indexOf(over - 128);
						} else {
							// states 3 and 5
							if (over === state3 || over === state5) {
								// if dead cell then use state 4
								if (col < this.aliveStart) {
									over = state4;
								}
								result = ViewConstants.stateMap.indexOf(over - 128);
							} else {
								if (col === this.unoccupied) {
									result = 0;
								} else {
									if (col <= this.deadStart) {
										result = 2;
									} else {
										result = 1;
									}
								}
							}
						}
					}
				} else {
					// no overlay grid
					if (col <= this.deadStart || col === this.boundedBorderColour) {
						result = 0;
					} else {
						result = 1;
					}
				}
			}
		}

		// return the result
		return result;
	};

	// count tiles in a grid
	/** @returns {number} */
	Life.prototype.tileCount = function(/** @type {Array<Uint16Array>} */ tile) {
		var	/** @type {Uint16Array} */ tileRow = null,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ tileGroup = 0,
			/** @const {number} */ l = tile.length,
			/** @const {number} */ w = tile[0].length,
			/** @type {Uint8Array} */ bitCounts16 = this.bitCounts16,

			// zero count
			/** @type {number} */ result = 0;

		// count tiles used
		for (y = 0; y < l; y += 1) {
			// get the next tile row
			tileRow = tile[y];

			// get each set of tiles in the row
			for (x = 0; x < w; x += 1) {
				// get the next tile group
				tileGroup = tileRow[x];

				// count the tiles in the group
				if (tileGroup) {
					result += bitCounts16[tileGroup];
				}
			}
		}

		// return count
		return result;
	};

	// check for reverse playback
	Life.prototype.checkReverse = function(/** @type {View} */ view, /** @type {number} */ gen) {
		var	/** @type {number} */ i = 0,
			/** @type {Array} */ editList = view.editList,
			/** @type {number} */ l = editList.length,
			record = null,
			/** @type {boolean} */ found = false;

		// do nothing if not Margolus rule
		if (this.isMargolus || this.isPCA) {
			// see if there is a reverse playback record at the given generation
			this.reversePending = false;
			this.reverseMargolus = false;
			i = 0;
			while (!found && i < l) {
				// get next record
				record = editList[i];

				// check if it is before target generation
				if (record.gen < gen) {
					// check if it is a reverse playback record
					if (record.action === "reverse playback") {
						this.reverseMargolus = !this.reverseMargolus;
					}
					// next record
					i += 1;
				} else {
					// check if it is after taget generation
					if (record.gen > gen) {
						// bail out since generation passed
						found = true;
					} else {
						// at target generation
						if (record.action === "reverse playback") {
							this.reversePending = true;
							found = true;
						} else {
							// next record
							i += 1;
						}
					}
				}
			}
		}
	};

	// run to given generation (used to step back)
	Life.prototype.runTo = function(/** @type {number} */ targetGen, /** @type {boolean} */ graphDisabled, /** @type {View} */ view) {
		// get the latest snapshot
		var	/** @type {number} */ numSnapshots = this.snapshotManager.snapshots.length,
			/** @type {Snapshot} */ snapshot = null;

		// if generation is earlier than current go from previous snapshot
		if (targetGen < this.counter) {
			snapshot = this.snapshotManager.snapshotBefore(targetGen);

			// check if a snapshot was deleted
			if (numSnapshots !== this.snapshotManager.snapshots.length) {
				// reduce snapshot target
				this.nextSnapshotTarget -= (LifeConstants.snapshotInterval * (numSnapshots - this.snapshotManager.snapshots.length));
			}
		}

		// check if the snapshot exists
		if (snapshot) {
			// restore the snapshot
			this.restoreSnapshot(snapshot, view);
			view.pasteRLEList();
			view.pasteEdits();
		}

		// play from the snapshot counter to the target
		while (this.counter < targetGen) {
			// check for reverse direction
			this.checkReverse(view, this.counter);

			// for PCA clear the next generation
			if (this.isPCA) {
				if ((this.counter & 1) === 1) {
					this.colourGrid.whole.fill(0);
				} else {
					this.nextColourGrid.whole.fill(0);
				}
			}

			this.nextGeneration(true);
			view.fixedPointCounter += view.refreshRate;
			this.convertToPensTile();

			this.saveSnapshotIfNeeded(view);
			view.pasteRLEList();
			this.savePopulationData();
		}
	};

	// restore snapshot
	Life.prototype.restoreSnapshot = function(/** @type {Snapshot} */ snapshot, /** @type {View} */ view) {
		var	/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid;

		// restore the counter
		this.counter = snapshot.counter;
		view.fixedPointCounter = snapshot.fixedCounter;
		this.counterMargolus = snapshot.counterMargolus;
		this.maxMargolusGen = snapshot.maxMargolusGen;

		// check which buffer to copy to
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid;
			nextGrid = this.grid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.grid;
			nextGrid = this.nextGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear the grid, colour grid and small colour grid
		grid.whole.fill(0);
		nextGrid.whole.fill(0);
		this.colourGrid.whole.fill(0);
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			this.nextColourGrid.whole.fill(0);
		}
		this.smallColourGrid.whole.fill(0);

		// restore grid from snapshot
		snapshot.restoreGridUsingTile(grid, tileGrid, this);

		// restore colour grid from snapshot
		snapshot.restoreColourGridUsingTile(colourGrid, this.colourTileGrid, this, snapshot.colourBuffer);
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			Array.copy(colourGrid, this.nextColourGrid);
		}

		// restore overlay if required
		if (this.drawOverlay) {
			overlayGrid.whole.fill(0);
			this.smallOverlayGrid.whole.fill(0);
			snapshot.restoreColourGridUsingTile(overlayGrid, this.colourTileHistoryGrid, this, snapshot.overlayBuffer);
			this.populateState6MaskFromColGrid();
		} else {
			if (this.isSuper) {
				this.populateState6MaskFromColGrid();
			}

			// copy the colour tile history grid into the colour grid
			Array.copy(this.colourTileHistoryGrid, this.colourTileGrid);
		}

		// copy the tile grid to the next tile grid
		Array.copy(tileGrid, nextTileGrid);
		Array.copy(grid, nextGrid);

		// restore the bounding box
		this.zoomBox.leftX = snapshot.zoomBox.leftX;
		this.zoomBox.bottomY = snapshot.zoomBox.bottomY;
		this.zoomBox.rightX = snapshot.zoomBox.rightX;
		this.zoomBox.topY = snapshot.zoomBox.topY;
		this.HROTBox.leftX = snapshot.HROTBox.leftX;
		this.HROTBox.bottomY = snapshot.HROTBox.bottomY;
		this.HROTBox.rightX = snapshot.HROTBox.rightX;
		this.HROTBox.topY = snapshot.HROTBox.topY;

		// restore the population
		this.population = snapshot.population;
		this.births = snapshot.births;
		this.deaths = snapshot.deaths;
	};

	// save snapshot
	Life.prototype.saveSnapshot = function(/** @type {View} */ view) {
		var	/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null;

		// check which buffer to copy
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid;
			tileGrid = this.tileGrid;
		}

		// save to specific snapshot
		this.saveToSnapshot(false, grid, tileGrid, view);
	};

	// restore saved grid
	Life.prototype.restoreSavedGrid = function(/** @type {View} */ view, /** @type {boolean} */ noHistory) {
		// restore the reset snapshot
		this.restoreSnapshot(this.resetSnapshot, view);

		// clear the snapshots
		this.snapshotManager.reset();
		this.nextSnapshotTarget = LifeConstants.snapshotInterval;

		// save reset position as initial snapshot if history enabled
		if (!noHistory) {
			this.saveSnapshot(view);
		}
	};

	// save to a specific snapshot
	Life.prototype.saveToSnapshot = function(/** @type {boolean} */ isReset, /** @type {Array<Uint8Array>} */ grid, /** @type {Array<Uint16Array>} */ tileGrid, /** @type {View} */ view) {
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid;

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// create the snapshot
		this.snapshotManager.saveSnapshot(grid, tileGrid, colourGrid, this.colourTileHistoryGrid, this.overlayGrid, this.colourTileHistoryGrid, this.zoomBox, this.HROTBox, this.population, this.births, this.deaths, this.counter, view.fixedPointCounter, this.counterMargolus, this.maxMargolusGen, ((this.tileCols - 1) >> 4) + 1, this.tileRows, this, isReset);
	};

	// save grid
	Life.prototype.saveGrid = function(/** @type {boolean} */ noHistory, /** @type {View} */ view) {
		// reset snapshot manager
		this.snapshotManager.reset();

		// create reset snapshot
		this.resetSnapshot = this.snapshotManager.createSnapshot(((this.tileCols - 1) >> 4) + 1, this.tileRows, true, this.drawOverlay);

		// save initial position for reset
		this.saveToSnapshot(true, this.grid, this.tileGrid, view);

		// save reset position as initial snapshot if history enabled
		if (!noHistory) {
			this.saveSnapshot(view);
		}
	};

	// reset grid size
	Life.prototype.resetGridSize = function(/** @type {number} */ width, /** @type {number} */ height) {
		// get current grid buffers
		var	/** @type {Array<Uint8Array>} */ currentOverlayGrid = this.overlayGrid,
			/** @type {Array<Uint16Array>} */ currentMaskGrid = this.state6Mask;

		// set width and height
		this.width = width;
		this.height = height;

		// set HROT buffers if used
		if (this.isHROT) {
			this.HROT.resize(this.width, this.height);
		}

		// allocate the new buffers
		this.grid = Array.matrix(Type.Uint8, this.height, ((this.width - 1) >> 3) + 1, 0, this.allocator, "Life.grid", Controller.useWASM);
		this.nextGrid = Array.matrix(Type.Uint8, this.height, ((this.width - 1) >> 3) + 1, 0, this.allocator, "Life.nextGrid", Controller.useWASM);

		// 16bit view of grid and double buffer
		this.grid16 = Array.matrixView(Type.Uint16, this.grid, "Life.grid16");
		this.nextGrid16 = Array.matrixView(Type.Uint16, this.nextGrid, "Life.nextGrid16");

		// check if the mask is allocated
		if (currentMaskGrid) {
			this.state6Mask = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Mask", false);
			this.state6Alive = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Alive", false);
			this.state6Cells = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Cells", false);
		}

		// recompute the number of tile rows and columns
		this.tileCols = this.width >> this.tilePower;
		this.tileRows = this.height >> this.tilePower;

		// allocate the tile grids
		if (currentMaskGrid) {
			this.state6TileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.state6TileGrid", false);
		}
		this.tileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.tileGrid", Controller.useWASM);
		this.nextTileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.nextTileGrid", Controller.useWASM);
		this.colourTileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.colourTileGrid", Controller.useWASM);
		this.colourTileHistoryGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.colourTileHistoryGrid", Controller.useWASM);
		this.diedGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.diedGrid", Controller.useWASM);

		// blank row for 16 bit life grid
		this.blankRow16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.blankRow16", Controller.useWASM));

		// blank tile row to prevent wrap
		this.blankTileRow = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, this.tileCols >> 4, "Life.blankTileRow", Controller.useWASM));

		// blank colour grid row
		this.blankColourRow = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, this.width, "Life.blankColourRow", Controller.useWASM));

		// column occupancy array for grid bounding box calculation
		this.columnOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.columnOccupied16", Controller.useWASM));

		// row occupancy array for grid bounding box calculation
		this.rowOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.height - 1) >> 4) + 1, "Life.rowOccupied16", Controller.useWASM));

		// column occupancy array for grid alive bounding box calculation
		this.columnAliveOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.columnAliveOccupied16", Controller.useWASM));

		// row occupancy array for grid alive bounding box calculation
		this.rowAliveOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.height - 1) >> 4) + 1, "Life.rowAliveOccupied16", Controller.useWASM));

		// colour grid
		this.colourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.colourGrid", Controller.useWASM);
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			this.nextColourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.nextColourGrid", Controller.useWASM);
			this.nextColourGrid16 = Array.matrixView(Type.Uint16, this.nextColourGrid, "Life.nextColourGrid16");
			this.nextColourGrid32 = Array.matrixView(Type.Uint32, this.nextColourGrid, "Life.nextColourGrid32");
		} else {
			this.nextColourGrid = null;
			this.nextColourGrid16 = null;
			this.nextColourGrid32 = null;
		}
		this.smallColourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.smallColourGrid", Controller.useWASM);
		this.colourGrid16 = Array.matrixView(Type.Uint16, this.colourGrid, "Life.colourGrid16");
		this.colourGrid32 = Array.matrixView(Type.Uint32, this.colourGrid, "Life.colourGrid32");

		// check if overlay grid was allocated
		if (currentOverlayGrid) {
			this.overlayGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.overlayGrid", Controller.useWASM);
			this.smallOverlayGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.smallOverlayGrid", Controller.useWASM);
			this.overlayGrid16 = Array.matrixView(Type.Uint16, this.overlayGrid, "Life.overlayGrid16");
			this.overlayGrid32 = Array.matrixView(Type.Uint32, this.overlayGrid, "Life.overlayGrid32");
		}

		// create the grid width and height masks
		this.widthMask = this.width - 1;
		this.heightMask = this.height - 1;

		// update the snapshots
		this.snapshotManager.resizeSnapshots(((this.tileCols - 1) >> 4) + 1, this.tileRows, 0, 0, 0, 0, this.drawOverlay);
	};

	// allocate grid
	Life.prototype.allocateGrid = function(/** @type {number} */ width, /** @type {number} */ height) {
		// allocate new grid
		this.width = width;
		this.height = height;

		// allocate the new buffers
		this.grid = Array.matrix(Type.Uint8, this.height, ((this.width - 1) >> 3) + 1, 0, this.allocator, "Life.grid", Controller.useWASM);
		this.nextGrid = Array.matrix(Type.Uint8, this.height, ((this.width - 1) >> 3) + 1, 0, this.allocator, "Life.nextGrid", Controller.useWASM);

		// 16bit view of grid and double buffer
		this.grid16 = Array.matrixView(Type.Uint16, this.grid, "Life.grid16");
		this.nextGrid16 = Array.matrixView(Type.Uint16, this.nextGrid, "Life.nextGrid16");

		// recompute the number of tile rows and columns
		this.tileCols = this.width >> this.tilePower;
		this.tileRows = this.height >> this.tilePower;

		// create state 6 grids if required
		if (this.state6TileGrid) {
			this.createState6Mask();
		}

		// allocate tile grids
		this.tileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.tileGrid", Controller.useWASM);
		this.nextTileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.nextTileGrid", Controller.useWASM);
		this.colourTileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.colourTileGrid", Controller.useWASM);
		this.colourTileHistoryGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.colourTileHistoryGrid", Controller.useWASM);
		this.diedGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.diedGrid", Controller.useWASM);

		// blank row for 16 bit life grid
		this.blankRow16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.blankRow16", Controller.useWASM));

		// blank tile row to prevent wrap
		this.blankTileRow = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, this.tileCols >> 4, "Life.blankTileRow", Controller.useWASM));

		// blank colour grid row
		this.blankColourRow = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, this.width, "Life.blankColourRow", Controller.useWASM));

		// column occupancy array for grid bounding box calculation
		this.columnOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.columnOccupied16", Controller.useWASM));

		// row occupancy array for grid bounding box calculation
		this.rowOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.height - 1) >> 4) + 1, "Life.rowOccupied16", Controller.useWASM));

		// column occupancy array for grid alive bounding box calculation
		this.columnAliveOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.columnAliveOccupied16", Controller.useWASM));

		// row occupancy array for grid alive bounding box calculation
		this.rowAliveOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.height - 1) >> 4) + 1, "Life.rowAliveOccupied16", Controller.useWASM));

		// colour grid
		this.colourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.colourGrid", Controller.useWASM);
		this.colourGrid16 = Array.matrixView(Type.Uint16, this.colourGrid, "Life.colourGrid16");
		this.colourGrid32 = Array.matrixView(Type.Uint32, this.colourGrid, "Life.colourGrid32");
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			this.nextColourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.nextColourGrid", Controller.useWASM);
			this.nextColourGrid16 = Array.matrixView(Type.Uint16, this.nextColourGrid, "Life.nextColourGrid16");
			this.nextColourGrid32 = Array.matrixView(Type.Uint32, this.nextColourGrid, "Life.nextColourGrid32");
		} else {
			this.nextColourGrid = null;
			this.nextColourGrid16 = null;
			this.nextColourGrid32 = null;
		}
		this.smallColourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.smallColourGrid", Controller.useWASM);

		// check if overlay grid was allocated
		if (this.overlayGrid) {
			this.createOverlay();
		}

		// create the grid width and height masks
		this.widthMask = this.width - 1;
		this.heightMask = this.height - 1;
	};


	// copy data in a grid to the center of a new grid (used by grow grid)
	Life.prototype.copyGridToCenter = function(/** @type {number} */ currentHeight, /** @type {number} */ yOffset, /** @type {number} */ xOffset, /** @type {Array} */ destGrid, /** @type {Array} */ sourceGrid) {
		var	/** @type {number} */ y = 0;

		for (y = 0; y < currentHeight; y += 1) {
			destGrid[y + yOffset].set(sourceGrid[y], xOffset);
		}
	};

	// grow grid
	Life.prototype.growGrid = function(/** @type {boolean} */ growX, /** @type {boolean} */ growY, /** @type {boolean} */ copyContents) {
		// get the current grid size
		var	/** @type {number} */ currentWidth = this.width,
			/** @type {number} */ currentHeight = this.height,

			// get current grid buffers
			/** @type {Array<Uint8Array>} */ currentGrid = this.grid,
			/** @type {Array<Uint8Array>} */ currentNextGrid = this.nextGrid,
			/** @type {Array<Uint8Array>} */ currentColourGrid = this.colourGrid,
			/** @type {Array<Uint8Array>} */ currentNextColourGrid = this.nextColourGrid,
			/** @type {Array<Uint8Array>} */ currentOverlayGrid = this.overlayGrid,
			/** @type {Array<Uint16Array>} */ currentMaskGrid = this.state6Mask,
			/** @type {Array<Uint16Array>} */ currentMaskAliveGrid = this.state6Alive,
			/** @type {Array<Uint16Array>} */ currentMaskCellsGrid = this.state6Cells,
			/** @type {Array<Uint8Array>} */ currentCountList = this.countList,
			/** @type {Array<Uint8Array>} */ currentInitList = this.initList,

			// get current tile buffers
			/** @type {Array<Uint16Array>} */ currentMaskTileGrid = this.state6TileGrid,
			/** @type {Array<Uint16Array>} */ currentTileGrid = this.tileGrid,
			/** @type {Array<Uint16Array>} */ currentNextTileGrid = this.nextTileGrid,
			/** @type {Array<Uint16Array>} */ currentColourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ currentColourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ currentDiedGrid = this.diedGrid,
			/** @type {Array<Uint16Array>} */ currentOccTileMap = this.occTileMap,
			/** @type {Array<Uint16Array>} */ currentOccMergedTileMap = this.occMergedTileMap,

			// current tile height
			/** @type {number} */ currentTileHeight = this.tileRows,

			// x and y offsets
			/** @type {number} */ xOffset = 0,
			/** @type {number} */ yOffset = 0,
			/** @type {number} */ xOffsetTile = 0,
			/** @type {number} */ yOffsetTile = 0,

			// identify bottom left
			/** @type {number} */ idCurrent = 0,
			/** @type {number} */ idBottom = 0,
			/** @type {number} */ idLeft = 0,

			// row number
			/** @type {number} */ y = 0;

		// check if already at maximum size or just allocating
		if (currentWidth < this.maxGridSize || currentHeight < this.maxGridSize || !copyContents) {
			// double the size
			if (growX) {
				xOffset = this.width >> 1;
				this.width *= 2;
			}
			if (growY) {
				yOffset = this.height >> 1;
				this.height *= 2;
			}

			// grow HROT buffers if used
			if (this.isHROT) {
				this.HROT.resize(this.width, this.height);
			}

			// allocate the new buffers
			this.grid = Array.matrix(Type.Uint8, this.height, ((this.width - 1) >> 3) + 1, 0, this.allocator, "Life.grid", Controller.useWASM);
			this.nextGrid = Array.matrix(Type.Uint8, this.height, ((this.width - 1) >> 3) + 1, 0, this.allocator, "Life.nextGrid", Controller.useWASM);

			// 16bit view of grid and double buffer
			this.grid16 = Array.matrixView(Type.Uint16, this.grid, "Life.grid16");
			this.nextGrid16 = Array.matrixView(Type.Uint16, this.nextGrid, "Life.nextGrid16");

			// check if the mask is allocated
			if (currentMaskGrid) {
				this.state6Mask = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Mask", false);
				this.state6Alive = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Alive", false);
				this.state6Cells = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Cells", false);
			}

			// recompute the number of tile rows and columns
			this.tileCols = this.width >> this.tilePower;
			this.tileRows = this.height >> this.tilePower;

			// allocate the tile grids
			if (currentMaskGrid) {
				this.state6TileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.state6TileGrid", false);
			}
			this.tileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.tileGrid", Controller.useWASM);
			this.nextTileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.nextTileGrid", Controller.useWASM);
			this.colourTileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.colourTileGrid", Controller.useWASM);
			this.colourTileHistoryGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.colourTileHistoryGrid", Controller.useWASM);
			this.diedGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.diedGrid", Controller.useWASM);

			if (currentOccTileMap) {
				this.occTileMap = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.occTileMap", false);
			}
			if (currentOccMergedTileMap) {
				this.occMergedTileMap = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.occMergedTileMap", false);
			}

			// blank row for 16 bit life grid
			this.blankRow16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.blankRow16", Controller.useWASM));

			// blank tile row to prevent wrap
			this.blankTileRow = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, this.tileCols >> 4, "Life.blankTileRow", Controller.useWASM));

			// blank colour grid row
			this.blankColourRow = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, this.width, "Life.blankColourRow", Controller.useWASM));

			// column occupancy array for grid bounding box calculation
			this.columnOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.columnOccupied16", Controller.useWASM));

			// row occupancy array for grid bounding box calculation
			this.rowOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.height - 1) >> 4) + 1, "Life.rowOccupied16", Controller.useWASM));

			// column occupancy array for grid alive bounding box calculation
			this.columnAliveOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.width - 1) >> 4) + 1, "Life.columnAliveOccupied16", Controller.useWASM));

			// row occupancy array for grid alive bounding box calculation
			this.rowAliveOccupied16 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, ((this.height - 1) >> 4) + 1, "Life.rowAliveOccupied16", Controller.useWASM));

			// count grid
			if (currentCountList) {
				this.countList = Array.matrix(Type.Uint8, this.height, this.width, LifeConstants.cellWasDead, this.allocator, "Life.countList", false);
			}

			// init grid
			if (currentInitList) {
				this.initList = Array.matrix(Type.Uint8, this.height, this.width, 0, this.allocator, "Life.initList", false);
			}

			// colour grid
			this.colourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.colourGrid", Controller.useWASM);
			this.smallColourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.smallColourGrid", Controller.useWASM);
			this.colourGrid16 = Array.matrixView(Type.Uint16, this.colourGrid, "Life.colourGrid16");
			this.colourGrid32 = Array.matrixView(Type.Uint32, this.colourGrid, "Life.colourGrid32");
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				this.nextColourGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.nextColourGrid", Controller.useWASM);
				this.nextColourGrid16 = Array.matrixView(Type.Uint16, this.nextColourGrid, "Life.nextColourGrid16");
				this.nextColourGrid32 = Array.matrixView(Type.Uint32, this.nextColourGrid, "Life.nextColourGrid32");
			} else {
				this.nextColourGrid = null;
				this.nextColourGrid16 = null;
				this.nextColourGrid32 = null;
			}

			// check if overlay grid was allocated
			if (currentOverlayGrid) {
				this.overlayGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.overlayGrid", Controller.useWASM);
				this.smallOverlayGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.smallOverlayGrid", Controller.useWASM);
				this.overlayGrid16 = Array.matrixView(Type.Uint16, this.overlayGrid, "Life.overlayGrid16");
				this.overlayGrid32 = Array.matrixView(Type.Uint32, this.overlayGrid, "Life.overlayGrid32");
			}

			// create the grid width and height masks
			this.widthMask = this.width - 1;
			this.heightMask = this.height - 1;

			// copy the old grids to the center of the new ones if required
			if (copyContents) {
				this.copyGridToCenter(currentHeight, yOffset, xOffset >> 3, this.grid, currentGrid);
				this.copyGridToCenter(currentHeight, yOffset, xOffset >> 3, this.nextGrid, currentNextGrid);
				this.copyGridToCenter(currentHeight, yOffset, xOffset, this.colourGrid, currentColourGrid);
				// ignore small colour grid it will be generated next time it is needed

				if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
					this.copyGridToCenter(currentHeight, yOffset, xOffset, this.nextColourGrid, currentNextColourGrid);
				}
				if (this.countList) {
					this.copyGridToCenter(currentHeight, yOffset, xOffset, this.countList, currentCountList);
				}
				if (this.initList) {
					this.copyGridToCenter(currentHeight, yOffset, xOffset, this.initList, currentInitList);
				}
				if (currentOverlayGrid) {
					this.copyGridToCenter(currentHeight, yOffset, xOffset, this.overlayGrid, currentOverlayGrid);
					// ignore small overlay grid it will be generated next time it is needed
				}

				if (currentMaskGrid && currentMaskAliveGrid && currentMaskCellsGrid) {
					this.copyGridToCenter(currentHeight, yOffset, xOffset >> 4, this.state6Mask, currentMaskGrid);
					this.copyGridToCenter(currentHeight, yOffset, xOffset >> 4, this.state6Alive, currentMaskAliveGrid);
					this.copyGridToCenter(currentHeight, yOffset, xOffset >> 4, this.state6Cells, currentMaskCellsGrid);
				}

				// copy the old tile grids to the center of the new ones
				if (growY) {
					yOffsetTile = currentTileHeight >> 1;
				}
				if (growX) {
					xOffsetTile = this.tileGrid[0].length >> 2;
				}

				if (currentMaskTileGrid) {
					this.copyGridToCenter(currentTileHeight, yOffsetTile, xOffsetTile, this.state6TileGrid, currentMaskTileGrid);
				}
				this.copyGridToCenter(currentTileHeight, yOffsetTile, xOffsetTile, this.tileGrid, currentTileGrid);
				this.copyGridToCenter(currentTileHeight, yOffsetTile, xOffsetTile, this.nextTileGrid, currentNextTileGrid);
				this.copyGridToCenter(currentTileHeight, yOffsetTile, xOffsetTile, this.colourTileGrid, currentColourTileGrid);
				this.copyGridToCenter(currentTileHeight, yOffsetTile, xOffsetTile, this.colourTileHistoryGrid, currentColourTileHistoryGrid);
				this.copyGridToCenter(currentTileHeight, yOffsetTile, xOffsetTile, this.diedGrid, currentDiedGrid);
			}

			// update the snapshots
			this.snapshotManager.resizeSnapshots(((this.tileCols - 1) >> 4) + 1, this.tileRows, xOffsetTile, yOffsetTile, xOffset, yOffset, this.drawOverlay);

			// update camera x and y
			this.xOff += xOffset;
			this.yOff += yOffset;

			// check for hex rule
			if (this.isHex) {
				this.xOff -= (yOffset / 2 | 0);
			}

			// update bounding boxes
			this.zoomBox.leftX += xOffset;
			this.zoomBox.rightX += xOffset;
			this.zoomBox.topY += yOffset;
			this.zoomBox.bottomY += yOffset;

			this.HROTBox.leftX += xOffset;
			this.HROTBox.rightX += xOffset;
			this.HROTBox.topY += yOffset;
			this.HROTBox.bottomY += yOffset;

			this.initialBox.leftX += xOffset;
			this.initialBox.rightX += xOffset;
			this.initialBox.topY += yOffset;
			this.initialBox.bottomY += yOffset;

			this.historyBox.leftX += xOffset;
			this.historyBox.rightX += xOffset;
			this.historyBox.topY += yOffset;
			this.historyBox.bottomY += yOffset;

			// update identify positions
			if (this.oscLength > 0) {
				for (y = 0; y < this.oscLength; y += 1) {
					idCurrent = this.negBoxList[(y << 1) + 1];
					idLeft = (idCurrent >> 16) + xOffset;
					idBottom = (idCurrent & 65535) + yOffset;
					this.negBoxList[(y << 1) + 1] = (idLeft << 16) | idBottom;
				}

				for (y = 0; y < this.fullOscLength; y += 1) {
					idCurrent = this.boxList[(y << 1) + 1];
					idLeft = (idCurrent >> 16) + xOffset;
					idBottom = (idCurrent & 65535) + yOffset;
					this.boxList[(y << 1) + 1] = (idLeft << 16) | idBottom;
				}
			}
		}
	};

	// check if the grid buffer needs to grow for one step
	/** @returns {boolean} */
	Life.prototype.checkForGrowthStep = function(/** @type {View} */ view, /** @type {BoundingBox} */ box, /** @type {number} */ maxStep) {
	    // get the current grid width and height
		var	/** @type {number} */ width = this.width,
			/** @type {number} */ height = this.height,

			// growth direction
			/** @type {boolean} */ growX = false,
			/** @type {boolean} */ growY = false,

			// whether the buffer grew
			/** @type {boolean} */ result = false;

		// check if already at maximum size
		if ((width < this.maxGridSize || height < this.maxGridSize)) {
			// check bounding box
			if (width < this.maxGridSize && (box.leftX <= maxStep || box.rightX >= (width - maxStep))) {
				growX = true;
			}
			if (height < this.maxGridSize && (box.bottomY <= maxStep || box.topY >= (height - maxStep))) {
				growY = true;
			}
			if (growX || growY) {
				// compute the growth direction
				// grow the grid
				this.growGrid(growX, growY, true);
				result = true;

				// update the default x and y
				if (growX) {
					view.defaultX += this.width >> 2;
					view.savedX += this.width >> 2;
				}

				if (growY) {
					view.defaultY += this.height >> 2;
					view.savedY += this.height >> 2;
				}

				// check for hex mode
				if (view.engine.isHex) {
					if (growY) {
						view.defaultX -= this.height >> 3;
						view.savedX -= this.height >> 3;
					}
				}

				// update pan position
				if (growX) {
					view.panX += this.width >> 2;
				}
				if (growY) {
					view.panY += this.height >> 2;
				}

				// update the box position
				if (growX) {
					box.leftX += this.width >> 2;
					box.rightX += this.width >> 2;
				}

				if (growY) {
					box.bottomY += this.height >> 2;
					box.topY += this.height >> 2;
				}
			}
		}

		// return whether the buffer grew
		return result;
	};

	// check if the grid buffer needs to grow for as many steps as needed
	Life.prototype.checkForGrowth = function(/** @type {View} */ view, /** @type {BoundingBox} */ box, /** @type {number} */ borderSize) {
		// divide border by 2 since it includes space both sides of the pattern
		borderSize = (borderSize + 1) >> 1;

		// check the next step
		while (this.checkForGrowthStep(view, box, borderSize)) {
			// until no more steps required
		}
	};

	// initialise colour reset
	Life.prototype.initColourReset = function() {
		var	/** @type {number} */ h,
			/** @type {number} */ b,
			/** @type {Uint8Array} */ colourReset = this.colourReset,
			/** @type {number} */ aliveStart = this.aliveStart;

		// create the 8 colours for each of the 8 bit entries
		for (h = 0; h < 256; h += 1) {
			for (b = 0; b < 8; b += 1) {
				colourReset[(h << 3) + b] = (h & (1 << (7 - b))) ? aliveStart : 0;
			}
		}
	};

	// reset population for grid region
	Life.prototype.resetPopulationBox = function(/** @type {Array<Uint16Array>} */ grid16, /** @type {Array<Uint8Array>} */ colourGrid) {
		if (Controller.useWASM && Controller.wasmEnableResetPopulation && this.view.wasmEnabled) {
			// check for multi-state rules
			if (this.isHROT || this.isPCA || this.isRuleTree || this.isExtended || this.isNone) {
				this.resetPopulationBoxJS(grid16, colourGrid);
			} else {
				this.population = WASM.resetPopulationBit(
					grid16.whole.byteOffset | 0,
					grid16[0].length | 0,
					this.zoomBox.leftX | 0,
					this.zoomBox.bottomY | 0,
					this.zoomBox.rightX | 0,
					this.zoomBox.topY | 0
				);

				this.birth = 0;
				this.deaths = 0;
			}
		} else {
			this.resetPopulationBoxJS(grid16, colourGrid);
		}
	};

	// reset population for grid region - Javascript version
	Life.prototype.resetPopulationBoxJS = function(/** @type {Array<Uint16Array>} */ grid16, /** @type {Array<Uint8Array>} */ colourGrid) {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {Uint8Array} */ nextColourRow = null,
			/** @type {Uint16Array} */ nextRow = null,
			/** @type {number} */ population = 0,
			/** @type {number} */ count = 0,
			/** @type {Uint8Array} */ bitCounts16 = this.bitCounts16,
			/** @type {number} */ state = 0,
			/** @type {number} */ aliveState = 0,

			// get grid bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ leftX = zoomBox.leftX,
			/** @type {number} */ rightX = zoomBox.rightX,
			/** @type {number} */ topY = zoomBox.topY,
			/** @type {number} */ bottomY = zoomBox.bottomY;

		// get HROT alive state
		if (this.isHROT) {
			if (this.multiNumStates > 2) {
				aliveState = this.historyStates + this.multiNumStates - 1;
			} else {
				aliveState = LifeConstants.aliveStart;
			}
		}

		// check for multi-state rules
		if (this.isHROT || this.isPCA || this.isRuleTree || this.isExtended || this.isNone) {
			// compute population from colour grid
			for (h = bottomY; h <= topY; h += 1) {
				// get next row
				nextColourRow = colourGrid[h];

				// count population along the row
				for (w = leftX; w <= rightX; w += 1) {
					state = nextColourRow[w];
					if (state > 0) {
						if (this.isPCA) {
							population += bitCounts16[state - this.historyStates];
						} else {
							if (this.isHROT) {
								// HROT state 1 is alive, other states are dying and ignored for the count
								if (state === aliveState) {
									population += 1;
								}
							} else {
								// Extended
								if (this.isExtended) {
									if (state > 0) {
										population += 1;
									}
								} else {
									population += 1;
								}
							}
						}
					}
				}
			}
		} else {
			// compute population from bit grid
			leftX >>= 4;
			rightX >>= 4;
			for (h = bottomY; h <= topY; h += 1) {
				// get next row
				nextRow = grid16[h];

				// count population along row
				for (w = leftX; w <= rightX; w += 1) {
					count = bitCounts16[nextRow[w]];
					population += count;
				}
			}
		}

		// save statistics
		this.population = population;
		this.births = 0;
		this.deaths = 0;
	};

	// resize life display
	Life.prototype.resizeDisplay = function(/** @type {number} */ displayWidth, /** @type {number} */ displayHeight) {
		var	/** @type {CanvasRenderingContext2D} */ context = this.context,
			/** @type {number} */ pixelColour = 0,
			/** @type {number} */ i = 0,
			/** @type {boolean} */ sizeChanged = false;

		// set the black pixel colour
		if (this.littleEndian) {
			pixelColour = 0xff000000;
		} else {
			pixelColour = 0x000000ff;
		}

		// check if the display has actually changed size
		if (this.displayWidth !== displayWidth || this.displayHeight !== displayHeight) {
			sizeChanged = true;
		}

		// save the display width and height
		this.displayWidth = displayWidth;
		this.displayHeight = displayHeight;

		// clear the old buffers
		this.imageData = null;
		this.data32 = null;
		this.data8 = null;
		this.mode7Buffer = null;

		if (Controller.useWASM) {
			// reset top of memory pointer since screen related data will be at top of memory coming down to handle resizes
			this.allocator.resetTop();
		}

		// update the drawing context
		const /** @type {Uint8ClampedArray} */ buffer = /** @type {Uint8ClampedArray} */ (this.allocator.allocateTop(Type.Uint8Clamped, context.canvas.width * context.canvas.height * 4, "Life.imageData", Controller.useWASM));
		/** @suppress {checkTypes} */
		this.imageData = new ImageData(/** @type {Uint8ClampedArray<ArrayBuffer>} */ (buffer), context.canvas.width, context.canvas.height);

		if (Controller.useWASM) {
			this.xOffsets = /** @type {!Uint32Array}*/ (this.allocator.allocateTop(Type.Uint32, this.displayWidth, "Life.xOffsets", Controller.useWASM));
			this.xMaxOffsets = /** @type {!Uint32Array}*/ (this.allocator.allocateTop(Type.Uint32, this.displayWidth, "Life.xMaxOffsets", Controller.useWASM));
		}

		// check if buffer is available
		if (this.imageData.data.buffer) {
			// create 32 bit view over the image data buffer
			this.data32 = new Uint32Array(this.imageData.data.buffer, this.imageData.data.byteOffset, context.canvas.width * context.canvas.height);
		} else {
			// buffer not available so create buffer to copy
			this.data32 = new Uint32Array(this.imageData.data.length >> 2);

			// create an 8bit view of the buffer
			this.data8 = new Uint8Array(this.data32.buffer);
		}

		if (sizeChanged) {
			// clear pixels
			this.clearPixels(pixelColour);

			// resize the scale canvas to double the drawing canvas plus one zoom cell
			this.initPretty();

			// resize the icon drawing canvas if it exists
			if (this.iconCanvas !== null) {
				// icon canvas is display width plus one zoom cell
				this.iconCanvas.width = this.displayWidth + this.view.maxZoom;
				this.iconCanvas.height = this.displayHeight + this.view.maxZoom;

				this.iconContext = /** @type {!CanvasRenderingContext2D} */ (this.iconCanvas.getContext("2d", {alpha: false}));
				this.iconImageData = this.iconContext.createImageData(this.iconCanvas.width, this.iconCanvas.height);
			}
		}
	};

	// copy state 2 cells to colour grid
	Life.prototype.copyState2 = function(/** @type {Pattern} */ pattern, /** @type {number} */ panX, /** @type {number} */ panY) {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,

			// colour grid and row
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourGridRow = null,

			// pattern width and height
			/** @type {number} */ width = pattern.width,
			/** @type {number} */ height = pattern.height,

			// width and height masks
			/** @type {number} */ wm = this.widthMask,
			/** @type {number} */ hm = this.heightMask,

			// multi-state view
			/** @type {Uint8Array} */ multiStateRow = null,

			// starting dead colour
			/** @type {number} */ deadMin = this.deadMin,

			// translated state 2 value
			/** @type {number} */ state2 = ViewConstants.stateMap[2];

		// copy LifeHistory state 2 to colour grid
		for (y = 0; y < height; y += 1) {
			multiStateRow = pattern.multiStateMap[y];
			colourGridRow = colourGrid[(y + panY) & hm];

			// check row
			for (x = 0; x < width; x += 1) {
				// check for state 2
				if (multiStateRow[x] === state2) {
					// set the colour grid to the dead start colour
					colourGridRow[(x + panX) & wm] = deadMin;
				}
			}
		}
	};

	// free the overlay
	Life.prototype.freeOverlay = function() {
		this.overlayGrid = null;
		this.smallOverlayGrid = null;
		this.overlayGrid16 = null;
		this.overlayGrid32 = null;
	};

	// create the overlay
	Life.prototype.createOverlay = function() {
		this.overlayGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.overlayGrid", Controller.useWASM);
		this.smallOverlayGrid = Array.matrix(Type.Uint8, this.height, this.width, this.unoccupied, this.allocator, "Life.smallOverlayGrid", Controller.useWASM);
		this.overlayGrid16 = Array.matrixView(Type.Uint16, this.overlayGrid, "Life.overlayGrid16");
		this.overlayGrid32 = Array.matrixView(Type.Uint32, this.overlayGrid, "Life.overlayGrid32");
	};

	// free the state6 mask
	Life.prototype.freeState6Mask = function() {
		this.state6Mask = null;
		this.state6Alive = null;
		this.state6Cells = null;
		this.state6TileGrid = null;
	};

	// create the state6 mask
	Life.prototype.createState6Mask = function() {
		// allocate the mask and alive mask
		this.state6Mask = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Mask", false);
		this.state6Alive = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Alive", false);
		this.state6Cells = Array.matrix(Type.Uint16, this.height, ((this.width - 1) >> 4) + 1, 0, this.allocator, "Life.state6Cells", false);
		this.state6TileGrid = Array.matrix(Type.Uint16, this.tileRows, ((this.tileCols - 1) >> 4) + 1, 0, this.allocator, "Life.state6TileGrid", false);
	};

	// populate the state6 mask from the colour grid and update the state 6 bounding box
	Life.prototype.populateState6MaskFromColGrid = function() {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,

			// mask rows
			/** @type {Uint16Array} */ maskRow0 = null,
			/** @type {Uint16Array} */ maskRow1 = null,
			/** @type {Uint16Array} */ maskRow2 = null,
			/** @type {Uint16Array} */ cellsRow = null,

			// width and height masks
			/** @type {number} */ wm = this.widthMask,
			/** @type {number} */ hm = this.heightMask,

			// multi-state view
			/** @type {Uint8Array} */ overlayRow = null,

			// bit offset
			/** @type {number} */ offset = 0,

			// tile grid
			/** @type {Array<Uint16Array>} */ tileGrid = this.state6TileGrid,
			/** @type {Uint16Array} */ tileRow0 = null,
			/** @type {Uint16Array} */ tileRow1 = null,
			/** @type {Uint16Array} */ tileRow2 = null,

			// tile size (2^n)
			/** @type {number} */ tilePower = this.tilePower,

			// state 6 value in colour grid
			/** @type {number} */ state6 = this.isSuper ? 6 : ViewConstants.stateMap[6] + 128,

			// grid
			/** @type {Array<Uint8Array>} */ grid = this.isSuper ? this.colourGrid : this.overlayGrid,

			// state 6 bounding box
			/** @type {BoundingBox} */ state6Box = this.state6Box,
			/** @type {number} */ minX = this.width,
			/** @type {number} */ maxX = -1,
			/** @type {number} */ minY = this.height,
			/** @type {number} */ maxY = -1;

		// clear current mask and cells
		if (this.isLifeHistory) {
			// [R]History version
			this.state6Mask.whole.fill(0);
			this.state6Cells.whole.fill(0);
			this.state6Alive.whole.fill(0);

			// clear state6 tile grid
			this.state6TileGrid.whole.fill(0);

			// remove bits from the mask that are state 6 in the pattern
			for (y = 0; y < this.height; y += 1) {
				// get the rows
				overlayRow = grid[y];

				if (y > 0) {
					maskRow0 = this.state6Mask[(y - 1) & hm];
					tileRow0 = tileGrid[((y - 1) & hm) >> tilePower];
				} else {
					maskRow0 = null;
					tileRow0 = null;
				}

				maskRow1 = this.state6Mask[y & hm];
				tileRow1 = tileGrid[(y & hm) >> tilePower];
				cellsRow = this.state6Cells[y & hm];

				if (y < this.height - 1) {
					maskRow2 = this.state6Mask[(y + 1) & hm];
					tileRow2 = tileGrid[((y + 1) & hm) >> tilePower];
				} else {
					maskRow2 = null;
					tileRow2 = null;
				}

				// check row
				for (x = 0; x < this.width; x += 1) {
					// check for state 6
					if (overlayRow[x] === state6) {
						// update min and max
						if (x < minX) {
							minX = x;
						}
						if (x > maxX) {
							maxX = x;
						}
						if (y < minY) {
							minY = y;
						}
						if (y > maxY) {
							maxY = y;
						}

						// set the cell position itself
						offset = x & wm;
						cellsRow[offset >> 4] |= (1 << (~offset & 15));

						// set the cells around the state 6 cell in the mask
						// middle column
						if (maskRow0) {
							maskRow0[offset >> 4] |= (1 << (~offset & 15));
							tileRow0[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);
						}

						maskRow1[offset >> 4] |= (1 << (~offset & 15));
						tileRow1[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);

						if (maskRow2) {
							maskRow2[offset >> 4] |= (1 << (~offset & 15));
							tileRow2[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);
						}

						// left column
						if (x > 0) {
							offset = (x - 1) & wm;
							if (!this.isVonNeumann) {
								if (maskRow0) {
									maskRow0[offset >> 4] |= (1 << (~offset & 15));
									tileRow0[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);
								}
							}

							maskRow1[offset >> 4] |= (1 << (~offset & 15));
							tileRow1[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);

							if (!(this.isHex || this.isVonNeumann)) {
								if (maskRow2) {
									maskRow2[offset >> 4] |= (1 << (~offset & 15));
									tileRow2[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);
								}
							}
						}

						// right column
						if (x < this.width - 1) {
							offset = (x + 1) & wm;
							if (!(this.isHex || this.isVonNeumann)) {
								if (maskRow0) {
									maskRow0[offset >> 4] |= (1 << (~offset & 15));
									tileRow0[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);
								}
							}

							maskRow1[offset >> 4] |= (1 << (~offset & 15));
							tileRow1[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);

							if (!this.isVonNeumann) {
								if (maskRow2) {
									maskRow2[offset >> 4] |= (1 << (~offset & 15));
									tileRow2[(offset >> (tilePower + tilePower))] |= 1 << (~(offset >> tilePower) & 15);
								}
							}
						}
					}
				}
			}
		} else {
			// [R]Super version
			for (y = 0; y < this.height; y += 1) {
				overlayRow = grid[y];
				for (x = 0; x < this.width; x += 1) {
					if (overlayRow[x] === 6) {
						// update min and max
						if (x < minX) {
							minX = x;
						}
						if (x > maxX) {
							maxX = x;
						}
						if (y < minY) {
							minY = y;
						}
						if (y > maxY) {
							maxY = y;
						}
					}
				}
			}
		}

		// save the state 6 bounding box
		state6Box.leftX = minX;
		state6Box.bottomY = minY;
		state6Box.rightX = maxX;
		state6Box.topY = maxY;
	};

	// reset the colour grid from the grid
	Life.prototype.resetColourGridBox = function(/** @type {Array<Uint16Array>} */ grid) {
		if (!(this.isPCA || this.isRuleTree || this.isHROT)) {
			switch (this.cellRenderer) {
				case LifeConstants.shaderCellAge:
					if (Controller.useWASM && Controller.wasmEnableResetColourGrid && this.view.wasmEnabled) {
						WASM.resetColourGridNormal(
							grid.whole.byteOffset | 0,
							grid[0].length | 0,
							this.colourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							LifeConstants.aliveStart | 0,
							this.zoomBox.leftX | 0,
							this.zoomBox.bottomY | 0,
							this.zoomBox.rightX | 0,
							this.zoomBox.topY | 0
						);
					} else {
						this.resetColourGridBoxNormal(grid);
					}
					break;

				case LifeConstants.shaderRainbow:
					this.resetColourGridBoxRainbow(grid);
					break;

				case LifeConstants.shaderNeighbourCount:
					this.resetColourGridBoxNormal(grid);
					this.convertToPensTileNeighbourCount();
					break;

				case LifeConstants.shaderBasic:
					this.resetColourGridBoxNormal(grid);
					break;
			}
		}
	};

	// set the colour grid from the grid using rainbow
	Life.prototype.resetColourGridBoxRainbow = function(/** @type {Array<Uint16Array>} */ grid) {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ cr = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {number} */ cells = 0,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Uint8Array} */ colourRow = null,

			// get the grid bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ leftX = zoomBox.leftX >> 4,
			/** @type {number} */ rightX = zoomBox.rightX >> 4,
			/** @type {number} */ topY = zoomBox.topY,
			/** @type {number} */ bottomY = zoomBox.bottomY;

		// set the colour grid from the grid
		for (y = bottomY; y <= topY; y += 1) {
			gridRow = grid[y];
			colourRow = colourGrid[y];
			cr = (leftX << 4);
			for (x = leftX; x <= rightX; x += 1) {
				// get first 8 bits
				cells = (gridRow[x] >> 8);
				if (cells & 128) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 64) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 32) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 16) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 8) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 4) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 2) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 1) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				// get second 8 bits
				cells = gridRow[x] & 255;
				if (cells & 128) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 64) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 32) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 16) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 8) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 4) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 2) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
				if (cells & 1) {
					colourRow[cr] = ((y + cr) & 127) + 64;
				} else {
					colourRow[cr] = 0;
				}
				cr += 1;
			}
		}
	};

	// set the colour grid from the grid
	Life.prototype.resetColourGridBoxNormal = function(/** @type {Array<Uint16Array>} */ grid) {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ pixels = 0,
			/** @type {Array<Uint32Array>} */ colourGrid32 = this.colourGrid32,
			/** @type {Uint32Array} */ colourReset32 = new Uint32Array(this.colourReset.buffer, this.colourReset.byteOffset, this.colourReset.length / 4),
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Uint32Array} */ colourRow32 = null,
			/** @type {number} */ rowOffset = 0,

			// get the grid bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ leftX = zoomBox.leftX >> 4,
			/** @type {number} */ rightX = zoomBox.rightX >> 4,
			/** @type {number} */ topY = zoomBox.topY,
			/** @type {number} */ bottomY = zoomBox.bottomY;

		// ignore for PCA, RuleTree and HROT rules
		if (!(this.isPCA || this.isRuleTree || this.isHROT)) {
			// set the colour grid from the grid
			for (y = bottomY; y <= topY; y += 1) {
				gridRow = grid[y];
				colourRow32 = colourGrid32[y];

				//cr = (leftX << 4);
				cr = (leftX << 2);
				for (x = leftX; x <= rightX; x += 1) {
					pixels = gridRow[x];
					if (pixels) {
						// get first 8 bits
						rowOffset = (pixels >> 8) << 1;

						// copy 8 pixels
						colourRow32[cr] = colourReset32[rowOffset];
						cr += 1;
						colourRow32[cr] = colourReset32[rowOffset + 1];
						cr += 1;

						// get second 8 bits
						rowOffset = (pixels & 255) << 1;

						// copy 8 pixels
						colourRow32[cr] = colourReset32[rowOffset];
						cr += 1;
						colourRow32[cr] = colourReset32[rowOffset + 1];
						cr += 1;
					} else {
						colourRow32[cr] = 0;
						cr += 1;
						colourRow32[cr] = 0;
						cr += 1;
						colourRow32[cr] = 0;
						cr += 1;
						colourRow32[cr] = 0;
						cr += 1;
					}
				}
			}
		}
	};

	// create 7x7 glider
	/** @returns {Array<Array<number>>} */
	Life.prototype.create7x7Glider = function(/** @type {Array<Array<number>>} */ glider3x3) {
		var	/** @type {Array<Array<number>>} */ result = [],
			/** @type {Array<number>} */ row = null,
			/** @type {number} */ i = 0,
			/** @type {number} */ j = 0;

		// create the empty 7x7 matrix
		for (j = 0; j < 7; j += 1) {
			row = [];
			for (i = 0; i < 7; i += 1) {
				row[i] = 0;
			}
			result[j] = row;
		}

		// fill the middle 3x3 from the glider
		for (j = 0; j < 3; j += 1) {
			for (i = 0; i < 3; i += 1) {
				result[j + 2][i + 2] = glider3x3[j][i];
			}
		}

		return result;
	};

	// create 7x7 gliders for glider detection and removal
	Life.prototype.create7x7Gliders = function() {
		this.gliderNW07x7 = this.create7x7Glider(LifeConstants.gliderNW0);
		this.gliderNW17x7 = this.create7x7Glider(LifeConstants.gliderNW1);
		this.gliderNW27x7 = this.create7x7Glider(LifeConstants.gliderNW2);
		this.gliderNW37x7 = this.create7x7Glider(LifeConstants.gliderNW3);
		this.gliderNE07x7 = this.create7x7Glider(LifeConstants.gliderNE0);
		this.gliderNE17x7 = this.create7x7Glider(LifeConstants.gliderNE1);
		this.gliderNE27x7 = this.create7x7Glider(LifeConstants.gliderNE2);
		this.gliderNE37x7 = this.create7x7Glider(LifeConstants.gliderNE3);
		this.gliderSW07x7 = this.create7x7Glider(LifeConstants.gliderSW0);
		this.gliderSW17x7 = this.create7x7Glider(LifeConstants.gliderSW1);
		this.gliderSW27x7 = this.create7x7Glider(LifeConstants.gliderSW2);
		this.gliderSW37x7 = this.create7x7Glider(LifeConstants.gliderSW3);
		this.gliderSE07x7 = this.create7x7Glider(LifeConstants.gliderSE0);
		this.gliderSE17x7 = this.create7x7Glider(LifeConstants.gliderSE1);
		this.gliderSE27x7 = this.create7x7Glider(LifeConstants.gliderSE2);
		this.gliderSE37x7 = this.create7x7Glider(LifeConstants.gliderSE3);
	};

	// initialise life engine
	Life.prototype.initEngine = function(/** @type {CanvasRenderingContext2D} */ context, /** @type {number} */ displayWidth, /** @type {number} */ displayHeight) {
		var	/** @type {number} */ unoccupied = this.unoccupied,
			/** @type {Uint16Array} */ blankRow16 = this.blankRow16,
			/** @type {Uint8Array} */ blankColourRow = this.blankColourRow,
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow;

		// disable overlay
		this.drawOverlay = false;

		// create the grid width and height masks
		this.widthMask = this.width - 1;
		this.heightMask = this.height - 1;

		// initialise the bounding boxes
		this.zoomBox = new BoundingBox(0, 0, this.width - 1, this.height - 1);

		// initial bounding box for Track speed
		this.initialBox = new BoundingBox(0, 0, this.width - 1, this.height - 1);

		// initial bounding box for HROT alive cells
		this.HROTBox = new BoundingBox(0, 0, this.width - 1, this.height - 1);

		// Identify box
		this.identifyBox = new BoundingBox(0, 0, this.width - 1, this.height - 1);

		// state 6 [R]History box
		this.state6Box = new BoundingBox(this.width, this.height, -1, -1);

		// bounding box for history autofit
		this.historyBox = new BoundingBox(0, 0, this.width - 1, this.height - 1);

		// save drawing context
		this.context = context;

		// initialise colour reset
		this.initColourReset();

		// resize display
		this.resizeDisplay(displayWidth, displayHeight);

		// create the blank rows
		//blankRow16.fill(0);
		//blankColourRow.fill(unoccupied);
		//blankTileRow.fill(0);

		// create the 7x7 gliders
		this.create7x7Gliders();
	};

	// create the colour themes
	Life.prototype.createColourThemes = function() {
		var	/** @type {number} */ i = 0;

		// parameter order is:  (2-state)      deadRamp, dead, alive, aliveRamp, unoccupied
		//				(generations)  alive, dyingRamp, dying, deadRamp, dead, unoccupied
		//				(PCA)          pcaCols[N, E, NE, S, NS, ES, NES, W, NW, EW, NEW, SW, NSW, ESW, NESW]

		// monochrome
		this.themes[i] = new Theme("Mono", new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)), new Colour(0, 0, 0),
							new Colour(255, 255, 255), new ColourRange(new Colour(64, 64, 64), new Colour(-1, -1, -1)), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new Colour(0, 0, 0),
							[new Colour(64, 64, 64), new Colour(64, 64, 64), new Colour(128, 128, 128), new Colour(64, 64, 64),
							 new Colour(128, 128, 128), new Colour(128, 128, 128), new Colour(192, 192, 192), new Colour(64, 64, 64),
							 new Colour(128, 128, 128), new Colour(128, 128, 128), new Colour(192, 192, 192), new Colour(128, 128, 128),
							 new Colour(192, 192, 192), new Colour(192, 192, 192), new Colour(255, 255, 255)]);
		i += 1;

		// black to dark blue, cyan to white
		this.themes[i] = new Theme("Blues", new ColourRange(new Colour(0, 0, 47), new Colour(0, 0, 255)), new ColourRange(new Colour(0, 255, 255), new Colour(255, 255, 255)), new Colour(0, 0, 0),
							new Colour(0, 255, 255), new ColourRange(new Colour(0, 0, 255), new Colour(-1, -1, -1)), new ColourRange(new Colour(0, 0, 47), new Colour(0, 0, 128)), new Colour(0, 0, 0),
							[new Colour(0, 128, 255), new Colour(0, 255, 128), new Colour(0, 192, 192), new Colour(128, 0, 255),
							 new Colour(0, 0, 255), new Colour(96, 128, 192), new Colour(128, 255, 224), new Colour(255, 0, 128),
							 new Colour(96, 96, 192), new Colour(128, 128, 255), new Colour(128, 224, 255), new Colour(192, 0, 192),
							 new Colour(255, 128, 224), new Colour(224, 128, 225), new Colour(224, 224, 255)]);
		i += 1;

		// black to red, orange to yellow
		this.themes[i] = new Theme("Fire", new ColourRange(new Colour(32, 0, 0), new Colour(160, 0, 0)), new ColourRange(new Colour(255, 144, 0), new Colour(255, 255, 0)), new Colour(0, 0, 0),
							new Colour(255, 144, 0), new ColourRange(new Colour(240, 0, 0), new Colour(-1, -1, -1)), new ColourRange(new Colour(32, 0, 0), new Colour(160, 0, 0)), new Colour(0, 0, 0),
							[new Colour(128, 0, 0), new Colour(128, 0, 48), new Colour(192, 0, 48), new Colour(128, 48, 48),
							 new Colour(192, 64, 0), new Colour(192, 48, 64), new Colour(224, 48, 64), new Colour(128, 48, 0),
							 new Colour(192, 48, 0), new Colour(192, 0, 64), new Colour(224, 48, 64), new Colour(192, 64, 48),
							 new Colour(224, 64, 48), new Colour(224, 64, 64), new Colour(255, 192, 0)]);
		i += 1;

		// black to green, cyan to white
		this.themes[i] = new Theme("Poison", new ColourRange(new Colour(0, 24, 0), new Colour(0, 128, 0)), new ColourRange(new Colour(0, 255, 255), new Colour(255, 255, 255)), new Colour(0, 0, 0),
							new Colour(0, 255, 255), new ColourRange(new Colour(0, 192, 0), new Colour(-1, -1, -1)), new ColourRange(new Colour(0, 24, 0), new Colour(0, 128, 0)), new Colour(0, 0, 0),
							[new Colour(0, 128, 255), new Colour(0, 255, 128), new Colour(0, 192, 192), new Colour(64, 0, 255),
							 new Colour(0, 0, 255), new Colour(48, 128, 192), new Colour(64, 255, 224), new Colour(0, 128, 128),
							 new Colour(48, 128, 192), new Colour(64, 128, 255), new Colour(64, 224, 255), new Colour(0, 96, 192),
							 new Colour(64, 128, 224), new Colour(56, 128, 255), new Colour(224, 255, 224)]);
		i += 1;

		// black to purple, yellow to white
		this.themes[i] = new Theme("Yellow", new ColourRange(new Colour(0, 47, 0), new Colour(128, 0, 128)), new ColourRange(new Colour(255, 255, 0), new Colour(255, 255, 255)), new Colour(0, 32, 128),
							new Colour(255, 255, 0), new ColourRange(new Colour(192, 64, 64), new Colour(-1, -1, -1)), new ColourRange(new Colour(0, 47, 0), new Colour(128, 0, 128)), new Colour(0, 32, 128),
							[new Colour(0, 150, 0), new Colour(50, 100, 0), new Colour(50, 200, 0), new Colour(150, 0, 0),
							 new Colour(75, 25, 0), new Colour(200, 100, 0), new Colour(200, 225, 0), new Colour(100, 50, 0),
							 new Colour(100, 200, 0), new Colour(25, 75, 0), new Colour(150, 255, 0), new Colour(200, 50, 0),
							 new Colour(225, 200, 0), new Colour(255, 225, 0), new Colour(255, 255, 0)]);
		i += 1;

		// grey scale
		this.themes[i] = new Theme("Gray", new ColourRange(new Colour(16, 16, 16), new Colour(104, 104, 104)), new ColourRange(new Colour(176, 176, 176), new Colour(240, 240, 240)), new Colour(0, 0, 0),
							new Colour(240, 240, 240), new ColourRange(new Colour(160, 160, 160), new Colour(-1, -1, -1)), new ColourRange(new Colour(16, 16, 16), new Colour(104, 104, 104)), new Colour(0, 0, 0),
							[new Colour(128, 128, 128), new Colour(128, 128, 128), new Colour(144, 144, 144), new Colour(128, 128, 128),
							 new Colour(144, 144, 144), new Colour(144, 144, 144), new Colour(160, 160, 160), new Colour(128, 128, 128),
							 new Colour(144, 144, 144), new Colour(144, 144, 144), new Colour(160, 160, 160), new Colour(144, 144, 144),
							 new Colour(160, 160, 160), new Colour(160, 160, 160), new Colour(192, 192, 192)]);
		i += 1;

		// inverse monochrome
		this.themes[i] = new Theme("Inverse", new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new Colour(255, 255, 255),
							new Colour(0, 0, 0), new ColourRange(new Colour(192, 192, 192), new Colour(-1, -1, -1)), new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)), new Colour(255, 255, 255),
							[new Colour(192, 192, 192), new Colour(192, 192, 192), new Colour(128, 128, 128), new Colour(192, 192, 192),
							 new Colour(128, 128, 128), new Colour(128, 128, 128), new Colour(64, 64, 64), new Colour(192, 192, 192),
							 new Colour(128, 128, 128), new Colour(128, 128, 128), new Colour(64, 64, 64), new Colour(128, 128, 128),
							 new Colour(64, 64, 64), new Colour(64, 64, 64), new Colour(0, 0, 0)]);
		i += 1;

		// white to cyan, blue to black
		this.themes[i] = new Theme("Day", new ColourRange(new Colour(240, 240, 240), new Colour(0, 255, 255)), new ColourRange(new Colour(0, 0, 255), new Colour(0, 0, 0)), new Colour(255, 255, 255),
							new Colour(0, 0, 255), new ColourRange(new Colour(0, 255, 255), new Colour(-1, -1, -1)), new ColourRange(new Colour(240, 240, 240), new Colour(0, 192, 192)), new Colour(255, 255, 255),
							[new Colour(192, 224, 255), new Colour(192, 255, 224), new Colour(192, 0, 192), new Colour(224, 192, 255),
							 new Colour(128, 128, 255), new Colour(96, 96, 192), new Colour(0, 128, 32), new Colour(255, 192, 224),
							 new Colour(96, 128, 192), new Colour(0, 0, 255), new Colour(0, 32, 128), new Colour(0, 192, 192),
							 new Colour(128, 0, 32), new Colour(32, 0, 128), new Colour(0, 0, 32)]);
		i += 1;

		// occupied vs unoccupied
		this.themes[i] = new Theme("Occupied", new ColourRange(new Colour(240, 240, 240), new Colour(240, 240, 240)), new ColourRange(new Colour(240, 240, 240), new Colour(240, 240, 240)), new Colour(0, 0, 0),
							new Colour(240, 240, 240), new ColourRange(new Colour(240, 240, 240), new Colour(240, 240, 240)), new ColourRange(new Colour(240, 240, 240), new Colour(240, 240, 240)), new Colour(0, 0, 0),
							[new Colour(240, 240, 240), new Colour(240, 240, 240), new Colour(240, 240, 240), new Colour(240, 240, 240),
							 new Colour(240, 240, 240), new Colour(240, 240, 240), new Colour(240, 240, 240), new Colour(240, 240, 240),
							 new Colour(240, 240, 240), new Colour(240, 240, 240), new Colour(240, 240, 240), new Colour(240, 240, 240),
							 new Colour(240, 240, 240), new Colour(240, 240, 240), new Colour(240, 240, 240)]);
		i += 1;

		// unoccupied, dead and alive only
		this.themes[i] = new Theme("Red", new ColourRange(new Colour(160, 0, 0), new Colour(160, 0, 0)), new ColourRange(new Colour(240, 240, 240), new Colour(240, 240, 240)), new Colour(0, 0, 0),
							new Colour(255, 255, 255), new ColourRange(new Colour(160, 160, 160), new Colour(160, 160, 160)), new ColourRange(new Colour(160, 0, 0), new Colour(160, 0, 0)), new Colour(0, 0, 0),
							[new Colour(255, 255, 255), new Colour(255, 255, 255), new Colour(255, 255, 255), new Colour(255, 255, 255),
							 new Colour(255, 255, 255), new Colour(255, 255, 255), new Colour(255, 255, 255), new Colour(255, 255, 255),
							 new Colour(255, 255, 255), new Colour(255, 255, 255), new Colour(255, 255, 255), new Colour(255, 255, 255),
							 new Colour(255, 255, 255), new Colour(255, 255, 255), new Colour(255, 255, 255)]);
		i += 1;

		// LifeHistory
		this.themes[i] = new Theme("LifeHistory", new ColourRange(new Colour(0, 0, 96), new Colour(0, 0, 160)), new ColourRange(new Colour(0, 240, 0), new Colour(16, 255, 16)), new Colour(0, 0, 0),
							new Colour(16, 255, 16), new ColourRange(new Colour(0, 128, 160), new Colour(-1, -1, -1)), new ColourRange(new Colour(0, 0, 96), new Colour(0, 0, 160)), new Colour(0, 0, 0),
							[new Colour(255, 0, 0), new Colour(192, 192, 0), new Colour(224, 144, 0), new Colour(0, 192, 192),
							 new Colour(192, 144, 144), new Colour(144, 192, 144), new Colour(255, 224, 192), new Colour(0, 0, 255),
							 new Colour(192, 0, 192), new Colour(144, 144, 192), new Colour(255, 192, 255), new Colour(0, 144, 224),
							 new Colour(128, 96, 224), new Colour(144, 192, 224), new Colour(224, 224, 224)]);
		i += 1;

		// Multi-state (Generations and HROT) - yellow to red
		this.themes[i] = new Theme("Generations", new ColourRange(new Colour(64, 0, 0), new Colour(255, 0, 0)), new ColourRange(new Colour(255, 255, 0), new Colour(255, 255, 255)), new Colour(0, 0, 0),
							new Colour(255, 255, 0), new ColourRange(new Colour(255, 0, 0), new Colour(-1, -1, -1)), new ColourRange(new Colour(64, 0, 0), new Colour(128, 0, 0)), new Colour(0, 0, 0),
							[new Colour(255, 128, 0), new Colour(224, 128, 64), new Colour(192, 192, 0), new Colour(255, 16, 32),
							 new Colour(255, 0, 0), new Colour(192, 128, 48), new Colour(255, 224, 64), new Colour(128, 32, 64),
							 new Colour(192, 96, 48), new Colour(255, 128, 64), new Colour(255, 224, 64), new Colour(192, 24, 48),
							 new Colour(224, 128, 128), new Colour(255, 218, 112), new Colour(255, 255, 224)]);
		i += 1;

		// Golly theme
		this.themes[i] = new Theme("Golly", new ColourRange(new Colour(48, 48, 48), new Colour(48, 48, 48)), new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)), new Colour(48, 48, 48),
							new Colour(255, 0, 0), new ColourRange(new Colour(255, 255, 0), new Colour(-1, -1, -1)), new ColourRange(new Colour(48, 48, 48), new Colour(48, 48, 48)), new Colour(48, 48, 48),
							[new Colour(0, 255, 127), new Colour(127, 0, 255), new Colour(148, 148, 148), new Colour(128, 255, 9),
							 new Colour(255, 0, 128), new Colour(0, 128, 255), new Colour(1, 159, 0), new Colour(159, 0, 1),
							 new Colour(255, 254, 96), new Colour(0, 1, 159), new Colour(96, 255, 254), new Colour(254, 96, 255),
							 new Colour(126, 125, 21), new Colour(21, 126, 125), new Colour(125, 21, 126)]);
		this.themes[i].setGridLines(10, new Colour(80, 80, 80), new Colour(112, 112, 112));
		i += 1;

		// MCell theme
		this.themes[i] = new Theme("MCell", new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new ColourRange(new Colour(255, 255, 0), new Colour(255, 255, 0)), new Colour(0, 0, 0),
							new Colour(255, 255, 0), new ColourRange(new Colour(0, 255, 0), new Colour(-1, -1, -1)), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new Colour(0, 0, 0),
							[new Colour(255, 255, 0), new Colour(255, 219, 0), new Colour(255, 183, 0), new Colour(255, 147, 0),
							 new Colour(255, 111, 0), new Colour(255, 75, 0), new Colour(255, 39, 0), new Colour(255, 0, 0),
							 new Colour(240, 0, 0), new Colour(225, 0, 0), new Colour(210, 0, 0), new Colour(195, 0, 0),
							 new Colour(180, 0, 0), new Colour(165, 0, 0), new Colour(150, 0, 0)]);
		this.themes[i].setGridLines(5, new Colour(64, 0, 0), new Colour(99, 3, 1));
		i += 1;

		// Catagolue theme
		this.themes[i] = new Theme("Catagolue", new ColourRange(new Colour(160, 221, 204), new Colour(160, 221, 204)), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new Colour(192, 255, 238),
							new Colour(0, 0, 0), new ColourRange(new Colour(96, 192, 139), new Colour(2, 129, 2)), new ColourRange(new Colour(160, 221, 204), new Colour(160, 221, 204)), new Colour(192, 255, 238),
							[new Colour(0, 0, 0), new Colour(0, 128, 0), new Colour(0, 64, 0), new Colour(255, 0, 0),
							 new Colour(128, 0, 0), new Colour(128, 64, 0), new Colour(85, 43, 0), new Colour(0, 0, 255),
							 new Colour(0, 0, 128), new Colour(0, 64, 128), new Colour(0, 43, 85), new Colour(128, 0, 128),
							 new Colour(85, 0, 85), new Colour(85, 43, 85), new Colour(64, 32, 64)]);
		this.themes[i].setGridLines(0, new Colour(160, 221, 204), new Colour(160, 221, 204));
		i += 1;

		// Caterer theme
		this.themes[i] = new Theme("Caterer", new ColourRange(new Colour(54, 57, 62), new Colour(54, 57, 62)), new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)), new Colour(54, 57, 62),
							new Colour(255, 170, 0), new ColourRange(new Colour(255, 85, 0), new Colour(-1, -1, -1)), new ColourRange(new Colour(54, 57, 62), new Colour(54, 57, 62)), new Colour(54, 57, 62),
							[new Colour(48, 93, 148), new Colour(58, 73, 148), new Colour(144, 137, 188), new Colour(78, 72, 128),
							 new Colour(134, 137, 218), new Colour(124, 147, 228), new Colour(130, 151, 242), new Colour(78, 93, 118),
							 new Colour(114, 107, 218), new Colour(114, 137, 238), new Colour(120, 171, 242), new Colour(84, 137, 188),
							 new Colour(150, 171, 212), new Colour(150, 151, 222), new Colour(186, 205, 255)]);
		this.themes[i].setGridLines(0, new Colour(0, 0, 0), new Colour(0, 0, 0));
		i += 1;

		// Life32 theme
		this.themes[i] = new Theme("Life32", new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)), new ColourRange(new Colour(0, 0, 128), new Colour(0, 0, 128)), new Colour(255, 255, 255),
							new Colour(0, 0, 128), new ColourRange(new Colour(0, 0, 64), new Colour(-1, -1, -1)), new ColourRange(new Colour(255, 255, 255), new Colour(255, 255, 255)), new Colour(255, 255, 255),
							[new Colour(255, 128, 128), new Colour(192, 192, 128), new Colour(224, 144, 96), new Colour(128, 192, 192),
							 new Colour(192, 112, 112), new Colour(144, 192, 144), new Colour(128, 96, 64), new Colour(128, 128, 255),
							 new Colour(192, 96, 192), new Colour(112, 112, 192), new Colour(128, 64, 128), new Colour(96, 144, 224),
							 new Colour(64, 32, 160), new Colour(32, 96, 128), new Colour(32, 32, 32)]);
		this.themes[i].setGridLines(5, new Colour(192, 192, 192), new Colour(128, 128, 128));
		i += 1;

		// Margolus theme
		this.themes[i] = new Theme("Margolus", new ColourRange(new Colour(0, 0, 47), new Colour(0, 0, 128)), new ColourRange(new Colour(255, 255, 0), new Colour(255, 255, 255)), new Colour(0, 0, 0),
							new Colour(255, 255, 0), new ColourRange(new Colour(64, 64, 128), new Colour(-1, -1, -1)), new ColourRange(new Colour(0, 0, 47), new Colour(0, 0, 128)), new Colour(0, 0, 0),
							[new Colour(255, 128, 0), new Colour(224, 128, 64), new Colour(192, 192, 0), new Colour(255, 16, 32),
							 new Colour(255, 0, 0), new Colour(192, 128, 48), new Colour(224, 255, 64), new Colour(128, 32, 64),
							 new Colour(192, 96, 48), new Colour(255, 128, 64), new Colour(255, 224, 64), new Colour(192, 24, 48),
							 new Colour(224, 128, 128), new Colour(255, 128, 112), new Colour(255, 255, 224)]);
		this.themes[i].setGridLines(2, new Colour(64, 64, 128), new Colour(32, 32, 255));
		i += 1;

		// PCA theme
		this.themes[i] = new Theme("PCA", new ColourRange(new Colour(24, 24, 24), new Colour(64, 64, 64)), new ColourRange(new Colour(176, 176, 176), new Colour(240, 240, 240)), new Colour(0, 0, 0),
							new Colour(240, 240, 240), new ColourRange(new Colour(160, 160, 160), new Colour(-1, -1, -1)), new ColourRange(new Colour(24, 24, 24), new Colour(64, 64, 64)), new Colour(0, 0, 0),
							[new Colour(255, 0, 0), new Colour(0, 255, 0), new Colour(128, 128, 0), new Colour(16, 16, 255),
							 new Colour(128, 0, 128), new Colour(0, 128, 128), new Colour(96, 96, 96), new Colour(255, 255, 0),
							 new Colour(255, 128, 0), new Colour(128, 255, 0), new Colour(170, 170, 0), new Colour(144, 144, 144),
							 new Colour(170, 85, 85), new Colour(85, 128, 43), new Colour(128, 128, 64)]);
		i += 1;

		// Book theme
		this.themes[i] = new Theme("Book", new ColourRange(new Colour(255, 220, 192), new Colour(192, 220, 255)), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new Colour(255, 255, 255),
							new Colour(0, 0, 0), new ColourRange(new Colour(128, 128, 128), new Colour(48, 48, 48)), new ColourRange(new Colour(255, 220, 192), new Colour(192, 220, 255)), new Colour(255, 255, 255),
							[new Colour(255, 128, 128), new Colour(192, 192, 128), new Colour(224, 144, 96), new Colour(128, 192, 192),
							 new Colour(192, 112, 112), new Colour(144, 192, 144), new Colour(128, 96, 64), new Colour(128, 128, 255),
							 new Colour(192, 96, 192), new Colour(112, 112, 192), new Colour(128, 64, 128), new Colour(96, 144, 224),
							 new Colour(64, 32, 160), new Colour(32, 96, 128), new Colour(32, 32, 32)]);
		this.themes[i].setGridLines(0, new Colour(192, 192, 192), new Colour(209, 209, 209));
		i += 1;

		// custom theme
		this.themes[i] = new Theme(Keywords.themeCustomWord, new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new Colour(0, 0, 0),
							new Colour(0, 0, 0), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0)), new Colour(0, 0, 0),
							[new Colour(255, 0, 0), new Colour(0, 255, 0), new Colour(128, 128, 0), new Colour(16, 16, 255),
							 new Colour(128, 0, 128), new Colour(0, 128, 128), new Colour(96, 96, 96), new Colour(255, 255, 0),
							 new Colour(255, 128, 0), new Colour(128, 255, 0), new Colour(170, 170, 0), new Colour(144, 144, 144),
							 new Colour(170, 85, 85), new Colour(85, 128, 43), new Colour(128, 128, 64)]);
		i += 1;

		// save number of themes less one (for the custom theme)
		this.numThemes = this.themes.length - 1;

		// set current colour theme
		// 2-state
		this.aliveColCurrent = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.deadColCurrent = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.unoccupiedCurrent = new Colour(0, 0, 0);

		// multi-state
		this.aliveGenColCurrent = new Colour(0, 0, 0);
		this.dyingGenColCurrent = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.deadGenColCurrent = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.unoccupiedGenCurrent = new Colour(0, 0, 0);

		// PCA
		this.pcaColsCurrent = [new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0),
					     new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0),
					     new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0),
					     new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0)];

		// set target colour theme
		// 2-state
		this.aliveColTarget = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.deadColTarget = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.unoccupiedTarget = new Colour(0, 0, 0);

		// multi-state
		this.aliveGenColTarget = new Colour(0, 0, 0);
		this.dyingGenColTarget = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.deadGenColTarget = new ColourRange(new Colour(0, 0, 0), new Colour(0, 0, 0));
		this.unoccupiedGenTarget = new Colour(0, 0, 0);

		// PCA
		this.pcaColsTarget = [new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0),
					    new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0),
					    new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0),
					    new Colour(0, 0, 0), new Colour(0, 0, 0), new Colour(0, 0, 0)];
	};

	// set the theme
	Life.prototype.setTheme = function(/** @type {number} */ theme, /** @type {number} */ switchTime, /** @type {View} */ view) {
		var	/** @type {Theme} */ newTheme = this.themes[theme],
			/** @type {number} */ i = 0;

		// save the theme
		this.colourTheme = theme;
		this.createColourIndex();

		// set current point to the target
		this.aliveColCurrent.set(this.aliveColTarget);
		this.deadColCurrent.set(this.deadColTarget);
		this.unoccupiedCurrent.set(this.unoccupiedTarget);
		this.aliveGenColCurrent.set(this.aliveGenColTarget);
		this.deadGenColCurrent.set(this.deadGenColTarget);
		this.dyingGenColCurrent.set(this.dyingGenColTarget);
		this.unoccupiedGenCurrent.set(this.unoccupiedGenTarget);
		for (i = 0; i < this.pcaColsTarget.length; i += 1) {
			this.pcaColsCurrent[i] = this.pcaColsTarget[i];
		}

		// set the colour target to the theme
		if (theme === 12 && this.isHROT) {
			this.aliveColTarget.set(this.themes[11].aliveRange);
		} else {
			this.aliveColTarget.set(newTheme.aliveRange);
		}
		this.deadColTarget.set(newTheme.deadRange);
		this.unoccupiedTarget.set(newTheme.unoccupied);

		// set the theme colours
		this.unoccupiedGenTarget.set(newTheme.unoccupiedGen);
		for (i = 0; i < this.pcaColsTarget.length; i += 1) {
			this.pcaColsTarget[i] = newTheme.pcaCols[i];
		}
		this.deadGenColTarget.set(newTheme.deadRangeGen);

		// for multi-state HROT rules use Generations Theme for Golly Theme for Alive and Dying colours
		if (theme === 12 && this.isHROT) {
			newTheme = this.themes[11];
		}

		this.aliveGenColTarget.set(newTheme.aliveGen);
		this.dyingGenColTarget.set(newTheme.dyingRangeGen);

		// set the change time
		this.colourChange = switchTime;

		// copy grid line colours from theme
		this.gridLineRaw  = newTheme.gridColour;
		this.gridLineBoldRaw = newTheme.gridMajorColour;

		// if the Theme is not custom and the pattern is Margolus then set grid major to 2 unless the Theme specifies it as zero
		if (theme !== this.numThemes && this.isMargolus && newTheme.gridMajor !== 0) {
			this.gridLineMajor = 2;
		} else {
			// copy grid line major interval from theme
			this.gridLineMajor = newTheme.gridMajor;
		}

		// create grid line colours
		if (this.littleEndian) {
			this.gridLineColour = (255 << 24) | ((this.gridLineRaw & 255) << 16) | (((this.gridLineRaw >> 8) & 255) << 8) | (this.gridLineRaw >> 16);
			this.gridLineBoldColour = (255 << 24) | ((this.gridLineBoldRaw & 255) << 16) | (((this.gridLineBoldRaw >> 8) & 255) << 8) | (this.gridLineBoldRaw >> 16);
		} else {
			this.gridLineColour = ((this.gridLineRaw >> 16) << 24) | (((this.gridLineRaw >> 8) & 255) << 16) | ((this.gridLineRaw & 255) << 8) | 255;
			this.gridLineBoldColour = ((this.gridLineBoldRaw >> 16) << 24) | (((this.gridLineBoldRaw >> 8) & 255) << 16) | ((this.gridLineBoldRaw & 255) << 8) | 255;
		}

		// set the [R]History overlay colours
		if (this.isLifeHistory) {
			// check for Custom theme
			if (theme === this.numThemes) {
				this.createLHOverlayColours(view.colourList, view.customColours);
			} else {
				// otherwise use default overlay colours
				this.createLHOverlayColours(view.colourList, null);
			}
		}

		// clear help cache
		view.clearHelpCache();
	};

	// create the colour index
	Life.prototype.createColourIndex = function() {
		var	/** @type {number} */ shader = this.cellRenderer;

		if (shader === LifeConstants.shaderCellAge && !this.themes[this.colourTheme].hasHistory(this.isLifeHistory, this.view) && !this.isHROT) {
			shader = LifeConstants.shaderBasic;
		}

		switch (shader) {
			case LifeConstants.shaderCellAge:
				this.createColourIndexRegular();
				break;

			case LifeConstants.shaderRainbow:
				this.createColourIndexRainbow();
				break;

			case LifeConstants.shaderNeighbourCount:
				// use the regular index for reset
				this.createColourIndexRegular();
				break;

			case LifeConstants.shaderBasic:
				this.createColourIndex2();
				break;
		}
	};

	// create basic colour index
	Life.prototype.createColourIndex2 = function() {
		var	/** @type {Uint16Array} */ colourLookup = this.colourLookup16;

		colourLookup[0] = 0;
		colourLookup[1] = (64 << 8);
		colourLookup[2] = 64;
		colourLookup[3] = (64 << 8) | 64;
	};

	// create rainbow colour index
	Life.prototype.createColourIndexRainbow = function() {
		var	/** @type {Uint16Array} */ colourLookup = this.colourLookup16,
			/** @type {number} */ left = 0,
			/** @type {number} */ right = 0,
			/** @type {number} */ offset = 0,
			/** @type {number} */ value = 0,
			/** @type {number} */ i = 0;

		// create 9 bit lookup
		// a b v v v v v v v
		// a - left cell alive
		// b - right cell alive
		// v - offset in rainbow

		// use byte lookup to create 16bit lookup
		for (i = 0; i < 512; i += 1) {
			left = i & 128;
			right = i & 256;
			offset = i & 127;
			value = 0;
			if (left !== 0) {
				value = (((offset + 1) & 127) + 64) << 8;
			}
			if (right !== 0) {
				value |= (offset & 127) + 64;
			}
			colourLookup[i] = value;
		}
	};

	// create normal colour index
	Life.prototype.createColourIndexRegular = function() {
		var	/** @type {Uint16Array} */ colourLookup16 = this.colourLookup16,
			/** @type {Uint16Array} */ colourLookup17 = this.colourLookup17,
			/** @type {number} */ aliveMax = this.aliveMax,
			/** @type {number} */ aliveStart = this.aliveStart,
			/** @type {number} */ deadMin = this.deadMin,
			/** @type {number} */ deadStart = this.deadStart,
			/** @type {number} */ i = 0,
			/** @type {number} */ v = 0,
			/** @type {Uint8Array} */ byteIndex = new Uint8Array(256);

		// create byte lookup
		// first pixel
		byteIndex[0] = 0;
		byteIndex[aliveMax + 1] = aliveStart;

		for (i = 1; i < aliveMax + 1; i += 1) {
			byteIndex[i] = Math.min(Math.max(i - 1, deadMin), deadStart);
			byteIndex[i + aliveMax + 1] = Math.max(Math.min(i + 1, aliveMax), aliveStart);
		}

		// adjust for history states setting
		if (this.historyStates === 0) {
			for (i = aliveStart; i <= aliveMax; i += 1) {
				byteIndex[i] = 0;
			}
		} else {
			if (this.historyStates < deadStart) {
				byteIndex[deadStart - this.historyStates] = 1;
			}
		}

		// use byte lookup to create 16bit lookup
		for (i = 0; i < 65536; i += 1) {
			colourLookup16[i] = (byteIndex[i >> 8] << 8) | byteIndex[i & 255];
		}

		// use 16bit lookup to create 17bit lookup
		for (i = 0; i < 131072; i += 1) {
			v = (i & 65407) | ((i & 65536) >> 9);
			colourLookup17[i] = colourLookup16[v];
		}
	};

	// process multi-state themes
	Life.prototype.processMultiStateThemes = function() {
		var	/** @type {number} */ i = 0,
			/** @type {Theme} */ theme = null,
			/** @type {number} */ weight = 1;

		// check for dynamic dying state in each theme
		for (i = 0; i < this.themes.length; i += 1) {
			theme = this.themes[i];
			// check if the dying colour was marked as dynamic
			if (theme.dyingRangeDynamic) {
				if (this.multiNumStates <= 2) {
					theme.dyingRangeGen.endColour.set(theme.aliveGen);
				} else {
					// convert it from the alive colour and number of states
					weight = 1 / (this.multiNumStates - 2);
					theme.dyingRangeGen.endColour.red = (theme.dyingRangeGen.startColour.red * weight + theme.aliveGen.red * (1 - weight)) | 0;
					theme.dyingRangeGen.endColour.green = (theme.dyingRangeGen.startColour.green * weight + theme.aliveGen.green * (1 - weight)) | 0;
					theme.dyingRangeGen.endColour.blue = (theme.dyingRangeGen.startColour.blue * weight + theme.aliveGen.blue * (1 - weight)) | 0;
				}
			}
		}
	};

	// create the colours
	Life.prototype.createColours = function() {
		var	/** @type {number} */ i,
			/** @type {number} */ mixWeight,
			/** @type {number} */ weight,
			/** @type {number} */ currentComponent,
			/** @type {number} */ targetComponent,
			/** @type {number} */ current,
			/** @type {number} */ deadMin;

		// set the weighting between the two colour ranges
		mixWeight = (this.colourChange - 1) / this.colourChangeSteps;

		// do nothing for "none" rule since colours are fixed
		if (!this.isNone) {
			// check for [R]Extended rules
			if (this.isExtended) {
				for (i = 0; i < LifeConstants.coloursExtended.length; i += 1) {
					this.redChannel[i + this.historyStates] = LifeConstants.coloursExtended[i][0];
					this.greenChannel[i + this.historyStates] = LifeConstants.coloursExtended[i][1];
					this.blueChannel[i + this.historyStates] = LifeConstants.coloursExtended[i][2];

					// override with custom colour if specified
					if (this.customColours && this.customColours.length >= i) {
						current = this.customColours[i];
						if (current !== -1) {
							this.redChannel[i + this.historyStates] = current >> 16;
							this.greenChannel[i + this.historyStates] = (current >> 8) & 255;
							this.blueChannel[i + this.historyStates] = (current & 255);
						}
					}
				}
			} else {
				// check for [R]Super rules
				if (this.isSuper) {
					for (i = 0; i < LifeConstants.coloursSuper.length; i += 1) {
						this.redChannel[i + this.historyStates] = LifeConstants.coloursSuper[i][0];
						this.greenChannel[i + this.historyStates] = LifeConstants.coloursSuper[i][1];
						this.blueChannel[i + this.historyStates] = LifeConstants.coloursSuper[i][2];

						// override with custom colour if specified
						if (this.customColours && this.customColours.length >= i) {
							current = this.customColours[i];
							if (current !== -1) {
								this.redChannel[i + this.historyStates] = current >> 16;
								this.greenChannel[i + this.historyStates] = (current >> 8) & 255;
								this.blueChannel[i + this.historyStates] = (current & 255);
							}
						}
					}
				} else {
					// check for Generations, HROT or PCA rules
					if (this.multiNumStates > 2 || this.isRuleTree) {
						// set unoccupied colour
						i = 0;
						if (this.isRuleTree) {
							this.redChannel[i] = this.ruleTreeColours[i] >> 16;
							this.greenChannel[i] = (this.ruleTreeColours[i] >> 8) & 255;
							this.blueChannel[i] = this.ruleTreeColours[i] & 255;
						} else {
							this.redChannel[i] = Math.round(this.unoccupiedGenCurrent.red * mixWeight + this.unoccupiedGenTarget.red * (1 - mixWeight));
							this.greenChannel[i] = Math.round(this.unoccupiedGenCurrent.green * mixWeight + this.unoccupiedGenTarget.green * (1 - mixWeight));
							this.blueChannel[i] = Math.round(this.unoccupiedGenCurrent.blue * mixWeight + this.unoccupiedGenTarget.blue * (1 - mixWeight));
						}

						// set generations or PCA colours
						for (i = 1; i < this.multiNumStates - 1; i += 1) {
							// compute the weighting between the start and end colours in the range
							if (this.multiNumStates <= 3) {
								weight = 0;
							} else {
								weight = (i - 1) / (this.multiNumStates - 3);
							}

							// check for PCA
							if (this.isPCA) {
								this.redChannel[i + this.historyStates] = Math.round(this.pcaColsCurrent[i - 1].red * mixWeight + this.pcaColsTarget[i - 1].red * (1 - mixWeight));
								this.greenChannel[i + this.historyStates] = Math.round(this.pcaColsCurrent[i - 1].green * mixWeight + this.pcaColsTarget[i - 1].green * (1 - mixWeight));
								this.blueChannel[i + this.historyStates] = Math.round(this.pcaColsCurrent[i - 1].blue * mixWeight + this.pcaColsTarget[i - 1].blue * (1 - mixWeight));
							} else {
								if (this.isRuleTree) {
									this.redChannel[i] = this.ruleTreeColours[i] >> 16;
									this.greenChannel[i] = (this.ruleTreeColours[i] >> 8) & 255;
									this.blueChannel[i] = this.ruleTreeColours[i] & 255;
								} else {
									// compute the red component of the current and target colour
									currentComponent = Math.round(this.dyingGenColCurrent.endColour.red * weight + this.dyingGenColCurrent.startColour.red * (1 - weight));
									targetComponent = Math.round(this.dyingGenColTarget.endColour.red * weight + this.dyingGenColTarget.startColour.red * (1 - weight));
									this.redChannel[i + this.historyStates] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

									// compute the green component of the current and target colour
									currentComponent = Math.round(this.dyingGenColCurrent.endColour.green * weight + this.dyingGenColCurrent.startColour.green * (1 - weight));
									targetComponent = Math.round(this.dyingGenColTarget.endColour.green * weight + this.dyingGenColTarget.startColour.green * (1 - weight));
									this.greenChannel[i + this.historyStates] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

									// compute the blue component of the current and target colour
									currentComponent = Math.round(this.dyingGenColCurrent.endColour.blue * weight + this.dyingGenColCurrent.startColour.blue * (1 - weight));
									targetComponent = Math.round(this.dyingGenColTarget.endColour.blue * weight + this.dyingGenColTarget.startColour.blue * (1 - weight));
									this.blueChannel[i + this.historyStates] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));
								}
							}

							// override with custom colour if specified and the Custom Theme is active
							if (this.customColours && this.customColours.length > i && this.colourTheme === this.numThemes) {
								if (!(this.isPCA || this.isRuleTree)) {
									current = this.customColours[this.multiNumStates - i];
								} else {
									current = this.customColours[i];
								}
								if (current !== -1) {
									this.redChannel[i + this.historyStates] = current >> 16;
									this.greenChannel[i + this.historyStates] = (current >> 8) & 255;
									this.blueChannel[i + this.historyStates] = (current & 255);
								}
							}
						}

						// set alive colour
						i = this.multiNumStates - 1;
						if (this.isPCA) {
							this.redChannel[i + this.historyStates] = Math.round(this.pcaColsCurrent[i - 1].red * mixWeight + this.pcaColsTarget[i - 1].red * (1 - mixWeight));
							this.greenChannel[i + this.historyStates] = Math.round(this.pcaColsCurrent[i - 1].green * mixWeight + this.pcaColsTarget[i - 1].green * (1 - mixWeight));
							this.blueChannel[i + this.historyStates] = Math.round(this.pcaColsCurrent[i - 1].blue * mixWeight + this.pcaColsTarget[i - 1].blue * (1 - mixWeight));
						} else {
							if (this.isRuleTree) {
								this.redChannel[i] = this.ruleTreeColours[i] >> 16;
								this.greenChannel[i] = (this.ruleTreeColours[i] >> 8) & 255;
								this.blueChannel[i] = this.ruleTreeColours[i] & 255;
							} else {
								this.redChannel[i + this.historyStates] = Math.round(this.aliveGenColCurrent.red * mixWeight + this.aliveGenColTarget.red * (1 - mixWeight));
								this.greenChannel[i + this.historyStates] = Math.round(this.aliveGenColCurrent.green * mixWeight + this.aliveGenColTarget.green * (1 - mixWeight));
								this.blueChannel[i + this.historyStates] = Math.round(this.aliveGenColCurrent.blue * mixWeight + this.aliveGenColTarget.blue * (1 - mixWeight));
							}
						}

						// override with custom colour if specified
						if (this.customColours && this.customColours.length > i && this.colourTheme === this.numThemes) {
							if (!(this.isPCA || this.isRuleTree)) {
								current = this.customColours[this.multiNumStates - i];
							} else {
								current = this.customColours[i];
							}
							if (current !== -1) {
								this.redChannel[i + this.historyStates] = current >> 16;
								this.greenChannel[i + this.historyStates] = (current >> 8) & 255;
								this.blueChannel[i + this.historyStates] = (current & 255);
							}
						}

						// create history colours if specified
						for (i = 0; i < this.historyStates; i += 1) {
							if (this.historyStates > 1) {
								weight = 1 - (i / (this.historyStates - 1));
							} else {
								weight = 1;
							}
							// compute the red component of the current and target colour
							currentComponent = Math.round(this.deadGenColCurrent.startColour.red * weight + this.deadGenColCurrent.endColour.red * (1 - weight));
							targetComponent = Math.round(this.deadGenColTarget.startColour.red * weight + this.deadGenColTarget.endColour.red * (1 - weight));
							this.redChannel[i + 1] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

							// compute the green component of the current and target colour
							currentComponent = Math.round(this.deadGenColCurrent.startColour.green * weight + this.deadGenColCurrent.endColour.green * (1 - weight));
							targetComponent = Math.round(this.deadGenColTarget.startColour.green * weight + this.deadGenColTarget.endColour.green * (1 - weight));
							this.greenChannel[i + 1] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

							// compute the blue component of the current and target colour
							currentComponent = Math.round(this.deadGenColCurrent.startColour.blue * weight + this.deadGenColCurrent.endColour.blue * (1 - weight));
							targetComponent = Math.round(this.deadGenColTarget.startColour.blue * weight + this.deadGenColTarget.endColour.blue * (1 - weight));
							this.blueChannel[i + 1] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));
						}

						// override colour 0 if specified
						if (this.customColours && this.customColours.length > 0 && this.colourTheme === this.numThemes) {
							current = this.customColours[0];
							if (current !== -1) {
								this.redChannel[0] = current >> 16;
								this.greenChannel[0] = (current >> 8) & 255;
								this.blueChannel[0] = (current & 255);
							}
						}
					} else {
						// set unoccupied colour
						i = 0;
						this.redChannel[i] = Math.round(this.unoccupiedCurrent.red * mixWeight + this.unoccupiedTarget.red * (1 - mixWeight));
						this.greenChannel[i] = Math.round(this.unoccupiedCurrent.green * mixWeight + this.unoccupiedTarget.green * (1 - mixWeight));
						this.blueChannel[i] = Math.round(this.unoccupiedCurrent.blue * mixWeight + this.unoccupiedTarget.blue * (1 - mixWeight));

						// set dead colours and start by clearing unused history colours
						if (this.historyStates === 0) {
							for (i = 1; i <= this.deadStart; i += 1) {
								this.redChannel[i] = Math.round(this.unoccupiedCurrent.red * mixWeight + this.unoccupiedTarget.red * (1 - mixWeight));
								this.greenChannel[i] = Math.round(this.unoccupiedCurrent.green * mixWeight + this.unoccupiedTarget.green * (1 - mixWeight));
								this.blueChannel[i] = Math.round(this.unoccupiedCurrent.blue * mixWeight + this.unoccupiedTarget.blue * (1 - mixWeight));
							}
						} else {
							deadMin = this.deadStart - this.historyStates + 1;
							for (i = 1; i < deadMin; i += 1) {
								this.redChannel[i] = Math.round(this.deadColCurrent.startColour.red * mixWeight + this.deadColTarget.startColour.red * (1 - mixWeight));
								this.greenChannel[i] = Math.round(this.deadColCurrent.startColour.green * mixWeight + this.deadColTarget.startColour.green * (1 - mixWeight));
								this.blueChannel[i] = Math.round(this.deadColCurrent.startColour.blue * mixWeight + this.deadColTarget.startColour.blue * (1 - mixWeight));
							}
							for (i = deadMin; i <= this.deadStart; i += 1) {
								// compute the weighting between the start and end colours in the range
								if (this.deadStart === deadMin) {
									weight = 1;
								} else {
									weight = 1 - ((i - deadMin) / (this.deadStart - deadMin));
								}

								// compute the red component of the current and target colour
								currentComponent = Math.round(this.deadColCurrent.startColour.red * weight + this.deadColCurrent.endColour.red * (1 - weight));
								targetComponent = Math.round(this.deadColTarget.startColour.red * weight + this.deadColTarget.endColour.red * (1 - weight));
								this.redChannel[i] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

								// compute the green component of the current and target colour
								currentComponent = Math.round(this.deadColCurrent.startColour.green * weight + this.deadColCurrent.endColour.green * (1 - weight));
								targetComponent = Math.round(this.deadColTarget.startColour.green * weight + this.deadColTarget.endColour.green * (1 - weight));
								this.greenChannel[i] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

								// compute the blue component of the current and target colour
								currentComponent = Math.round(this.deadColCurrent.startColour.blue * weight + this.deadColCurrent.endColour.blue * (1 - weight));
								targetComponent = Math.round(this.deadColTarget.startColour.blue * weight + this.deadColTarget.endColour.blue * (1 - weight));
								this.blueChannel[i] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));
							}
						}

						// set alive colours
						for (i = this.aliveStart; i <= this.aliveMax; i += 1) {
							// compute the weighting between the start and end colours in the range
							if (this.aliveStates === 0) {
								weight = 1;
							} else {
								if (i < this.aliveStart + this.aliveStates) {
									weight = 1 - ((i - this.aliveStart) / this.aliveStates);
								} else {
									weight = 0;
								}
							}

							// compute the red component of the current and target colour
							currentComponent = Math.round(this.aliveColCurrent.startColour.red * weight + this.aliveColCurrent.endColour.red * (1 - weight));
							targetComponent = Math.round(this.aliveColTarget.startColour.red * weight + this.aliveColTarget.endColour.red * (1 - weight));
							this.redChannel[i] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

							// compute the green component of the current and target colour
							currentComponent = Math.round(this.aliveColCurrent.startColour.green * weight + this.aliveColCurrent.endColour.green * (1 - weight));
							targetComponent = Math.round(this.aliveColTarget.startColour.green * weight + this.aliveColTarget.endColour.green * (1 - weight));
							this.greenChannel[i] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));

							// compute the blue component of the current and target colour
							currentComponent = Math.round(this.aliveColCurrent.startColour.blue * weight + this.aliveColCurrent.endColour.blue * (1 - weight));
							targetComponent = Math.round(this.aliveColTarget.startColour.blue * weight + this.aliveColTarget.endColour.blue * (1 - weight));
							this.blueChannel[i] = Math.round(currentComponent * mixWeight + targetComponent * (1 - mixWeight));
						}
					}
				}
			}
		}
	};

	// create multi-state pixel colours
	Life.prototype.createMultiStateColours = function(/** @type {Array<number>} */ colourList, /** @type {Int32Array} */ customColours) {
		var	/** @type {Uint8Array} */ redChannel = this.redChannel,
			/** @type {Uint8Array} */ greenChannel = this.greenChannel,
			/** @type {Uint8Array} */ blueChannel = this.blueChannel,
			/** @type {number} */ i = 0,
			/** @type {number} */ stateColour = 0;

		// create multi-state pixel colours
		for (i = 0; i < colourList.length; i += 1) {
			// check if a custom colour is defined
			if (customColours && customColours[i] !== -1) {
				// use the custom colour
				stateColour = customColours[i];
			} else {
				// use the library colour
				stateColour = colourList[i];
			}
			redChannel[i] = stateColour >> 16;
			greenChannel[i] = (stateColour >> 8) & 255;
			blueChannel[i] = stateColour & 255;
		}

		// clear colour change flag
		this.colourChange = 0;
	};

	// create LifeHistory overlay colours
	Life.prototype.createLHOverlayColours = function(/** @type {Array<number>} */ colourList, /** @type {Int32Array} */ customColours) {
		var	/** @type {Uint8Array} */ redChannel = this.redChannel,
			/** @type {Uint8Array} */ greenChannel = this.greenChannel,
			/** @type {Uint8Array} */ blueChannel = this.blueChannel,

			// look up the [R]History state translation map
			/** @type {Array<number>} */ stateMap = ViewConstants.stateMap,
			/** @type {number} */ i = 0;

		// create default colours
		for (i = 0; i < colourList.length; i += 1) {
			// check if a custom colour is defined
			if (customColours && customColours[i] !== -1) {
				// use the custom colour
				redChannel[128 + stateMap[i]] = customColours[i] >> 16;
				greenChannel[128 + stateMap[i]] = (customColours[i] >> 8) & 255;
				blueChannel[128 + stateMap[i]] = customColours[i] & 255;
			} else {
				// use the library colour
				redChannel[128 + stateMap[i]] = colourList[i] >> 16;
				greenChannel[128 + stateMap[i]] = (colourList[i] >> 8) & 255;
				blueChannel[128 + stateMap[i]] = colourList[i] & 255;
			}
		}
	};

	// create pixel colours
	Life.prototype.createPixelColours = function(/** @type {number} */ brightness) {
		// initialize colours
		this.pixelColours.fill(0xffffffff);

		switch (this.cellRenderer) {
			case LifeConstants.shaderCellAge:
				this.createPixelColoursNormal(brightness);
				break;

			case LifeConstants.shaderRainbow:
				this.createPixelColoursRainbow(brightness);
				break;

			case LifeConstants.shaderNeighbourCount:
				this.createPixelColoursNeighbourCount(brightness);
				break;

			case LifeConstants.shaderBasic:
				// use normal pixel colours
				this.createPixelColoursNormal(brightness);
				break;
		}
	};

	// create rainbow pixel colours
	Life.prototype.createPixelColoursRainbow = function(/** @type {number} */ brightness) {
		var	/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {number} */ i = 0,
			/** @type {number} */ r = 255,
			/** @type {number} */ g = 15,
			/** @type {number} */ b = 15,
			/** @type {number} */ alpha = 255,
			/** @type {number} */ s = 0,
			/** @type {number} */ steps = 240 / 6,
			/** @type {number} */ inc = 0,
			/** @type {number} */ amount = (240 / steps),
			/** @type {Array<Array<number>>} */ incs = [[0, 1, 0], [-1, 0, 0], [0, 0, 1], [0, -1, 0], [1, 0, 0], [0, 0, -1]],
			/** @type {Uint8Array} */ redChannel = this.redChannel,
			/** @type {Uint8Array} */ greenChannel = this.greenChannel,
			/** @type {Uint8Array} */ blueChannel = this.blueChannel,
			/** @type {Array<string>} */ colourStrings = this.cellColourStrings,
			/** @type {boolean} */ needStrings = (this.isHex && !this.forceRectangles) || (this.isTriangular && !this.forceRectangles),
			/** @type {number} */ gridLineRaw = this.gridLineRaw,
			/** @type {number} */ gridLineBoldRaw = this.gridLineBoldRaw;

		// create rainbow colours
		for (i = 1; i <= 240; i += 1) {
			if (this.littleEndian) {
				pixelColours[i] = (alpha << 24) | ((b * brightness) << 16) | ((g * brightness) << 8) | (r * brightness);
			} else {
				pixelColours[i] = ((r * brightness) << 24) | ((g * brightness) << 16) | ((b * brightness) << 8) | alpha;
			}

			r += incs[inc][0] * amount;
			g += incs[inc][1] * amount;
			b += incs[inc][2] * amount;

			s += 1;
			if (s === steps) {
				s = 0;
				inc += 1;
			}
		}

		// spread across colours 64 to 127 to match renderer
		for (i = 0; i < 128; i += 1) {
			s = ((i + 1) * 240 / 128) | 0;
			pixelColours[i] = pixelColours[s];
		}

		// move to start at 64 and create colour strings if needed for hexagons or triangles
		for (i = 127; i >= 0; i -= 1) {
			pixelColours[i + 64] = pixelColours[i];
			if (needStrings) {
				if (this.littleEndian) {
					r = pixelColours[i] & 255;
					g = (pixelColours[i] >> 8) & 255;
					b = (pixelColours[i] >> 16) & 255;
				} else {
					r = (pixelColours[i] >> 24);
					g = (pixelColours[i] >> 16) & 255;
					b = (pixelColours[i] >> 8) & 255;
				}
				colourStrings[i + 64] = "#" + (0x1000000 + ((r << 16) + (g << 8) + b)).toString(16).substring(1);
			}
		}

		// create background colour
		if (this.littleEndian) {
			pixelColours[0] = (alpha << 24) | ((blueChannel[0] * brightness) << 16) | ((greenChannel[0] * brightness) << 8) | (redChannel[0] * brightness);
		} else {
			pixelColours[0] = ((redChannel[0] * brightness) << 24) | ((greenChannel[0] * brightness) << 16) | ((blueChannel[0] * brightness) << 8) | alpha;
		}

		// create grid line colours
		if (this.littleEndian) {
			this.gridLineColour = (alpha << 24) | ((gridLineRaw & 255) << 16) | (((gridLineRaw >> 8) & 255) << 8) | (gridLineRaw >> 16);
			this.gridLineBoldColour = (alpha << 24) | ((gridLineBoldRaw & 255) << 16) | (((gridLineBoldRaw >> 8) & 255) << 8) | (gridLineBoldRaw >> 16);
		} else {
			this.gridLineColour = ((gridLineRaw >> 16) << 24) | (((gridLineRaw >> 8) & 255) << 16) | ((gridLineRaw & 255) << 8) | alpha;
			this.gridLineBoldColour = ((gridLineBoldRaw >> 16) << 24) | (((gridLineBoldRaw >> 8) & 255) << 16) | ((gridLineBoldRaw & 255) << 8) | alpha;
		}

		// create bounded grid border colour
		if (this.boundedGridType !== -1) {
			i = this.boundedBorderColour;
			if (this.littleEndian) {
				pixelColours[i] = (alpha << 24) | ((blueChannel[i] * brightness) << 16) | ((greenChannel[i] * brightness) << 8) | (redChannel[i] * brightness);
			} else {
				pixelColours[i] = ((redChannel[i] * brightness) << 24) | ((greenChannel[i] * brightness) << 16) | ((blueChannel[i] * brightness) << 8) | alpha;
			}
			if (needStrings) {
				colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
			}
		}

		// create off grid colour string
		colourStrings[256] = this.boundaryColourString;
	};

	// create rainbow pixel colours
	Life.prototype.createPixelColoursNeighbourCount = function(/** @type {number} */ brightness) {
		var	/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {number} */ i = 0,
			/** @type {number} */ r = 255,
			/** @type {number} */ g = 15,
			/** @type {number} */ b = 15,
			/** @type {number} */ alpha = 255,
			/** @type {number} */ s = 0,
			/** @type {number} */ steps = 240 / 6,
			/** @type {number} */ inc = 0,
			/** @type {number} */ amount = (240 / steps),
			/** @type {Array<number>} */ temp = [],
			/** @type {Array<Array<number>>} */ incs = [[0, 1, 0], [-1, 0, 0], [0, 0, 1], [0, -1, 0], [1, 0, 0], [0, 0, -1]],
			/** @type {Uint8Array} */ redChannel = this.redChannel,
			/** @type {Uint8Array} */ greenChannel = this.greenChannel,
			/** @type {Uint8Array} */ blueChannel = this.blueChannel,
			/** @type {Array<string>} */ colourStrings = this.cellColourStrings,
			/** @type {boolean} */ needStrings = (this.isHex && !this.forceRectangles) || (this.isTriangular && !this.forceRectangles),
			/** @type {number} */ gridLineRaw = this.gridLineRaw,
			/** @type {number} */ gridLineBoldRaw = this.gridLineBoldRaw;

		// create background colour
		if (this.littleEndian) {
			pixelColours[0] = (alpha << 24) | ((blueChannel[0] * brightness) << 16) | ((greenChannel[0] * brightness) << 8) | (redChannel[0] * brightness);
		} else {
			pixelColours[0] = ((redChannel[0] * brightness) << 24) | ((greenChannel[0] * brightness) << 16) | ((blueChannel[0] * brightness) << 8) | alpha;
		}

		// create grid line colours
		if (this.littleEndian) {
			this.gridLineColour = (alpha << 24) | ((gridLineRaw & 255) << 16) | (((gridLineRaw >> 8) & 255) << 8) | (gridLineRaw >> 16);
			this.gridLineBoldColour = (alpha << 24) | ((gridLineBoldRaw & 255) << 16) | (((gridLineBoldRaw >> 8) & 255) << 8) | (gridLineBoldRaw >> 16);
		} else {
			this.gridLineColour = ((gridLineRaw >> 16) << 24) | (((gridLineRaw >> 8) & 255) << 16) | ((gridLineRaw & 255) << 8) | alpha;
			this.gridLineBoldColour = ((gridLineBoldRaw >> 16) << 24) | (((gridLineBoldRaw >> 8) & 255) << 16) | ((gridLineBoldRaw & 255) << 8) | alpha;
		}

		// create bounded grid border colour
		if (this.boundedGridType !== -1) {
			i = this.boundedBorderColour;
			if (this.littleEndian) {
				pixelColours[i] = (alpha << 24) | ((blueChannel[i] * brightness) << 16) | ((greenChannel[i] * brightness) << 8) | (redChannel[i] * brightness);
			} else {
				pixelColours[i] = ((redChannel[i] * brightness) << 24) | ((greenChannel[i] * brightness) << 16) | ((blueChannel[i] * brightness) << 8) | alpha;
			}
			if (needStrings) {
				colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
			}
		}

		// create off grid colour string
		colourStrings[256] = this.boundaryColourString;

		// create rainbow colours
		inc = 0;
		s = 0;
		for (i = 1; i <= 240; i += 1) {
			if (this.littleEndian) {
				pixelColours[i] = (alpha << 24) | ((b * brightness) << 16) | ((g * brightness) << 8) | (r * brightness);
			} else {
				pixelColours[i] = ((r * brightness) << 24) | ((g * brightness) << 16) | ((b * brightness) << 8) | alpha;
			}
			r += incs[inc][0] * amount;
			g += incs[inc][1] * amount;
			b += incs[inc][2] * amount;
			s += 1;
			if (s === steps) {
				s = 0;
				inc += 1;
			}
		}

		// get the 24 required
		for (i = 0; i < 25; i += 1) {
			s = ((i + 1) * 240 / 25) | 0;

			temp[i] = pixelColours[s - 1];
		}

		// move them starting at the alive index
		for (i = 0; i < 25; i += 1) {
			pixelColours[64 + i] = temp[i];
		}

		// check theme
		switch (this.colourTheme) {
			case 0: // Mono
				for (i = 0; i < 25; i += 1) {
					var j = (i * 6) + 105;
					pixelColours[64 + i] = (255 << 24) | (j << 16) | (j << 8) | j;
				}
				break;

			case 2: // Fire
				pixelColours[64] = 0xff000033;
				pixelColours[64 + 4] = 0xff003399;
				pixelColours[64 + 8] = 0xff0055ff;
				pixelColours[64 + 12] = 0xff00ffff;
				pixelColours[64 + 16] = 0xffffffff;
				pixelColours[64 + 20] = 0xffccffff;
				pixelColours[64 + 24] = 0xffccffff;
				for (i = 1; i < 4; i += 1) {
					pixelColours[64 + i] = pixelColours[64];
					pixelColours[68 + i] = pixelColours[68];
					pixelColours[72 + i] = pixelColours[72];
					pixelColours[76 + i] = pixelColours[76];
					pixelColours[80 + i] = pixelColours[80];
					pixelColours[84 + i] = pixelColours[84];
				}
				break;

			case 3: // Poison
				pixelColours[64] = 0xff660033;
				pixelColours[64 + 4] = 0xffff4400;
				pixelColours[64 + 8] = 0xffffff00;
				pixelColours[64 + 12] = 0xffff00ff;
				pixelColours[64 + 16] = 0xffff66ff;
				pixelColours[64 + 20] = 0xffffffff;
				pixelColours[64 + 24] = 0xffffffff;
				for (i = 1; i < 4; i += 1) {
					pixelColours[64 + i] = pixelColours[64];
					pixelColours[68 + i] = pixelColours[68];
					pixelColours[72 + i] = pixelColours[72];
					pixelColours[76 + i] = pixelColours[76];
					pixelColours[80 + i] = pixelColours[80];
					pixelColours[84 + i] = pixelColours[84];
				}
				break;

			case 4: // Yellow
				pixelColours[64] = 0xff331100;
				pixelColours[64 + 4] = 0xff664400;
				pixelColours[64 + 8] = 0xff00ff00;
				pixelColours[64 + 12] = 0xff00ffff;
				pixelColours[64 + 16] = 0xff00aaff;
				pixelColours[64 + 20] = 0xffffffff;
				pixelColours[64 + 24] = 0xffffffff;
				for (i = 1; i < 4; i += 1) {
					pixelColours[64 + i] = pixelColours[64];
					pixelColours[68 + i] = pixelColours[68];
					pixelColours[72 + i] = pixelColours[72];
					pixelColours[76 + i] = pixelColours[76];
					pixelColours[80 + i] = pixelColours[80];
					pixelColours[84 + i] = pixelColours[84];
				}
				break;

			case 5:
				pixelColours[64] = 0xffaa0000;
				pixelColours[64 + 4] = 0xff00aa00;
				pixelColours[64 + 8] = 0xff00aaaa;
				pixelColours[64 + 12] = 0xff0077ff;
				pixelColours[64 + 16] = 0xff0000ff;
				pixelColours[64 + 20] = 0xffffffff;
				pixelColours[64 + 24] = 0xffffffff;
				for (i = 1; i < 4; i += 1) {
					pixelColours[64 + i] = pixelColours[64];
					pixelColours[68 + i] = pixelColours[68];
					pixelColours[72 + i] = pixelColours[72];
					pixelColours[76 + i] = pixelColours[76];
					pixelColours[80 + i] = pixelColours[80];
					pixelColours[84 + i] = pixelColours[84];
				}
				break;
		}

		// create the colour strings for hexagons or triangles if required
		if (needStrings) {
			for (i = 64; i < 64 + 25; i += 1) {
				if (this.littleEndian) {
					r = pixelColours[i] & 255;
					g = (pixelColours[i] >> 8) & 255;
					b = (pixelColours[i] >> 16) & 255;
				} else {
					r = (pixelColours[i] >> 24);
					g = (pixelColours[i] >> 16) & 255;
					b = (pixelColours[i] >> 8) & 255;
				}
				colourStrings[i + 64] = "#" + (0x1000000 + ((r << 16) + (g << 8) + b)).toString(16).substring(1);
			}
		}
	};

	// create pixel colours
	Life.prototype.createPixelColoursNormal = function(/** @type {number} */ brightness) {
		var	/** @type {Uint8Array} */ redChannel = this.redChannel,
			/** @type {Uint8Array} */ greenChannel = this.greenChannel,
			/** @type {Uint8Array} */ blueChannel = this.blueChannel,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {number} */ gridLineRaw = this.gridLineRaw,
			/** @type {number} */ gridLineBoldRaw = this.gridLineBoldRaw,
			/** @type {Array<string>} */ colourStrings = this.cellColourStrings,
			/** @type {boolean} */ needStrings = (this.isHex && !this.forceRectangles) || (this.isTriangular && !this.forceRectangles) || this.iconsAvailable,
			/** @type {number} */ alpha = 255,
			/** @type {number} */ i = 0;

		// check for Generations, HROT, [R]Super or [R]Extended
		if (this.multiNumStates > 2) {
			if (this.littleEndian) {
				pixelColours[0] = (alpha << 24) | (blueChannel[0] << 16) | (greenChannel[0] << 8) | redChannel[0];
				if (needStrings) {
					colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
				}

				if (this.isSuper || this.isExtended || this.isHROT || this.isPCA) {
					for (i = 1; i <= this.multiNumStates + this.historyStates; i += 1) {
						pixelColours[i] = (alpha << 24) | ((blueChannel[i] * brightness) << 16) | ((greenChannel[i] * brightness) << 8) | (redChannel[i] * brightness);
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}
				} else {
					for (i = 1; i <= this.historyStates; i += 1) {
						pixelColours[i] = (alpha << 24) | (blueChannel[i] << 16) | (greenChannel[i] << 8) | redChannel[i];
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}

					for (i = this.historyStates + 1; i <= this.multiNumStates + this.historyStates; i += 1) {
						pixelColours[i] = (alpha << 24) | ((blueChannel[i] * brightness) << 16) | ((greenChannel[i] * brightness) << 8) | (redChannel[i] * brightness);
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}
				}
			} else {
				pixelColours[0] = (redChannel[0] << 24) | (greenChannel[i] << 16) | (blueChannel[i] << 8) | alpha;
				if (needStrings) {
					colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
				}

				if (this.isSuper || this.isExtended || this.isHROT || this.isPCA) {
					for (i = 1; i <= this.multiNumStates + this.historyStates; i += 1) {
						pixelColours[i] = ((redChannel[i] * brightness) << 24) | ((greenChannel[i] * brightness) << 16) | ((blueChannel[i] * brightness) << 8) | alpha;
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}
				} else {
					for (i = 1; i <= this.historyStates; i += 1) {
						pixelColours[i] = (redChannel[i] << 24) | (greenChannel[i] << 16) | (blueChannel[i] << 8) | alpha;
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}

						for (i = this.historyStates + 1; i <= this.multiNumStates + this.historyStates; i += 1) {
							pixelColours[i] = ((redChannel[i] * brightness) << 24) | ((greenChannel[i] * brightness) << 16) | ((blueChannel[i] * brightness) << 8) | alpha;
							if (needStrings) {
								colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
							}
						}
					}
				}
			}
		} else {
			if (this.isRuleTree) {
				// 2-state RuleLoader
				if (this.littleEndian) {
					pixelColours[0] = (alpha << 24) | (blueChannel[0] << 16) | (greenChannel[0] << 8) | redChannel[0];
					pixelColours[1] = (alpha << 24) | ((blueChannel[1] * brightness) << 16) | ((greenChannel[1] * brightness) << 8) | (redChannel[1] * brightness);
				} else {
					pixelColours[0] = (redChannel[0] << 24) | (greenChannel[0] << 16) | (blueChannel[0] << 8) | alpha;
					pixelColours[1] = ((redChannel[1] * brightness) << 24) | ((greenChannel[1] * brightness) << 16) | ((blueChannel[1] * brightness) << 8) | alpha;
				}
				if (needStrings) {
					colourStrings[0] = "#" + (0x1000000 + ((redChannel[0] << 16) + (greenChannel[0] << 8) + blueChannel[0])).toString(16).substring(1);
					colourStrings[1] = "#" + (0x1000000 + ((redChannel[1] << 16) + (greenChannel[1] << 8) + blueChannel[1])).toString(16).substring(1);
				}
			} else {
				// create pixels from rgb and brightness
				if (this.littleEndian) {
					// create dead colours
					for (i = 0; i < this.aliveStart; i += 1) {
						pixelColours[i] = (alpha << 24) | (blueChannel[i] << 16) | (greenChannel[i] << 8) | redChannel[i];
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}

					// create alive colours
					for (i = this.aliveStart; i <= this.aliveMax; i += 1) {
						pixelColours[i] = (alpha << 24) | ((blueChannel[i] * brightness) << 16) | ((greenChannel[i] * brightness) << 8) | (redChannel[i] * brightness);
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}

					// create remaining multi-state colours
					for (i = this.aliveMax + 1; i < 256; i += 1) {
						pixelColours[i] = (alpha << 24) | ((blueChannel[i] * brightness) << 16) | ((greenChannel[i] * brightness) << 8) | (redChannel[i] * brightness);
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}
				} else {
					// create dead colours
					for (i = 0; i < this.aliveStart; i += 1) {
						pixelColours[i] = (redChannel[i] << 24) | (greenChannel[i] << 16) | (blueChannel[i] << 8) | alpha;
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}

					// create alive colours
					for (i = this.aliveStart; i <= this.aliveMax; i += 1) {
						pixelColours[i] = ((redChannel[i] * brightness) << 24) | ((greenChannel[i] * brightness) << 16) | ((blueChannel[i] * brightness) << 8) | alpha;
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}

					// create remaining multi-state colours
					for (i = this.aliveMax + 1; i < 256; i += 1) {
						pixelColours[i] = ((redChannel[i] * brightness) << 24) | ((greenChannel[i] * brightness) << 16) | ((blueChannel[i] * brightness) << 8) | alpha;
						if (needStrings) {
							colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
						}
					}
				}
			}
		}

		// create grid line colours
		if (this.littleEndian) {
			this.gridLineColour = (alpha << 24) | ((gridLineRaw & 255) << 16) | (((gridLineRaw >> 8) & 255) << 8) | (gridLineRaw >> 16);
			this.gridLineBoldColour = (alpha << 24) | ((gridLineBoldRaw & 255) << 16) | (((gridLineBoldRaw >> 8) & 255) << 8) | (gridLineBoldRaw >> 16);
		} else {
			this.gridLineColour = ((gridLineRaw >> 16) << 24) | (((gridLineRaw >> 8) & 255) << 16) | ((gridLineRaw & 255) << 8) | alpha;
			this.gridLineBoldColour = ((gridLineBoldRaw >> 16) << 24) | (((gridLineBoldRaw >> 8) & 255) << 16) | ((gridLineBoldRaw & 255) << 8) | alpha;
		}

		// create bounded grid border colour
		if (this.boundedGridType !== -1) {
			i = this.boundedBorderColour;
			if (this.littleEndian) {
				pixelColours[i] = (alpha << 24) | ((blueChannel[i] * brightness) << 16) | ((greenChannel[i] * brightness) << 8) | (redChannel[i] * brightness);
			} else {
				pixelColours[i] = ((redChannel[i] * brightness) << 24) | ((greenChannel[i] * brightness) << 16) | ((blueChannel[i] * brightness) << 8) | alpha;
			}
			if (needStrings) {
				colourStrings[i] = "#" + (0x1000000 + ((redChannel[i] << 16) + (greenChannel[i] << 8) + blueChannel[i])).toString(16).substring(1);
			}

			// for [R]Super or [R]Extended (TBD check [R]Extended) the bounded colour will become 31 at Zoom < 1 so set this colour to the bounded grid cell colour
			if (this.isSuper || this.isExtended) {
				pixelColours[31] = pixelColours[i];
			}
		}

		// create off grid colour string
		colourStrings[256] = this.boundaryColourString;
	};

	// set the bounded grid border cell
	Life.prototype.setBoundedGridBorderCell = function() {
		var	/** @type {Uint8Array} */ redChannel = this.redChannel,
			/** @type {Uint8Array} */ greenChannel = this.greenChannel,
			/** @type {Uint8Array} */ blueChannel = this.blueChannel;

		// check for bounded grid
		if (this.boundedGridType !== -1 && (this.multiNumStates + this.historyStates === 256)) {
			this.boundedBorderColour = 2;
		} else {
			this.boundedBorderColour = 255;
		}

		// create bounded grid border colour if specified
		if (this.boundedGridType !== -1 && (this.multiNumStates + this.historyStates < 256)) {
			if (this.littleEndian) {
				redChannel[this.boundedBorderColour] = this.boundedColour & 255;
				greenChannel[this.boundedBorderColour] = (this.boundedColour >> 8) & 255;
				blueChannel[this.boundedBorderColour] = (this.boundedColour >> 16) & 255;
			} else {
				redChannel[this.boundedBorderColour] = this.boundedColour >> 24;
				greenChannel[this.boundedBorderColour] = (this.boundedColour >> 16) & 255;
				blueChannel[this.boundedBorderColour] = (this.boundedColour >> 8) & 255;
			}
		}
	};

	// clear the life grids (or just the bit grids if specified)
	Life.prototype.clearGrids = function(/** @type {boolean} */ bitOnly) {
		var	/** @type {Array<Uint8Array>} */ grid = this.grid,
			/** @type {Array<Uint8Array>} */ nextGrid = this.nextGrid,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint8Array>} */ nextColourGrid = this.nextColourGrid,
			/** @type {Array<Uint8Array>} */ smallColourGrid = this.smallColourGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {Array<Uint8Array>} */ smallOverlayGrid = this.smallOverlayGrid,
			/** @type {Array<Uint16Array>} */ tileGrid = this.tileGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ nextTileGrid = this.nextTileGrid,
			/** @type {number} */ unoccupied = this.unoccupied;

		// clear each cell
		grid.whole.fill(0);
		nextGrid.whole.fill(0);
		if (!bitOnly) {
			colourGrid.whole.fill(unoccupied);
			if (nextColourGrid) {
				nextColourGrid.whole.fill(unoccupied);
			}
			smallColourGrid.whole.fill(unoccupied);
			if (overlayGrid) {
				overlayGrid.whole.fill(unoccupied);
				smallOverlayGrid.whole.fill(unoccupied);
			}
		}

		// clear the tiles
		tileGrid.whole.fill(0);
		nextTileGrid.whole.fill(0);
		if (!bitOnly) {
			colourTileGrid.whole.fill(0);
			colourTileHistoryGrid.whole.fill(0);
		}
	};

	// create the triangular life index
	Life.prototype.createTriangularIndex = function(/** @type {Uint8Array} */ indexLookupTriangular, /** @type {Uint8Array} */ ruleArray) {
		var	/** @type {number} */ n = LifeConstants.hashTriangular,  // TBD will be Double when 2-cell lookup implemented
			/** @type {number} */ i = 0;

		// create each hash entry
		for (i = 0; i < n; i += 1) {  // TBD set()?
			indexLookupTriangular[i] = ruleArray[i];
		}
	};

	// create the 6x3 lookup life index from the 3x3 index
	Life.prototype.createLifeIndex63 = function(/** @type {Uint8Array} */ indexLookup63, /** @type {Uint8Array} */ indexLookup33) {
		var	/** @type {number} */ n = LifeConstants.hash63,
			/** @type {number} */ i = 0,
			/** @type {number} */ v = 0;

		// create each hash entry
		for (i = 0; i < n; i += 1) {
			// lookup four bits
			v = indexLookup33[((i >> 9) & 448) | ((i >> 6) & 56) | ((i >> 3) & 7)] << 3;
			v |= indexLookup33[((i >> 8) & 448) | ((i >> 5) & 56) | ((i >> 2) & 7)] << 2;
			v |= indexLookup33[((i >> 7) & 448) | ((i >> 4) & 56) | ((i >> 1) & 7)] << 1;
			v |= indexLookup33[((i >> 6) & 448) | ((i >> 3) & 56) | (i & 7)];

			// write into 6x3 array
			indexLookup63[i] = v;
		}
	};

	// create PCA index
	Life.prototype.createPCAIndex = function(/** @type {Uint16Array} */ index, /** @type {Uint8Array} */ ruleArray, /** @type {boolean} */ reverse) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ value = 0,
			/** @type {Uint8Array} */ reverseArray = null;

		// index lookup is 4 x 4bit values for n, e, s, w
		// n3 n2 n1 n0 e3 e2 e1 e0 s3 s2 s1 s0 w3 w2 w1 w0
		// 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
		//           2        3        0       1
		if (reverse) {
			reverseArray = new Uint8Array(16);

			// create the reverse transitions
			for (i = 0; i < 16; i += 1) {
				value = ruleArray[i];
				reverseArray[value] = i;
			}

			// create the lookup index
			for (i = 0; i < LifeConstants.hashPCA; i += 1) {
				value = (reverseArray[i >> 12] & 1) << 2;
				value |= (reverseArray[(i >> 8) & 15] & 2) << 2;
				value |= (reverseArray[(i >> 4) & 15] & 4) >> 2;
				value |= (reverseArray[i & 15] & 8) >> 2;
				index[i] = value;
			}
		} else {
			for (i = 0; i < LifeConstants.hashPCA; i += 1) {
				value = ((i >> 12) & 1) << 2;
				value |= ((i >> 8) & 2) << 2;
				value |= ((i >> 4) & 4) >> 2;
				value |= (i & 8) >> 2;
				index[i] = ruleArray[value];
			}
		}
	};

	// create Margolus index
	Life.prototype.createMargolusIndex = function(/** @type {Uint16Array} */ index, /** @type {Uint8Array} */ ruleArray) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ row0 = 0,
			/** @type {number} */ row1 = 0,
			/** @type {number} */ value = 0,
			/** @type {number} */ mask = 0,
			/** @type {number} */ shift = 0,
			/** @type {number} */ lookup = 0,
			/** @type {number} */ dest0 = 0,
			/** @type {number} */ dest1 = 0;

		// index holds four 2x2 blocks
		// 00 00  01 01  02 02  03 03
		// 00 00  01 01  02 02  03 03
		for (i = 0; i < LifeConstants.hashMargolus; i += 1) {
			row1 = i >> 8;
			row0 = i & 255;
			mask = 192;
			shift = 6;
			dest0 = 0;
			dest1 = 0;
			for (j = 0; j < 4; j += 1) {
				// compute the lookup value
				if (shift === 0) {
					value = ((row0 & mask) << 2) | (row1 & mask);
				} else {
					value = ((row0 & mask) >> (shift - 2)) | ((row1 & mask) >> shift);
				}

				// get the mapping
				value = ((value & 8) >> 1) | ((value & 4) << 1) | ((value & 2) >> 1) | ((value & 1) << 1);
				lookup = ruleArray[value];
				lookup = ((lookup & 8) >> 1) | ((lookup & 4) << 1) | ((lookup & 2) >> 1) | ((lookup & 1) << 1);

				// update the destination rows
				dest0 <<= 2;
				dest1 <<= 2;
				dest0 |= (lookup >> 2) & 3;
				dest1 |= lookup & 3;

				// next 2x2 item
				mask >>= 2;
				shift -= 2;
			}

			// update the index
			index[i] = (dest1 << 8) | dest0;
		}
	};

	// check whether a margolus rule can reverse and if so reverse it
	/** @returns {boolean} */
	Life.prototype.canReverse = function(/** @type {Uint8Array} */ rule, /** @type {boolean} */ copy) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ states = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ value = 0,
			/** @type {boolean} */ result = true,
			/** @type {Uint8Array} */ temp = new Uint8Array(16);

		// make sure there are no duplicate transitions
		i = 0;
		while (result && i < 16) {
			bit = 1 << rule[i];
			if ((states & bit) !== 0) {
				result = false;
			} else {
				states |= bit;
				i += 1;
			}
		}

		// check if reversible
		if (result) {
			// reverse rule
			for (i = 0; i < 16; i += 1) {
				value = rule[i];
				temp[value] = i;
			}

			// copy to original rule
			if (copy) {
				for (i = 0; i < temp.length; i += 1) {
					rule[i] = temp[i];
				}
			}
		}

		return result;
	};

	// create non-strobing Margolus alternate rules
	Life.prototype.createNonStrobingAlternates = function(/** @type {Uint8Array} */ ruleArray, /** @type {Uint8Array} */ ruleAltArray) {
		var	/** @type {number} */ i = 0;

		// create two alternate arrays
		for (i = 0; i < 16; i += 1) {
			ruleAltArray[i] = ruleArray[15 - i];
		}

		for (i = 0; i < 16; i += 1) {
			ruleArray[i] = 15 - ruleArray[i];
		}
	};

	// create rule table lookup step
	Life.prototype.createRuleTableLookupStep = function() {
		// get number of cells in neighbourhood
		var	/** @type {number} */ bitsNeeded = 0,
			/** @type {number} */ states = this.multiNumStates,
			/** @type {number} */ i = 0,
			/** @type {number} */ state = 0,

			// cells in neighbourhood
			/** @type {number} */ nw = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ sw = 0,

			// cell offsets in index in bits
			/** @type {number} */ nwi = 0,
			/** @type {number} */ ni = 0,
			/** @type {number} */ nei = 0,
			/** @type {number} */ ei = 0,
			/** @type {number} */ ci = 0,
			/** @type {number} */ wi = 0,
			/** @type {number} */ swi = 0,
			/** @type {number} */ si = 0,
			/** @type {number} */ sei = 0,

			// cell partial index
			/** @type {number} */ nwx = 0,
			/** @type {number} */ nx = 0,
			/** @type {number} */ nex = 0,
			/** @type {number} */ wx = 0,
			/** @type {number} */ ex = 0,
			/** @type {number} */ swx = 0,
			/** @type {number} */ sex = 0,

			// cell lookup
			/** @type {Array<Array<Uint32Array>>} */ lut = this.ruleTableLUT,
			/** @type {Array<Uint32Array>} */ lut0 = null,
			/** @type {Array<Uint32Array>} */ lut1 = null,
			/** @type {Array<Uint32Array>} */ lut2 = null,
			/** @type {Array<Uint32Array>} */ lut3 = null,
			/** @type {Array<Uint32Array>} */ lut4 = null,
			/** @type {Array<Uint32Array>} */ lut5 = null,
			/** @type {Array<Uint32Array>} */ lut6 = null,
			/** @type {Array<Uint32Array>} */ lut7 = null,
			/** @type {Array<Uint32Array>} */ lut8 = null,
			/** @type {Uint32Array} */ lutnw = null,
			/** @type {Uint32Array} */ lutn = null,
			/** @type {Uint32Array} */ lutne = null,
			/** @type {Uint32Array} */ lutw = null,
			/** @type {Uint32Array} */ lutc = null,
			/** @type {Uint32Array} */ lute = null,
			/** @type {Uint32Array} */ lutsw = null,
			/** @type {Uint32Array} */ luts = null,
			/** @type {Uint32Array} */ lutse = null,
			/** @type {Uint8Array} */ output = this.ruleTableOutput,
			/** @type {number} */ nCompressed = this.ruleTableCompressedRules,
			/** @type {number} */ isMatch = 0,
			/** @type {number} */ iRuleC = 0,
			/** @type {number} */ iBit = 0,
			/** @type {number} */ mask = 0,

			// mods for index
			/** @type {number} */ states2 = states * states,
			/** @type {number} */ states3 = states2 * states,
			/** @type {number} */ states4 = states3 * states,
			/** @type {number} */ states5 = states4 * states,

			/** @type {number} */ index = 0;

		// compute how many bits needed for states
		i = 0;
		while ((1 << i) < states) {
			i += 1;
		}

		// compute bits needed as cells in neighbourhood * state bits
		switch (this.ruleTableNeighbourhood) {
		case PatternConstants.ruleTableMoore:
			bitsNeeded = 9 * i;
			break;

		default:
			bitsNeeded = LifeConstants.maxRuleTreeLookupBits + 1;
			break;
		}

		// check lookup is small enough for lookup table
		if (bitsNeeded <= LifeConstants.maxRuleTreeLookupBits) {
			// build lookup table
			if (this.ruleLoaderLookup === null) {
				this.ruleLoaderLookup = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, (1 << bitsNeeded), "Life.ruleTreeLookup", Controller.useWASM));
				this.ruleLoaderLookupBits = i;
			}

			if (this.ruleTableNeighbourhood === PatternConstants.ruleTableMoore) {
				// Moore
				// create the bit shifts
				nwi = 0;
				wi = nwi + i;
				swi = wi + i;
				ni = swi + i;
				ci = ni + i;
				si = ci + i;
				nei = si + i;
				ei = nei + i;
				sei = ei + i;

				// get the lookups
				lut0 = lut[0];
				lut1 = lut[1];
				lut2 = lut[2];
				lut3 = lut[3];
				lut4 = lut[4];
				lut5 = lut[5];
				lut6 = lut[6];
				lut7 = lut[7];
				lut8 = lut[8];

				// create the entries
				nw = (this.ruleLoaderStep / states4) | 0;
				nwx = (nw << nwi);
				lutnw = lut8[nw];
				ne = ((this.ruleLoaderStep % states4) / states3) | 0;
				nex = nwx | (ne << nei);
				lutne = lut2[ne];
				sw = ((this.ruleLoaderStep % states3) / states2) | 0;
				swx = nex | (sw << swi);
				lutsw = lut6[sw];
				se = ((this.ruleLoaderStep % states2) / states) | 0;
				sex = swx | (se << sei);
				lutse = lut4[se];
				n = (this.ruleLoaderStep % states);
				nx = sex | (n << ni);
				lutn = lut1[n];
				for (w = 0; w < states; w += 1) {
					wx = nx | (w << wi);
					lutw = lut7[w];
					for (e = 0; e < states; e += 1) {
						ex = wx | (e << ei);
						lute = lut3[e];
						for (s = 0; s < states; s += 1) {
							luts = lut5[s];
							index = ex | (s << si);
							for (c = 0; c < states; c += 1) {
								lutc = lut0[c];
								state = c;

								// search for the match
								for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
									isMatch = lutc[iRuleC] & lutn[iRuleC];
									if (isMatch) {
										isMatch &= lutne[iRuleC] & lute[iRuleC];
										if (isMatch) {
											isMatch &= lutse[iRuleC] & luts[iRuleC];
											if (isMatch) {
												isMatch &= lutsw[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
												if (isMatch) {
													iBit = 0;
													mask = 1;
													while (!(isMatch & mask)) {
														iBit += 1;
														mask <<= 1;
													}
													state = output[(iRuleC << 5) + iBit];
													break;
												}
											}
										}
									}
								}

								// add to the lookup table
								this.ruleLoaderLookup[index + (c << ci)] = state;
							}
						}
					}
				}

				// increment step
				this.ruleLoaderStep += 1;
				if (this.ruleLoaderStep === states5) {
					this.ruleLoaderStep = -1;
				}
			}
		} else {
			// too many bits for lookup table
			this.ruleLoaderLookup = null;
		}
	};

	// create rule table lookup
	Life.prototype.createRuleTableLookup = function() {
		// get number of cells in neighbourhood
		var	/** @type {number} */ bitsNeeded = 0,
			/** @type {number} */ states = this.multiNumStates,
			/** @type {number} */ i = 0,
			/** @type {number} */ state = 0,

			// cells in neighbourhood
			/** @type {number} */ nw = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ sw = 0,

			// cell offsets in index in bits
			/** @type {number} */ nwi = 0,
			/** @type {number} */ ni = 0,
			/** @type {number} */ nei = 0,
			/** @type {number} */ ei = 0,
			/** @type {number} */ ci = 0,
			/** @type {number} */ wi = 0,
			/** @type {number} */ swi = 0,
			/** @type {number} */ si = 0,
			/** @type {number} */ sei = 0,

			// cell partial index
			/** @type {number} */ nwx = 0,
			/** @type {number} */ nx = 0,
			/** @type {number} */ nex = 0,
			/** @type {number} */ wx = 0,
			/** @type {number} */ ex = 0,
			/** @type {number} */ swx = 0,
			/** @type {number} */ sex = 0,

			// cell lookup
			/** @type {Array<Array<Uint32Array>>} */ lut = this.ruleTableLUT,
			/** @type {Array<Uint32Array>} */ lut0 = null,
			/** @type {Array<Uint32Array>} */ lut1 = null,
			/** @type {Array<Uint32Array>} */ lut2 = null,
			/** @type {Array<Uint32Array>} */ lut3 = null,
			/** @type {Array<Uint32Array>} */ lut4 = null,
			/** @type {Array<Uint32Array>} */ lut5 = null,
			/** @type {Array<Uint32Array>} */ lut6 = null,
			/** @type {Array<Uint32Array>} */ lut7 = null,
			/** @type {Array<Uint32Array>} */ lut8 = null,
			/** @type {Uint32Array} */ lutnw = null,
			/** @type {Uint32Array} */ lutn = null,
			/** @type {Uint32Array} */ lutne = null,
			/** @type {Uint32Array} */ lutw = null,
			/** @type {Uint32Array} */ lutc = null,
			/** @type {Uint32Array} */ lute = null,
			/** @type {Uint32Array} */ lutsw = null,
			/** @type {Uint32Array} */ luts = null,
			/** @type {Uint32Array} */ lutse = null,
			/** @type {Uint8Array} */ output = this.ruleTableOutput,
			/** @type {number} */ nCompressed = this.ruleTableCompressedRules,
			/** @type {number} */ isMatch = 0,
			/** @type {number} */ iRuleC = 0,
			/** @type {number} */ iBit = 0,
			/** @type {number} */ mask = 0,

			/** @type {number} */ index = 0;

		// compute how many bits needed for states
		i = 0;
		while ((1 << i) < states) {
			i += 1;
		}

		// compute bits needed as cells in neighbourhood * state bits
		switch (this.ruleTableNeighbourhood) {
		case PatternConstants.ruleTableVN:
			bitsNeeded = 5 * i;
			break;

		case PatternConstants.ruleTableHex:
			bitsNeeded = 7 * i;
			break;

		case PatternConstants.ruleTableMoore:
			bitsNeeded = 9 * i;
			break;

		case PatternConstants.ruleTableOneD:
			bitsNeeded = 3 * i;
			break;

		default:
			bitsNeeded = LifeConstants.maxRuleTreeLookupBits + 1;
			break;
		}

		// check lookup is small enough for lookup table
		if (bitsNeeded <= LifeConstants.maxRuleTreeLookupBits) {
			// build lookup table
			this.ruleLoaderLookup = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, (1 << bitsNeeded), "Life.ruleTreeLookup", Controller.useWASM));
			this.ruleLoaderLookupBits = i;

			// check neighbourhood
			switch (this.ruleTableNeighbourhood) {
			case PatternConstants.ruleTableVN:
				// von Neumann
				// create the bit shifts
				si = 0;
				ni = si + i;
				ei = ni + i;
				ci = ei + i;
				wi = ci + i;

				// get the lookups
				lut0 = lut[0];
				lut1 = lut[1];
				lut2 = lut[2];
				lut3 = lut[3];
				lut4 = lut[4];

				// create the entries
				for (w = 0; w < states; w += 1) {
					lutw = lut4[w];
					for (e = 0; e < states; e += 1) {
						lute = lut2[e];
						for (n = 0; n < states; n += 1) {
							lutn = lut1[n];
							for (s = 0; s < states; s += 1) {
								luts = lut3[s];
								index = (w << wi) | (e << ei) | (n << ni) | (s << si);
								for (c = 0; c < states; c += 1) {
									lutc = lut0[c];
									state = c;

									// search for the match
									for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
										isMatch = lutc[iRuleC] & lutn[iRuleC];
										if (isMatch) {
											isMatch &= lute[iRuleC] & luts[iRuleC] & lutw[iRuleC];
											if (isMatch) {
												iBit = 0;
												mask = 1;
												while (!(isMatch & mask)) {
													iBit += 1;
													mask <<= 1;
												}
												state = output[(iRuleC << 5) + iBit];
												break;
											}
										}
									}

									// add to the lookup table
									this.ruleLoaderLookup[index + (c << ci)] = state;
								}
							}
						}
					}
				}
				break;

			case PatternConstants.ruleTableMoore:
				// Moore
				// create the bit shifts
				nwi = 0;
				wi = nwi + i;
				swi = wi + i;
				ni = swi + i;
				ci = ni + i;
				si = ci + i;
				nei = si + i;
				ei = nei + i;
				sei = ei + i;

				// get the lookups
				lut0 = lut[0];
				lut1 = lut[1];
				lut2 = lut[2];
				lut3 = lut[3];
				lut4 = lut[4];
				lut5 = lut[5];
				lut6 = lut[6];
				lut7 = lut[7];
				lut8 = lut[8];

				// create the entries
				for (nw = 0; nw < states; nw += 1) {
					nwx = (nw << nwi);
					lutnw = lut8[nw];
					for (ne = 0; ne < states; ne += 1) {
						nex = nwx | (ne << nei);
						lutne = lut2[ne];
						for (sw = 0; sw < states; sw += 1) {
							swx = nex | (sw << swi);
							lutsw = lut6[sw];
							for (se = 0; se < states; se += 1) {
								sex = swx | (se << sei);
								lutse = lut4[se];
								for (n = 0; n < states; n += 1) {
									nx = sex | (n << ni);
									lutn = lut1[n];
									for (w = 0; w < states; w += 1) {
										wx = nx | (w << wi);
										lutw = lut7[w];
										for (e = 0; e < states; e += 1) {
											ex = wx | (e << ei);
											lute = lut3[e];
											for (s = 0; s < states; s += 1) {
												luts = lut5[s];
												index = ex | (s << si);
												for (c = 0; c < states; c += 1) {
													lutc = lut0[c];
													state = c;

													// search for the match
													for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
														isMatch = lutc[iRuleC] & lutn[iRuleC];
														if (isMatch) {
															isMatch &= lutne[iRuleC] & lute[iRuleC];
															if (isMatch) {
																isMatch &= lutse[iRuleC] & luts[iRuleC];
																if (isMatch) {
																	isMatch &= lutsw[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
																	if (isMatch) {
																		iBit = 0;
																		mask = 1;
																		while (!(isMatch & mask)) {
																			iBit += 1;
																			mask <<= 1;
																		}
																		state = output[(iRuleC << 5) + iBit];
																		break;
																	}
																}
															}
														}
													}

													// add to the lookup table
													this.ruleLoaderLookup[index + (c << ci)] = state;
												}
											}
										}
									}
								}
							}
						}
					}
				}
				break;

			case PatternConstants.ruleTableHex:
				// Hexagonal
				// create the bit shifts
				ci = 0;
				si = ci + i;
				ei = si + i;
				wi = ei + i;
				ni = wi + i;
				sei = ni + i;
				nwi = sei + i;

				// get the lookups
				lut0 = lut[0];
				lut1 = lut[1];
				lut2 = lut[2];
				lut3 = lut[3];
				lut4 = lut[4];
				lut5 = lut[5];
				lut6 = lut[6];

				// create the entries
				for (nw = 0; nw < states; nw += 1) {
					nwx = (nw << nwi);
					lutnw = lut6[nw];
					for (se = 0; se < states; se += 1) {
						sex = nwx | (se << sei);
						lutse = lut3[se];
						for (n = 0; n < states; n += 1) {
							nx = sex | (n << ni);
							lutn = lut1[n];
							for (w = 0; w < states; w += 1) {
								wx = nx | (w << wi);
								lutw = lut5[w];
								for (e = 0; e < states; e += 1) {
									ex = wx | (e << ei);
									lute = lut2[e];
									for (s = 0; s < states; s += 1) {
										index = ex | (s << si);
										luts = lut4[s];
										for (c = 0; c < states; c += 1) {
											lutc = lut0[c];
											state = c;

											// search for the match
											for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
												isMatch = lutc[iRuleC] & lutn[iRuleC];
												if (isMatch) {
													isMatch &= lute[iRuleC] & lutse[iRuleC];
													if (isMatch) {
														isMatch &= luts[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
														if (isMatch) {
															iBit = 0;
															mask = 1;
															while (!(isMatch & mask)) {
																iBit += 1;
																mask <<= 1;
															}
															state = output[(iRuleC << 5) + iBit];
															break;
														}
													}
												}
											}

											// add to the lookup table
											this.ruleLoaderLookup[index + (c << ci)] = state;
										}
									}
								}
							}
						}
					}
				}
				break;

			case PatternConstants.ruleTableOneD:
				// One Dimensional - TBD currently not used
				// create the bit shifts
				ei = 0;
				ci = ei + i;
				wi = ci + i;

				// get the lookups
				lut0 = lut[0];
				lut1 = lut[1];
				lut2 = lut[2];

				// create the entries
				for (w = 0; w < states; w += 1) {
					lutw = lut1[w];
					for (e = 0; e < states; e += 1) {
						lute = lut2[e];
						index = (w << wi) | (e << ei);
						for (c = 0; c < states; c += 1) {
							lutc = lut0[c];
							state = c;

							// search for the match
							for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
								isMatch = lutc[iRuleC] & lutw[iRuleC] & lute[iRuleC];
								if (isMatch) {
									iBit = 0;
									mask = 1;
									while (!(isMatch & mask)) {
										iBit += 1;
										mask <<= 1;
									}
									state = output[(iRuleC << 5) + iBit];
									break;
								}
							}

							// add to the lookup table
							this.ruleLoaderLookup[index + (c << ci)] = state;
						}
					}
				}
				break;

			default:
				// unsupported neighbourhood
				this.ruleLoaderLookup = null;
			}
		} else {
			// too many bits for lookup table
			this.ruleLoaderLookup = null;
		}
	};

	// create rule tree lookup
	Life.prototype.createRuleTreeLookup = function() {
		// get number of cells in neighbourhood
		var	/** @type {number} */ bitsNeeded = 0,
			/** @type {number} */ states = this.ruleTreeStates,
			/** @type {number} */ i = 0,

			// cells in neighbourhood
			/** @type {number} */ nw = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ sw = 0,

			// cell offsets in index in bits
			/** @type {number} */ nwi = 0,
			/** @type {number} */ ni = 0,
			/** @type {number} */ nei = 0,
			/** @type {number} */ ei = 0,
			/** @type {number} */ ci = 0,
			/** @type {number} */ wi = 0,
			/** @type {number} */ swi = 0,
			/** @type {number} */ si = 0,
			/** @type {number} */ sei = 0,

			// cell partial index
			/** @type {number} */ nwx = 0,
			/** @type {number} */ nx = 0,
			/** @type {number} */ nex = 0,
			/** @type {number} */ wx = 0,
			/** @type {number} */ ex = 0,
			/** @type {number} */ swx = 0,
			/** @type {number} */ sex = 0,

			// cell partial lookup
			/** @type {number} */ nwp = 0,
			/** @type {number} */ np = 0,
			/** @type {number} */ nep = 0,
			/** @type {number} */ wp = 0,
			/** @type {number} */ ep = 0,
			/** @type {number} */ swp = 0,
			/** @type {number} */ sep = 0,

			/** @type {number} */ index = 0,
			/** @type {number} */ part = 0,
			/** @type {number} */ base = this.ruleTreeBase,
			/** @type {Uint32Array} */ a = this.ruleTreeA,
			/** @type {Uint8Array} */ b = this.ruleTreeB;

		// clear any previous partial lookpu
		this.ruleTreePartialLookup = null;

		// compute how many bits needed for states
		i = 0;
		while ((1 << i) < states) {
			i += 1;
		}

		// compute bits needed as cells in neighbourhood * state bits
		bitsNeeded = (this.ruleTreeNeighbours + 1) * i;

		// check lookup is small enough for lookup table
		if (bitsNeeded <= LifeConstants.maxRuleTreeLookupBits) {
			// build lookup table
			this.ruleLoaderLookup = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, (1 << bitsNeeded), "Life.ruleTreeLookup", Controller.useWASM));
			this.ruleLoaderLookupBits = i;

			// check neighbourhood
			if (this.ruleTreeNeighbours === 4) {
				// von Neumann
				// create the bit shifts
				si = 0;
				ni = si + i;
				ei = ni + i;
				ci = ei + i;
				wi = ci + i;

				// create the entries
				for (w = 0; w < states; w += 1) {
					for (e = 0; e < states; e += 1) {
						for (n = 0; n < states; n += 1) {
							for (s = 0; s < states; s += 1) {
								part = a[a[a[a[base + n] + w] + e] + s];
								index = (n << ni) | (e << ei) | (w << wi) | (s << si);
								for (c = 0; c < states; c += 1) {
									this.ruleLoaderLookup[index + (c << ci)] = b[part + c];
								}
							}
						}
					}
				}
			} else {
				if (this.ruleTreeNeighbours === 8) {
					// Moore
					// create the bit shifts
					nwi = 0;
					wi = nwi + i;
					swi = wi + i;
					ni = swi + i;
					ci = ni + i;
					si = ci + i;
					nei = si + i;
					ei = nei + i;
					sei = ei + i;

					// create the entries
					for (nw = 0; nw < states; nw += 1) {
						nwx = (nw << nwi);
						nwp = a[base + nw];
						for (ne = 0; ne < states; ne += 1) {
							nex = nwx | (ne << nei);
							nep = a[nwp + ne];
							for (sw = 0; sw < states; sw += 1) {
								swx = nex | (sw << swi);
								swp = a[nep + sw];
								for (se = 0; se < states; se += 1) {
									sex = swx | (se << sei);
									sep = a[swp + se];
									for (n = 0; n < states; n += 1) {
										nx = sex | (n << ni);
										np = a[sep + n];
										for (w = 0; w < states; w += 1) {
											wx = nx | (w << wi);
											wp = a[np + w];
											for (e = 0; e < states; e += 1) {
												ex = wx | (e << ei);
												ep = a [wp + e];
												for (s = 0; s < states; s += 1) {
													part = a[ep + s];
													index = ex | (s << si);
													for (c = 0; c < states; c += 1) {
														index = (nw << nwi) | (w << wi) | (sw << swi) | (n << ni) | (c << ci) | (s << si) | (ne << nei) | (e << ei) | (se << sei);
														this.ruleLoaderLookup[index] = b[part + c];
													}
												}
											}
										}
									}
								}
							}
						}
					}
				} else {
					// unsupported neighbourhood
					this.ruleLoaderLookup = null;
				}
			}
		} else {
			// too many bits for lookup table
			this.ruleLoaderLookup = null;

			// check for partial lookup
			if (this.ruleTreeNeighbours === 8 && bitsNeeded <= LifeConstants.maxRuleTreePartialLookupBits) {
				// build lookup table
				this.ruleTreePartialLookup = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, 1 << 24, "Life.ruleTreePartialLookup", Controller.useWASM));
				this.ruleLoaderLookupBits = 24;

				// Moore
				// create the bit shifts
				wi = 0;
				ni = wi + i;
				sei = ni + i;
				swi = sei + i;
				nei = swi + i;
				nwi = nei + i;

				// create the entries
				for (nw = 0; nw < states; nw += 1) {
					nwx = (nw << nwi);
					nwp = a[base + nw];
					for (ne = 0; ne < states; ne += 1) {
						nex = nwx | (ne << nei);
						nep = a[nwp + ne];
						for (sw = 0; sw < states; sw += 1) {
							swx = nex | (sw << swi);
							swp = a[nep + sw];
							for (se = 0; se < states; se += 1) {
								sex = swx | (se << sei);
								sep = a[swp + se];
								for (n = 0; n < states; n += 1) {
									nx = sex | (n << ni);
									np = a[sep + n];
									for (w = 0; w < states; w += 1) {
										wx = nx | (w << wi);
										wp = a[np + w];
										this.ruleTreePartialLookup[wx] = wp;
									}
								}
							}
						}
					}
				}
			}
		}
	};

	// create rule loader lookup
	Life.prototype.createRuleLoaderLookup = function() {
		// check if enabled
		if (this.ruleLoaderLookupEnabled) {
			// time the generation
			this.ruleLoaderGenerationTime = performance.now();

			// check whether algo is @TREE or @TABLE
			if (this.ruleTableOutput === null) {
				this.createRuleTreeLookup();
			} else {
				this.createRuleTableLookup();
			}

			this.ruleLoaderGenerationTime = performance.now() - this.ruleLoaderGenerationTime;
		}
	};

	// whether rule loader lookup available
	/** @returns {boolean} */
	Life.prototype.ruleLoaderLookupAvailable = function() {
		var	/** @type {boolean} */ result = false,
			/** @type {number} */ states = 0,
			/** @type {number} */ bitsNeeded = 0,
			/** @type {number} */ i = 0;

		if (this.isRuleTree) {
			if (this.ruleTableOutput === null) {
				// check @TREE
				states = this.ruleTreeStates;

				// compute how many bits needed for states
				i = 0;
				while ((1 << i) < states) {
					i += 1;
				}

				bitsNeeded = (this.ruleTreeNeighbours + 1) * i;
			} else {
				// check @TABLE
				states = this.multiNumStates;

				// compute how many bits needed for states
				i = 0;
				while ((1 << i) < states) {
					i += 1;
				}

				// check neighbourhood
				switch (this.ruleTableNeighbourhood) {
				case PatternConstants.ruleTableVN:
					bitsNeeded = 5 * i;
					break;

				case PatternConstants.ruleTableMoore:
					bitsNeeded = 9 * i;
					break;

				case PatternConstants.ruleTableHex:
					bitsNeeded = 7 * i;
					break;

					/*
				case PatternConstants.ruleTableOneD:
					bitsNeeded = 3 * i;
					break;
					*/

				default:
					bitsNeeded = LifeConstants.maxRuleTreeLookupBits + 1;
					break;
				}
			}

			// check lookup is small enough for lookup table
			if (bitsNeeded <= LifeConstants.maxRuleTreeLookupBits) {
				result = true;
			} else {
				// check lookup is small enough for partial RuleTree lookup talbe
				if (bitsNeeded <= LifeConstants.maxRuleTreePartialLookupBits && this.ruleTableOutput === null && this.ruleTreeNeighbours === 8) {
					result = true;
				}
			}
		}

		return result;
	};

	// update the Life rule
	Life.prototype.updateLifeRule = function(/** @type {View} */ view) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ tmp = 0,
			/** @type {Uint8Array} */ ruleArray = (this.isTriangular ? this.manager.ruleTriangularArray : this.manager.ruleArray),
			/** @type {Uint8Array} */ ruleAltArray = (this.isTriangular ? this.manager.ruleAltTriangularArray : this.manager.ruleAltArray),
			/** @type {boolean} */ altSpecified = this.manager.altSpecified,
			/** @type {number} */ hashSize = (this.isTriangular ? LifeConstants.hashTriangular : LifeConstants.hash33),
			/** @type {boolean} */ odd = false,
			/** @type {boolean} */ match = true,
			/** @type {Uint8Array} */ savedArray = null,
			/** @type {number} */ length = ruleArray.length;

		// save flag
		this.altSpecified = altSpecified;

		// check for RuleLoader rules
		if (this.isRuleTree) {
			// clear lookup table
			this.ruleLoaderLookup = null;
			this.ruleLoaderStep = -1;
			return;
		}

		// check for HROT
		if (this.altSpecified && this.isHROT) {
			// compare rule arrays
			i = 0;
			match = true;
			length = this.HROT.births.length;

			// check births
			while (i < length && match) {
				if (this.HROT.births[i] !== this.HROT.altBirths[i]) {
					match = false;
				} else {
					i += 1;
				}
			}

			// if identical then check survivals
			if (match) {
				i = 0;
				length = this.HROT.survivals.length;

				while (i < length && match) {
					if (this.HROT.survivals[i] !== this.HROT.altSurvivals[i]) {
						match = false;
					} else {
						i += 1;
					}
				}

				// if identical then disable alternate rule
				if (match) {
					this.altSpecified = false;

					// update the rule name and alias name
					i = view.patternRuleName.indexOf("|");
					if (i !== -1) {
						view.patternRuleName = view.patternRuleName.substring(0, i);
					}
					i = view.patternAliasName.indexOf("|");
					if (i !== -1) {
						view.patternAliasName = view.patternAliasName.substring(0, i);
					}
				}
			}

			// HROT check done
			return;
		}

		// check if alternates are duplicates
		if (this.altSpecified) {
			// Margolus and PCA only uses first 16 entries
			if (this.isMargolus || this.isPCA) {
				length = 16;
			}

			// compare rule arrays
			i = 0;
			match = true;
			while (i < length && match) {
				if (ruleArray[i] !== ruleAltArray[i]) {
					match = false;
				} else {
					i += 1;
				}
			}

			// if identical then disable alternate rule
			if (match) {
				this.altSpecified = false;

				// update the rule name and alias name
				i = view.patternRuleName.indexOf("|");
				if (i !== -1) {
					view.patternRuleName = view.patternRuleName.substring(0, i);
				}
				i = view.patternAliasName.indexOf("|");
				if (i !== -1) {
					view.patternAliasName = view.patternAliasName.substring(0, i);
				}
			}

			// if alt specified then swap
			if (this.altSpecified && (this.isMargolus || this.isPCA)) {
				for (i = 0; i < length; i += 1) {
					tmp = ruleArray[i];
					ruleArray[i] = ruleAltArray[i];
					ruleAltArray[i] = tmp;
				}
			}
		}

		// clear rule buffers
		this.indexLookup63 = null;
		this.indexLookup632 = null;
		this.indexLookupTri1 = null;
		this.indexLookupTri2 = null;
		this.margolusLookup1 = null;
		this.margolusLookup2 = null;
		this.margolusReverseLookup1 = null;
		this.margolusReverseLookup2 = null;

		// check for PCA
		if (this.isPCA) {
			// create lookup array
			this.margolusLookup1 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashPCA, "Life.PCALookup1", false));
			if (altSpecified) {
				this.margolusLookup2 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashPCA, "Life.PCALookup2", false));
				this.createPCAIndex(this.margolusLookup2, ruleArray, false);
				this.createPCAIndex(this.margolusLookup1, ruleAltArray, false);

				// check for alternate
				if (this.canReverse(ruleArray, false) && this.canReverse(ruleAltArray, false)) {
					this.margolusReverseLookup1 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashPCA, "Life.PCAReverseLookup1", false));
					this.margolusReverseLookup2 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashPCA, "Life.PCAReverseLookup2", false));
					this.canReverse(ruleAltArray, false);
					this.createPCAIndex(this.margolusReverseLookup1, ruleAltArray, true);
					this.canReverse(ruleArray, false);
					this.createPCAIndex(this.margolusReverseLookup2, ruleArray, true);
				}
			} else {
				this.createPCAIndex(this.margolusLookup1, ruleArray, false);

				// check for reverse
				if (this.canReverse(ruleArray, false)) {
					this.margolusReverseLookup1 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashPCA, "Life.PCAsReverseLookup1", false));
					this.createPCAIndex(this.margolusReverseLookup1, ruleArray, true);
				}
			}
		} else {
			// check for Margolus
			if (this.isMargolus) {
				// create lookup array
				this.margolusLookup1 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashMargolus, "Life.margolusLookup1", false));

				//  check for V0 = 15 and V15 = 0
				if (ruleArray[0] === 15 && ruleArray[15] === 0) {
					this.altSpecified = true;
					altSpecified = true;

					// save the original array
					savedArray = new Uint8Array(16);
					for (i = 0; i < 16; i += 1) {
						savedArray[i] = ruleArray[i];
					}

					// create non-strobing alternate rules
					this.createNonStrobingAlternates(ruleArray, ruleAltArray);
				}

				if (altSpecified) {
					this.margolusLookup2 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashMargolus, "Life.margolusLookup2", false));
					this.createMargolusIndex(this.margolusLookup2, ruleArray);
					this.createMargolusIndex(this.margolusLookup1, ruleAltArray);

					// check for reverse V0=15/V15=0
					if (savedArray && this.canReverse(savedArray, true)) {
						this.margolusReverseLookup1 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashMargolus, "Life.margolusReverseLookup1", false));
						this.margolusReverseLookup2 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashMargolus, "Life.margolusReverseLookup2", false));
						this.createNonStrobingAlternates(savedArray, ruleAltArray);
						this.createMargolusIndex(this.margolusReverseLookup1, ruleAltArray);
						this.createMargolusIndex(this.margolusReverseLookup2, savedArray);
					} else {
						// check for alternate
						if (this.canReverse(ruleArray, false) && this.canReverse(ruleAltArray, false)) {
							this.margolusReverseLookup1 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashMargolus, "Life.margolusReverseLookup1", false));
							this.margolusReverseLookup2 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashMargolus, "Life.margolusReverseLookup2", false));
							this.canReverse(ruleArray, true);
							this.createMargolusIndex(this.margolusReverseLookup1, ruleArray);
							this.canReverse(ruleAltArray, true);
							this.createMargolusIndex(this.margolusReverseLookup2, ruleAltArray);
						}
					}
				} else {
					this.createMargolusIndex(this.margolusLookup1, ruleArray);

					// check for reverse
					if (this.canReverse(ruleArray, true)) {
						this.margolusReverseLookup1 = /** @type {!Uint16Array} */ (this.allocator.allocate(Type.Uint16, LifeConstants.hashMargolus, "Life.margolusReverseLookup1", false));
						this.createMargolusIndex(this.margolusReverseLookup1, ruleArray);
					}
				}
			} else {
				// check for Triangular
				if (this.isTriangular) {
					// create lookup arrays
					this.indexLookupTri1 = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, LifeConstants.hashTriDouble, "Life.indexLookupTri1", false));
					if (altSpecified) {
						this.indexLookupTri2 = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, LifeConstants.hashTriDouble, "Life.indexLookupTri2", false));
						this.createTriangularIndex(this.indexLookupTri2, ruleArray);
						this.createTriangularIndex(this.indexLookupTri1, ruleAltArray);
					} else {
						this.createTriangularIndex(this.indexLookupTri1, ruleArray);
					}
				} else {
					// create the first lookup array
					this.indexLookup63 = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, LifeConstants.hash63, "Life.indexLookup63", Controller.useWASM));

					// check for Wolfram
					if (this.wolframRule === -1) {
						// check for B0
						if (ruleArray[0]) {
							// check for Smax
							if (ruleArray[hashSize - 1]) {
								// B0 with Smax: rule -> NOT(reverse(bits))
								for (i = 0; i < hashSize / 2; i += 1) {
									tmp = ruleArray[i];
									ruleArray[i] = 1 - ruleArray[hashSize - i - 1];
									ruleArray[hashSize - i - 1] = 1 - tmp;
								}
							} else {
								// B0 without Smax needs two rules
								// odd rule -> reverse(bits)
								for (i = 0; i < hashSize / 2; i += 1) {
									tmp = ruleArray[i];
									ruleArray[i] = ruleArray[hashSize - i - 1];
									ruleArray[hashSize - i - 1] = tmp;
								}
								odd = true;
								this.indexLookup632 = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, LifeConstants.hash63, "Life.indexLookup632", Controller.useWASM));
								this.createLifeIndex63(this.indexLookup632, ruleArray);

								// even rule -> NOT(bits)
								if (this.isExtended) {
									ruleAltArray.set(ruleArray);
									for (i = 0; i < hashSize / 2; i += 1) {
										tmp = ruleAltArray[i];
										// need to reverse then invert due to even rule above
										ruleAltArray[i] = 1 - ruleAltArray[hashSize - i - 1];
										ruleAltArray[hashSize - i - 1] = 1 - tmp;
									}
								} else {
									for (i = 0; i < hashSize / 2; i += 1) {
										tmp = ruleArray[i];
										// need to reverse then invert due to even rule above
										ruleArray[i] = 1 - ruleArray[hashSize - i - 1];
										ruleArray[hashSize - i - 1] = 1 - tmp;
									}
								}
							}
						}
					}

					// copy rules from pattern
					if (this.altSpecified) {
						this.indexLookup632 = /** @type {!Uint8Array} */ (this.allocator.allocate(Type.Uint8, LifeConstants.hash63, "Life.indexLookup632", Controller.useWASM));
						this.createLifeIndex63(this.indexLookup632, ruleArray);
						this.createLifeIndex63(this.indexLookup63, ruleAltArray);
					} else {
						this.createLifeIndex63(this.indexLookup63, ruleArray);
						if (odd) {
							this.altSpecified = true;
						}
					}
				}
			}
		}
	};

	// get the offset from the left most bit
	/** @returns {number} */
	Life.prototype.leftBitOffset16 = function(/** @type {number} */ value) {
		var	/** @type {number} */ result = 0;

		// find the left most bit number
		if ((value & 32768) !== 0) {
			result = 0;
		} else {
			if ((value & 16384) !== 0) {
				result = 1;
			} else {
				if ((value & 8192) !== 0) {
					result = 2;
				} else {
					if ((value & 4096) !== 0) {
						result = 3;
					} else {
						if ((value & 2048) !== 0) {
							result = 4;
						} else {
							if ((value & 1024) !== 0) {
								result = 5;
							} else {
								if ((value & 512) !== 0) {
									result = 6;
								} else {
									if ((value & 256) !== 0) {
										result = 7;
									} else {
										if ((value & 128) !== 0) {
											result = 8;
										} else {
											if ((value & 64) !== 0) {
												result = 9;
											} else {
												if ((value & 32) !== 0) {
													result = 10;
												} else {
													if ((value & 16) !== 0) {
														result = 11;
													} else {
														if ((value & 8) !== 0) {
															result = 12;
														} else {
															if ((value & 4) !== 0) {
																result = 13;
															} else {
																if ((value & 2) !== 0) {
																	result = 14;
																} else {
																	if ((value & 1) !== 0) {
																		result = 15;
																	}
																}
															}
														}
													}
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}

		// return the bit number
		return result;
	};

	// get the offset from the right most bit
	/** @returns {number} */
	Life.prototype.rightBitOffset16 = function(/** @type {number} */ value) {
		var	/** @type {number} */ result = 0;

		// find the right most bit number
		if ((value & 1) !== 0) {
			result = 15;
		} else {
			if ((value & 2) !== 0) {
				result = 14;
			} else {
				if ((value & 4) !== 0) {
					result = 13;
				} else {
					if ((value & 8) !== 0) {
						result = 12;
					} else {
						if ((value & 16) !== 0) {
							result = 11;
						} else {
							if ((value & 32) !== 0) {
								result = 10;
							} else {
								if ((value & 64) !== 0) {
									result = 9;
								} else {
									if ((value & 128) !== 0) {
										result = 8;
									} else {
										if ((value & 256) !== 0) {
											result = 7;
										} else {
											if ((value & 512) !== 0) {
												result = 6;
											} else {
												if ((value & 1024) !== 0) {
													result = 5;
												} else {
													if ((value & 2048) !== 0) {
														result = 4;
													} else {
														if ((value & 4096) !== 0) {
															result = 3;
														} else {
															if ((value & 8192) !== 0) {
																result = 2;
															} else {
																if ((value & 16384) !== 0) {
																	result = 1;
																}
															}
														}
													}
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}

		// return the bit number
		return result;
	};

	// create tiles for the overlay
	Life.prototype.createOverlayTiles = function(/** @type {boolean} */ state1Fit) {
		// overlay grid
		var	/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,

			// overlay row
			/** @type {Uint8Array} */ overlayRow = null,

			// tile grid
			/** @type {Array<Uint16Array>} */ tileGrid = this.tileGrid,

			// tile row
			/** @type {Uint16Array} */ tileRow = null,

			// width of overlay
			/** @type {number} */ width = this.width,

			// height of overlay
			/** @type {number} */ height = this.height,

			// tile size (2^n)
			/** @type {number} */ tilePower = this.tilePower,

			// whether something alive in the row
			/** @type {number} */ rowAlive = 0,

			// top and bottom bounding box
			/** @type {number} */ topY = this.zoomBox.topY,
			/** @type {number} */ bottomY = this.zoomBox.bottomY,
			/** @type {number} */ leftX = this.zoomBox.leftX,
			/** @type {number} */ rightX = this.zoomBox.rightX,

			// counters
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0;

		// process each row of the overlay grid
		for (h = 0; h < height; h += 1) {
			// get the overlay row
			overlayRow = overlayGrid[h];

			// get the tile row
			tileRow = tileGrid[h >> tilePower];
			rowAlive = 0;

			// check for non-zero states
			for (w = 0; w < width; w += 1) {
				if (overlayRow[w] !== 0) {
					// set the tile
					tileRow[(w >> (tilePower + tilePower))] |= 1 << (~(w >> tilePower) & 15);
					rowAlive |= overlayRow[w];

					// update bounding box
					if (w < leftX) {
						leftX = w;
					}
					if (w > rightX) {
						rightX = w;
					}
				}
			}

			// check if row was alive
			if (rowAlive) {
				if (h < bottomY) {
					bottomY = h;
				}
				if (h > topY) {
					topY = h;
				}
			}
		}

		// update bounding box
		if (!state1Fit) {
			this.zoomBox.topY = topY;
			this.zoomBox.bottomY = bottomY;
			this.zoomBox.leftX = leftX;
			this.zoomBox.rightX = rightX;
		}
	};

	// shrink the tile grid to the pattern
	Life.prototype.shrinkTileGrid = function() {
		if (Controller.useWASM && Controller.wasmEnableShrinkTileGrid && this.view.wasmEnabled) {
			WASM.shrinkTileGrid(
				this.grid16.whole.byteOffset | 0,
				this.grid16[0].length | 0,
				this.nextTileGrid.whole.byteOffset | 0,
				this.tileGrid.whole.byteOffset | 0,
				this.tileGrid[0].length | 0,
				this.tileGrid.whole.byteLength | 0,
				this.isTriangular ? 49152 : 32768,
				this.isTriangular ? 3 : 1,
				this.tileY | 0,
				this.tileX  |0,
				this.tileRows | 0,
				this.tileCols | 0,
				this.blankTileRow.byteOffset | 0,
				this.blankTileRow.length | 0,
				LifeConstants.bottomRightSet | 0,
				LifeConstants.bottomSet | 0,
				LifeConstants.topRightSet | 0,
				LifeConstants.topSet | 0,
				LifeConstants.bottomLeftSet | 0,
				LifeConstants.topLeftSet | 0,
				LifeConstants.leftSet | 0,
				LifeConstants.rightSet | 0,
				this.width | 0
			);
		} else {
			this.shrinkTileGridJS();
		}
	};

	// shrink the tile grid to the pattern - Javascript version
	Life.prototype.shrinkTileGridJS = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ output = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,

			// grid
			/** @type {Array<Uint16Array>} */ grid = this.grid16,
			/** @type {Uint16Array} */ gridRow = null,

			// source tile grid (from next tile grid template)
			/** @type {Array<Uint16Array>} */ tileGrid = this.nextTileGrid,
			/** @type {Uint16Array} */ tileRow = null,

			// destination tile grid
			/** @type {Array<Uint16Array>} */ nextTileGrid = this.tileGrid,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// left and right tile grid masks
			/** @type {number} */ leftMask = (this.isTriangular ? 49152 : 32768),
			/** @type {number} */ rightMask = (this.isTriangular ? 3 : 1),

			// flag if any cells in the tile are alive
			/** @type {boolean} */ tileAlive = false,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16 bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the destination tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// check for cells on the edge of tile boundaries
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << b)) !== 0) {
							// flag nothing alive in the tile
							tileAlive = false;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							h = bottomY;

							// current row
							gridRow = grid[h];

							// get the cells from the grid
							output = gridRow[leftX];

							// check if any cells are set
							if (output) {
								// update tile alive flag
								tileAlive = true;

								// check for left column now set
								if ((output & leftMask) !== 0) {
									neighbours |= LifeConstants.leftSet;
									neighbours |= LifeConstants.bottomLeftSet;
								}

								// check for right column now set
								if ((output & rightMask) !== 0) {
									neighbours |= LifeConstants.rightSet;
									neighbours |= LifeConstants.bottomRightSet;
								}

								// bottom row set
								neighbours |= LifeConstants.bottomSet;
							}

							// process middle rows of the tile
							h += 1;
							while (h < topY - 1) {
								// current row
								gridRow = grid[h];

								// get the cells from the grid
								output = gridRow[leftX];

								// check if any cells are set
								if (output) {
									// update tile alive flag
									tileAlive = true;

									// check for left column now set
									if ((output & leftMask) !== 0) {
										neighbours |= LifeConstants.leftSet;
									}

									// check for right column now set
									if ((output & rightMask) !== 0) {
										neighbours |= LifeConstants.rightSet;
									}
								}

								// next row
								h += 1;
							}

							// process top row of tile
							gridRow = grid[h];

							// get the cells from the grid
							output = gridRow[leftX];

							// check if any cells are set
							if (output) {
								// update tile alive flag
								tileAlive = true;

								// check for left column now set
								if ((output & leftMask) !== 0) {
									neighbours |= LifeConstants.leftSet;
									neighbours |= LifeConstants.topLeftSet;
								}

								// check for right column now set
								if ((output & rightMask) !== 0) {
									neighbours |= LifeConstants.rightSet;
									neighbours |= LifeConstants.topRightSet;
								}

								// top row set
								neighbours |= LifeConstants.topSet;
							}

							// check if the source was alive
							if (tileAlive) {
								// update
								nextTiles |= (1 << b);

								// check for neighbours
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (b < 15) {
											nextTiles |= (1 << (b + 1));
										} else {
											// set in previous set
											if (tw > 0) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (b > 0) {
											nextTiles |= (1 << (b - 1));
										} else {
											// set carry over to go into next set
											if (tw < tileCols16 - 1) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << b);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << b);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (b < 15) {
											belowNextTiles |= (1 << (b + 1));
										} else {
											if (tw > 0) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (b > 0) {
											belowNextTiles |= (1 << (b - 1));
										} else {
											if (tw < tileCols16 - 1) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (b < 15) {
											aboveNextTiles |= (1 << (b + 1));
										} else {
											if (tw > 0) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (b > 0) {
											aboveNextTiles |= (1 << (b - 1));
										} else {
											if (tw < tileCols16 - 1) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);
	};

	// shrink the tile grid to the pattern
	Life.prototype.shrinkTileGridGenerations = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ output = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,

			// grid
			/** @type {Array<Uint16Array>} */ grid = this.grid16,
			/** @type {Uint16Array} */ gridRow = null,

			// source tile grid (from next tile grid template)
			/** @type {Array<Uint16Array>} */ tileGrid = this.nextTileGrid,
			/** @type {Uint16Array} */ tileRow = null,

			// colour tile grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourGridRow = null,

			// destination tile grid
			/** @type {Array<Uint16Array>} */ nextTileGrid = this.tileGrid,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ cr = 0,

			// flag if any cells in the tile are alive
			/** @type {boolean} */ tileAlive = false,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16 bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the destination tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << b)) !== 0) {
							// flag nothing alive in the tile
							tileAlive = false;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							h = bottomY;

							// current row
							gridRow = grid[h];
							colourGridRow = colourGrid[h];

							// get the cells from the grid
							output = gridRow[leftX];

							// update from the colour row
							cr = leftX << 4;
							if (colourGridRow[cr] > 0) {
								output |= 32768;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 16384;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 8192;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 4096;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 2048;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 1024;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 512;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 256;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 128;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 64;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 32;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 16;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 8;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 4;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 2;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 1;
							}

							// check if any cells are set
							if (output) {
								// update tile alive flag
								tileAlive = true;

								// check for left column now set
								if ((output & 32768) !== 0) {
									neighbours |= LifeConstants.leftSet;
									neighbours |= LifeConstants.bottomLeftSet;
								}

								// check for right column now set
								if ((output & 1) !== 0) {
									neighbours |= LifeConstants.rightSet;
									neighbours |= LifeConstants.bottomRightSet;
								}

								// bottom row set
								neighbours |= LifeConstants.bottomSet;
							}

							// process middle rows of the tile
							h += 1;
							while (h < topY - 1) {
								// current row
								gridRow = grid[h];
								colourGridRow = colourGrid[h];

								// get the cells from the grid
								output = gridRow[leftX];

								// update from the colour row
								cr = leftX << 4;
								if (colourGridRow[cr] > 0) {
									output |= 32768;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 16384;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 8192;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 4096;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 2048;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 1024;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 512;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 256;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 128;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 64;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 32;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 16;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 8;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 4;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 2;
								}
								cr += 1;
								if (colourGridRow[cr] > 0) {
									output |= 1;
								}

								// check if any cells are set
								if (output) {
									// update tile alive flag
									tileAlive = true;

									// check for left column now set
									if ((output & 32768) !== 0) {
										neighbours |= LifeConstants.leftSet;
									}

									// check for right column now set
									if ((output & 1) !== 0) {
										neighbours |= LifeConstants.rightSet;
									}
								}

								// next row
								h += 1;
							}

							// process top row of tile
							gridRow = grid[h];
							colourGridRow = colourGrid[h];

							// get the cells from the grid
							output = gridRow[leftX];

							// update from the colour row
							cr = leftX << 4;
							if (colourGridRow[cr] > 0) {
								output |= 32768;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 16384;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 8192;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 4096;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 2048;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 1024;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 512;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 256;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 128;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 64;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 32;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 16;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 8;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 4;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 2;
							}
							cr += 1;
							if (colourGridRow[cr] > 0) {
								output |= 1;
							}

							// check if any cells are set
							if (output) {
								// update tile alive flag
								tileAlive = true;

								// check for left column now set
								if ((output & 32768) !== 0) {
									neighbours |= LifeConstants.leftSet;
									neighbours |= LifeConstants.topLeftSet;
								}

								// check for right column now set
								if ((output & 1) !== 0) {
									neighbours |= LifeConstants.rightSet;
									neighbours |= LifeConstants.topRightSet;
								}

								// top row set
								neighbours |= LifeConstants.topSet;
							}

							// check if the source was alive
							if (tileAlive) {
								// update
								nextTiles |= (1 << b);

								// check for neighbours
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (b < 15) {
											nextTiles |= (1 << (b + 1));
										} else {
											// set in previous set
											if (tw > 0) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (b > 0) {
											nextTiles |= (1 << (b - 1));
										} else {
											// set carry over to go into next set
											if (tw < tileCols16 - 1) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << b);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << b);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (b < 15) {
											belowNextTiles |= (1 << (b + 1));
										} else {
											if (tw > 0) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (b > 0) {
											belowNextTiles |= (1 << (b - 1));
										} else {
											if (tw < tileCols16 - 1) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (b < 15) {
											aboveNextTiles |= (1 << (b + 1));
										} else {
											if (tw > 0) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (b > 0) {
											aboveNextTiles |= (1 << (b - 1));
										} else {
											if (tw < tileCols16 - 1) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);
	};

	// get alive states selection box (used for HROT and Generations patterns)
	Life.prototype.getAliveStatesBox = function(/** @type {BoundingBox} */ selBox) {
		var	/** @type {number} */ w = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ aliveStart = this.historyStates + 1,

			// width and height
			/** @type {number} */ height = this.height,
			/** @type {number} */ width = this.width,

			// colour grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourGridRow = null,

			// new box extent
			/** @type {number} */ newBottomY = this.height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = this.width,
			/** @type {number} */ newRightX = -1,

			// box offset for bounded grid
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bounded grid top left
			/** @type {number} */ leftX = 0,
			/** @type {number} */ bottomY = 0,

			// bounded grid bottom right
			/** @type {number} */ rightX = 0,
			/** @type {number} */ topY = 0,

			// flag for live cells in a row
			/** @type {boolean} */ rowAlive = false;

		// select the correct grid for PCA
		if (this.isPCA) {
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// check if using bounded grid
		if (this.boundedGridType === -1) {
			leftX = 0;
			rightX = width - 1;
			bottomY = 0;
			topY = height - 1;
		} else {
			if (this.boundedGridWidth === 0) {
				leftX = 0;
				rightX = width - 1;
			} else {
				leftX = Math.round((this.width - this.boundedGridWidth) / 2) + boxOffset;
			      rightX = leftX + this.boundedGridWidth;
			}

			if (this.boundedGridHeight === 0) {
				bottomY = 0;
				topY = height - 1;
			} else {
				bottomY = Math.round((this.height - this.boundedGridHeight) / 2) + boxOffset;
				topY = bottomY + this.boundedGridHeight;
			}
		}

		// check each row
		for (h = bottomY; h < topY; h += 1) {
			colourGridRow = colourGrid[h];
			rowAlive = false;

			// check each column
			for (w = leftX; w < rightX; w += 1) {
				state = colourGridRow[w];
				if (state >= aliveStart && state <= 255) {
					rowAlive = true;

					if (w < newLeftX) {
						newLeftX = w;
					}
					if (w > newRightX) {
						newRightX = w;
					}
				}
			}

			// check if the row was alive
			if (rowAlive) {
				if (h < newBottomY) {
					newBottomY = h;
				}
				if (h > newTopY) {
					newTopY = h;
				}
			}
		}

		// ensure the box is not blank
		if (newTopY < 0 || newBottomY >= height || newLeftX >= width || newRightX < 0) {
			// set the box to the middle
			newTopY = (topY - bottomY) >> 1;
			newBottomY = newTopY;
			newLeftX = (rightX - leftX) >> 1;
			newRightX = newLeftX;
		}

		// clip to display
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save new grid box
		selBox.topY = newTopY;
		selBox.bottomY = newBottomY;
		selBox.leftX = newLeftX;
		selBox.rightX = newRightX;
	};

	// shrink grid (after major edit)
	Life.prototype.doShrink = function() {
		var	/** @type {number} */ w = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ input = 0,
			/** @type {number} */ over = 0,
			/** @type {number} */ state = 0,

			// width in 16bit chunks
			/** @type {number} */ w16 = this.width >> 4,

			// width and height
			/** @type {number} */ height = this.height,
			/** @type {number} */ width = this.width,

			// life grid
			/** @type {Array<Uint16Array>} */ grid16 = this.grid16,
			/** @type {Uint16Array} */ gridRow16 = null,

			// overlay colour grid
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {Uint8Array} */ overlayRow = null,

			// colour grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourGridRow = null,

			// bounding boxes
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ HROTBox = this.HROTBox,
			/** @type {BoundingBox} */ initialBox = this.initialBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox,
			/** @type {BoundingBox} */ identifyHistoryBox = this.identifyHistoryBox,

			// new box extent
			/** @type {number} */ newBottomY = this.height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = this.width,
			/** @type {number} */ newRightX = -1,

			// new overlay extent
			/** @type {number} */ overlayBottomY = this.height,
			/** @type {number} */ overlayTopY = -1,
			/** @type {number} */ overlayLeftX = this.width,
			/** @type {number} */ overlayRightX = -1,

			// flag if something in the row was alive
			/** @type {number} */ rowAlive = 0,

			// safe border size
			/** @type {number} */ safeBorder = this.isHROT ? (this.view.getSafeBorderSize() >> 1) : 0,

			// flags if something in the column was alive
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16;

		if (this.shrinkNeeded) {
			this.shrinkNeeded = false;

			// check for PCA, RuleTree, Super or Extended rules
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				// swap grids every generation
				if ((this.counter & 1) !== 0) {
					colourGrid = this.nextColourGrid;
				}
			}

			// determine the buffer for current generation
			if ((this.counter & 1) !== 0) {
				grid16 = this.nextGrid16;
			} else {
				grid16 = this.grid16;
			}

			// delete bounded grid border
			if (this.boundedGridType !== -1) {
				this.drawBoundedGridBorder(colourGrid, 0);
			}

			// check for LifeHistory pattern
			if (overlayGrid) {
				// use the overlay grid to set the bounding box (to cope with non-excecutable states)
				for (h = 0; h < height; h += 1) {
					overlayRow = overlayGrid[h];
					colourGridRow = colourGrid[h];

					// flag nothing in the row
					rowAlive = 0;

					// check each column
					for (w = 0; w < width; w += 1) {
						over = overlayRow[w];
						state = colourGridRow[w];
						if (over || (state > 0 && state < this.aliveStart)) {
							input = 1;
						} else {
							input = 0;
						}
						rowAlive |= input;

						if (input) {
							if (w < overlayLeftX) {
								overlayLeftX = w;
							}
							if (w > overlayRightX) {
								overlayRightX = w;
							}
						}
					}

					// check if the row was alive
					if (rowAlive) {
						if (h < overlayBottomY) {
							overlayBottomY = h;
						}
						if (h > overlayTopY) {
							overlayTopY = h;
						}
					}
				}
			}

			// use the pattern grid to set the bounding box
			// clear column occupied flags
			columnOccupied16.fill(0);

			// check for Generations or HROT
			if (this.isHROT && this.multiNumStates === 2) {
				// check each row
				for (h = 0; h < height; h += 1) {
					colourGridRow = colourGrid[h];

					// flag nothing in the row
					rowAlive = 0;

					// check each column
					for (w = 0; w < width; w += 1) {
						input = colourGridRow[w] & 64;
						rowAlive |= input;

						if (input) {
							if (w < newLeftX) {
								newLeftX = w;
							}
							if (w > newRightX) {
								newRightX = w;
							}
						}
					}

					// check if the row was alive
					if (rowAlive) {
						if (h < newBottomY) {
							newBottomY = h;
						}
						if (h > newTopY) {
							newTopY = h;
						}
					}
				}
			} else {
				if (this.multiNumStates !== -1) {
					// check each row
					for (h = 0; h < height; h += 1) {
						colourGridRow = colourGrid[h];

						// flag nothing in the row
						rowAlive = 0;

						// check each column
						for (w = 0; w < width; w += 1) {
							input = colourGridRow[w];
							rowAlive |= input;

							if (input) {
								if (w < newLeftX) {
									newLeftX = w;
								}
								if (w > newRightX) {
									newRightX = w;
								}
							}
						}

						// check if the row was alive
						if (rowAlive) {
							if (h < newBottomY) {
								newBottomY = h;
							}
							if (h > newTopY) {
								newTopY = h;
							}
						}
					}
				} else {
					// check each row
					for (h = 0; h < height; h += 1) {
						gridRow16 = grid16[h];

						// flag nothing alive in the row
						rowAlive = 0;

						// check each column
						for (w = 0; w < w16; w += 1) {
							// update row alive flag
							input = gridRow16[w];
							rowAlive |= input;

							// update the column alive flag
							columnOccupied16[w] |= input;
						}

						// check if the row was alive
						if (rowAlive) {
							if (h < newBottomY) {
								newBottomY = h;
							}
							if (h > newTopY) {
								newTopY = h;
							}
						}
					}

					// check the width of the box
					for (w = 0; w < w16; w += 1) {
						if (columnOccupied16[w]) {
							if (w < newLeftX) {
								newLeftX = w;
							}
							if (w > newRightX) {
								newRightX = w;
							}
						}
					}

					// convert new width to pixels
					newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
					newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);
				}
			}

			// ensure the box is not blank
			if (newTopY < 0 || newBottomY >= height || newLeftX >= width || newRightX < 0) {
				// set the box to the middle
				newTopY = height >> 1;
				newBottomY = newTopY;
				newLeftX = width >> 1;
				newRightX = newLeftX;
			}

			// copy to [R]History identify box
			identifyHistoryBox.leftX = newLeftX;
			identifyHistoryBox.bottomY = newBottomY;
			identifyHistoryBox.rightX = newRightX;
			identifyHistoryBox.topY = newTopY;

			// merge with overlay if required
			if (overlayGrid) {
				if (overlayTopY > newTopY) {
					newTopY = overlayTopY;
				}
				if (overlayBottomY < newBottomY) {
					newBottomY = overlayBottomY;
				}
				if (overlayLeftX < newLeftX) {
					newLeftX = overlayLeftX;
				}
				if (overlayRightX > newRightX) {
					newRightX = overlayRightX;
				}
			}

			// clip to display
			if (newTopY > this.height - 1 - safeBorder) {
				newTopY = this.height - 1 - safeBorder;
			}
			if (newBottomY < safeBorder) {
				newBottomY = safeBorder;
			}
			if (newLeftX < safeBorder) {
				newLeftX = safeBorder;
			}
			if (newRightX > this.width - 1 - safeBorder) {
				newRightX = this.width - 1 - safeBorder;
			}

			// save new grid box
			if (!(this.isHROT && this.multiNumStates === 2)) {
				zoomBox.topY = newTopY;
				zoomBox.bottomY = newBottomY;
				zoomBox.leftX = newLeftX;
				zoomBox.rightX = newRightX;
			}

			// copy to HROT alive state box
			HROTBox.topY = newTopY;
			HROTBox.bottomY = newBottomY;
			HROTBox.leftX = newLeftX;
			HROTBox.rightX = newRightX;

			// copy to inital box (for Track)
			initialBox.topY = newTopY;
			initialBox.bottomY = newBottomY;
			initialBox.leftX = newLeftX;
			initialBox.rightX = newRightX;

			// compute history extent if drawing with hexagons or triangles
			if (this.isHex || this.isTriangular) {
				newBottomY = this.height;
				newTopY = -1;
				newLeftX = this.width;
				newRightX = -1;

				// check each row
				for (h = 0; h < height; h += 1) {
					colourGridRow = colourGrid[h];

					// flag nothing in the row
					rowAlive = 0;

					// check each column
					for (w = 0; w < width; w += 1) {
						input = colourGridRow[w];
						rowAlive |= input;

						if (input) {
							if (w < newLeftX) {
								newLeftX = w;
							}
							if (w > newRightX) {
								newRightX = w;
							}
						}
					}

					// check if the row was alive
					if (rowAlive) {
						if (h < newBottomY) {
							newBottomY = h;
						}
						if (h > newTopY) {
							newTopY = h;
						}
					}
				}
			}

			historyBox.topY = newTopY;
			historyBox.bottomY = newBottomY;
			historyBox.leftX = newLeftX;
			historyBox.rightX = newRightX;
		}
	};

	Life.prototype.resetBoxes = function(/** @type {boolean} */ state1Fit) {
		var	/** @type {Array<Uint16Array>} */ grid16,
			/** @type {Array<Uint16Array>} */ tileGrid,
			/** @type {Array<Uint16Array>} */ nextTileGrid,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ newBottomY = 0,
			/** @type {number} */ newLeftX = 0,
			/** @type {number} */ newTopY = 0,
			/** @type {number} */ newRightX = 0;

		if (Controller.useWASM && Controller.wasmEnableResetBoxes && this.view.wasmEnabled) {
			if (this.multiNumStates === -1 && this.overlayGrid === null) {
				if ((this.counter & 1) !== 0) {
					grid16 = this.nextGrid16;
					tileGrid = this.nextTileGrid;
					nextTileGrid = this.tileGrid;
				} else {
					grid16 = this.grid16;
					tileGrid = this.tileGrid;
					nextTileGrid = this.nextTileGrid;
				}

				WASM.resetBoxesBit(
					grid16.whole.byteOffset | 0,
					grid16[0].length | 0,
					tileGrid.whole.byteOffset | 0,
					nextTileGrid.whole.byteOffset | 0,
					tileGrid[0].length | 0,
					this.columnOccupied16.byteOffset | 0,
					this.columnOccupied16.length | 0,
					this.rowOccupied16.byteOffset | 0,
					this.rowOccupied16.length | 0,
					this.width | 0,
					this.height | 0,
					this.tilePower | 0,
					(this.view.thumbLaunch && !this.view.autoStart && this.view.startFrom === -1 && !this.view.autoIdentify) ? 1 : 0,
					this.sharedBuffer.byteOffset | 0
				);

				// get the new bounding box
				newLeftX = this.sharedBuffer[0];
				newBottomY = this.sharedBuffer[1];
				newRightX = this.sharedBuffer[2];
				newTopY = this.sharedBuffer[3];

				zoomBox.leftX = newLeftX;
				zoomBox.bottomY = newBottomY;
				zoomBox.rightX = newRightX;
				zoomBox.topY = newTopY;

				// shrink the tile grid to the pattern unless thumb launch mode and the pattern is static
				if (!(this.view.thumbLaunch && !this.view.autoStart && this.view.startFrom === -1 && !this.view.autoIdentify)) {
					this.shrinkTileGrid();
				}

				// copy tile grid to the next tile grid
				Array.copy(tileGrid, nextTileGrid);

				// copy to the colour grids
				Array.copy(tileGrid, this.colourTileGrid);
				Array.copy(tileGrid, this.colourTileHistoryGrid);
			} else {
				this.resetBoxesJS(state1Fit);
			}
		} else {
			this.resetBoxesJS(state1Fit);
		}
	};

	// create bounding box from current state - Javascript version
	Life.prototype.resetBoxesJS = function(/** @type {boolean} */ state1Fit) {
		var	/** @type {number} */ w = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ input = 0,

			// width in 16bit chunks
			/** @type {number} */ w16 = this.width >> 4,

			// width and height
			/** @type {number} */ height = this.height,
			/** @type {number} */ width = this.width,

			// life grid
			/** @type {Array<Uint16Array>} */ grid16 = this.grid16,
			/** @type {Uint16Array} */ gridRow16 = null,

			// overlay colour grid
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {Uint8Array} */ overlayRow = null,

			// colour grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourGridRow = null,

			// bounding boxes
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ HROTBox = this.HROTBox,
			/** @type {BoundingBox} */ initialBox = this.initialBox,

			// new box extent
			/** @type {number} */ newBottomY = this.height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = this.width,
			/** @type {number} */ newRightX = -1,

			// new overlay extent
			/** @type {number} */ overlayBottomY = this.height,
			/** @type {number} */ overlayTopY = -1,
			/** @type {number} */ overlayLeftX = this.width,
			/** @type {number} */ overlayRightX = -1,

			// flag if something in the row was alive
			/** @type {number} */ rowAlive = 0,

			// flags if something in the column was alive
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,

			// tile grids
			/** @type {Array<Uint16Array>} */ tileGrid = this.tileGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = this.nextTileGrid,

			// colour tile grids
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,

			// bottom tile row
			/** @type {number} */ bottomY = 0,

			// top tile row
			/** @type {number} */ topY = 0,

			// left tile group column
			/** @type {number} */ leftX = 0,

			// right tile group column
			/** @type {number} */ rightX = 0;

		// determine the buffer for current generation
		if ((this.counter & 1) !== 0) {
			grid16 = this.nextGrid16;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid16 = this.grid16;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// check for LifeHistory pattern
		if (overlayGrid && !state1Fit) {
			// use the overlay grid to set the bounding box (to cope with non-excecutable states)
			for (h = 0; h < height; h += 1) {
				overlayRow = overlayGrid[h];

				// flag nothing in the row
				rowAlive = 0;

				// check each column
				for (w = 0; w < width; w += 1) {
					input = overlayRow[w];
					rowAlive |= input;

					if (input) {
						if (w < overlayLeftX) {
							overlayLeftX = w;
						}
						if (w > overlayRightX) {
							overlayRightX = w;
						}
					}
				}

				// check if the row was alive
				if (rowAlive) {
					if (h < overlayBottomY) {
						overlayBottomY = h;
					}
					if (h > overlayTopY) {
						overlayTopY = h;
					}
				}
			}
		}

		// use the pattern grid to set the bounding box
		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// check for Generations or HROT
		if (this.multiNumStates !== -1) {
			// check each row
			for (h = 0; h < height; h += 1) {
				colourGridRow = colourGrid[h];

				// flag nothing in the row
				rowAlive = 0;

				// check each column
				for (w = 0; w < width; w += 1) {
					input = colourGridRow[w];
					rowAlive |= input;

					if (input) {
						if (w < newLeftX) {
							newLeftX = w;
						}
						if (w > newRightX) {
							newRightX = w;
						}
					}
				}

				// check if the row was alive
				if (rowAlive) {
					if (h < newBottomY) {
						newBottomY = h;
					}
					if (h > newTopY) {
						newTopY = h;
					}
				}
			}
		} else {
			// check each row
			for (h = 0; h < height; h += 1) {
				gridRow16 = grid16[h];

				// flag nothing alive in the row
				rowAlive = 0;

				// check each column
				for (w = 0; w < w16; w += 1) {
					// update row alive flag
					input = gridRow16[w];
					rowAlive |= input;

					// update the column alive flag
					columnOccupied16[w] |= input;
				}

				// check if the row was alive
				if (rowAlive) {
					if (h < newBottomY) {
						newBottomY = h;
					}
					if (h > newTopY) {
						newTopY = h;
					}
				}
			}

			// check the width of the box
			for (w = 0; w < w16; w += 1) {
				if (columnOccupied16[w]) {
					if (w < newLeftX) {
						newLeftX = w;
					}
					if (w > newRightX) {
						newRightX = w;
					}
				}
			}

			// convert new width to pixels
			newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
			newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);
		}

		// ensure the box is not blank
		if (newTopY < 0 || newBottomY >= height || newLeftX >= width || newRightX < 0) {
			// set the box to the middle
			newTopY = height >> 1;
			newBottomY = newTopY;
			newLeftX = width >> 1;
			newRightX = newLeftX;
			overlayTopY = newTopY;
			overlayBottomY = newBottomY;
			overlayLeftX = newLeftX;
			overlayRightX = newRightX;
		}

		// merge with overlay if required
		if (overlayGrid && !state1Fit && overlayTopY !== -1) {
			if (overlayTopY < newTopY) {
				newTopY = overlayTopY;
			}
			if (overlayBottomY > newBottomY) {
				newBottomY = overlayBottomY;
			}
			if (overlayLeftX < newLeftX) {
				newLeftX = overlayLeftX;
			}
			if (overlayRightX > newRightX) {
				newRightX = overlayRightX;
			}
		}

		// clip to display
		if (newTopY > this.height - 1) {
			newTopY = this.height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > this.width - 1) {
			newRightX = this.width - 1;
		}

		// save new grid box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// copy to HROT alive state box
		HROTBox.topY = newTopY;
		HROTBox.bottomY = newBottomY;
		HROTBox.leftX = newLeftX;
		HROTBox.rightX = newRightX;

		// copy to the original box (for LifeHistory)
		initialBox.topY = newTopY;
		initialBox.bottomY = newBottomY;
		initialBox.leftX = newLeftX;
		initialBox.rightX = newRightX;

		// extend box by one cell
		if (newLeftX > 0) {
			newLeftX -= 1;
		}
		if (newBottomY > 0) {
			newBottomY -= 1;
		}
		if (newRightX < this.width - 1) {
			newRightX += 1;
		}
		if (newTopY < this.height - 1) {
			newTopY += 1;
		}

		// set the initial tile row
		bottomY = newBottomY >> this.tilePower;
		topY = newTopY >> this.tilePower;
		leftX = newLeftX >> (this.tilePower + 4);
		rightX = newRightX >> (this.tilePower + 4);

		// shrink the tile grid to the pattern unless thumb launch mode and the pattern is static
		if (this.view.thumbLaunch && !this.view.autoStart && this.view.startFrom !== -1 && !this.view.autoIdentify) {
			// set the current tile grid from the bounding box
			for (h = bottomY; h <= topY; h += 1) {
				tileRow = tileGrid[h];
				for (w = leftX; w <= rightX; w += 1) {
					tileRow[w] = -1;
				}
			}
		} else {
			// set the next tile grid from the bounding box
			for (h = bottomY; h <= topY; h += 1) {
				// get the tile row in the next tile grid since this will be used when shrinking
				tileRow = nextTileGrid[h];
				for (w = leftX; w <= rightX; w += 1) {
					tileRow[w] = -1;
				}
			}

			if (this.multiNumStates !== -1) {
				this.shrinkTileGridGenerations();
			} else {
				this.shrinkTileGrid();
			}
		}

		// add the overlay grid to the tiles
		if (overlayGrid) {
			this.createOverlayTiles(state1Fit);
			initialBox.topY = zoomBox.topY;
			initialBox.bottomY = zoomBox.bottomY;
			initialBox.leftX = zoomBox.leftX;
			initialBox.rightX = zoomBox.rightX;
		}

		// copy tile grid to the next tile grid
		Array.copy(tileGrid, nextTileGrid);

		// copy to the colour grids
		Array.copy(tileGrid, colourTileGrid);
		Array.copy(tileGrid, colourTileHistoryGrid);
	};

	// clear boundary of state 6 cells
	Life.prototype.clearBoundaryOfState6 = function() {
			// top left
		var	/* @type {number} */ leftX = Math.round((this.width - this.boundedGridWidth) / 2),
			/** @type {number} */ bottomY = Math.round((this.height - this.boundedGridHeight) / 2),

			// bottom right
			/** @type {number} */ rightX = leftX + this.boundedGridWidth - 1,
			/** @type {number} */ topY = bottomY + this.boundedGridHeight - 1,

			// overlay grid
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,

			// overlay rows
			/** @type {Uint8Array} */ topOverlayRow = null,
			/** @type {Uint8Array} */ bottomOverlayRow = null,

			// state 6 cell value in overlay map
			/** @type {number} */ state6Value = 129,

			// flag if state 6 cells found
			/** @type {boolean} */ state6Found = false,

			// counters
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0;

		// extend to the boundary
		leftX -= 1;
		rightX += 1;
		topY += 1;
		bottomY -= 1;

		// set top and bottom row
		topOverlayRow = overlayGrid[topY];
		bottomOverlayRow = overlayGrid[bottomY];

		// check for infinite width
		if (this.boundedGridWidth === 0) {
			// just clear top and bottom
			for (x = leftX; x <= rightX; x += 1) {
				if (bottomOverlayRow[x] === state6Value) {
					state6Found = true;
					bottomOverlayRow[x] = 0;
				}

				if (topOverlayRow[x] === state6Value) {
					state6Found = true;
					topOverlayRow[x] = 0;
				}
			}
		} else {
			// check for infinite height
			if (this.boundedGridHeight === 0) {
				// just clear left and right
				for (y = 0; y < this.height; y += 1) {
					if (overlayGrid[y][leftX] === state6Value) {
						state6Found = true;
						overlayGrid[y][leftX] = 0;
					}

					if (overlayGrid[y][rightX] === state6Value) {
						state6Found = true;
						overlayGrid[y][rightX] = 0;
					}
				}
			} else {
				// clear top and bottom boundary
				for (x = leftX; x <= rightX; x += 1) {
					if (bottomOverlayRow[x] === state6Value) {
						state6Found = true;
						bottomOverlayRow[x] = 0;
					}

					if (topOverlayRow[x] === state6Value) {
						state6Found = true;
						topOverlayRow[x] = 0;
					}
				}

				// clear left and right boundary
				for (y = bottomY + 1; y <= topY - 1; y += 1) {
					if (overlayGrid[y][leftX] === state6Value) {
						state6Found = true;
						overlayGrid[y][leftX] = 0;
					}

					if (overlayGrid[y][rightX] === state6Value) {
						state6Found = true;
						overlayGrid[y][rightX] = 0;
					}
				}
			}
		}

		// check if state 6 cells found
		if (state6Found) {
			this.populateState6MaskFromColGrid();
		}
	};

	// clear outside boundary for RuleTable B0 rules
	Life.prototype.clearOutsideRuleTableB0 = function() {
			// bounding box
		var	/** @type {number} */ leftX = Math.round((this.width - this.boundedGridWidth) / 2),
			/** @type {number} */ bottomY = Math.round((this.height - this.boundedGridHeight) / 2),
			/** @type {number} */ rightX = leftX + this.boundedGridWidth - 1,
			/** @type {number} */ topY = bottomY + this.boundedGridHeight - 1,

			// border around bounded grid
			/** @const {number} */ border = 1 << this.tilePower,

			// box around bounded grid
			/** @type {number} */ boxLeft = leftX - border,
			/** @type {number} */ boxBottom = bottomY - border,
			/** @type {number} */ boxRight = rightX + border,
			/** @type {number} */ boxTop = topY + border,

			// counters
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,

			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null;

		// swap grids every generation
		if ((this.counter & 1) !== 0) {
			colourGrid = this.nextColourGrid;
		}

		// clip box around bounded grid to grid
		if (boxLeft < 0) {
			boxLeft = 0;
		}

		if (boxRight >= this.width) {
			boxRight = this.width - 1;
		}

		if (boxBottom < 0) {
			boxBottom = 0;
		}

		if (boxBottom >= this.height) {
			boxBottom = this.height - 1;
		}

		// clear cells outside the grid
		for (y = boxBottom; y <= boxTop; y += 1) {
			colourRow = colourGrid[y];
			if (y < bottomY || y > topY) {
				for (x = boxLeft; x <= boxRight; x += 1) {
					if (colourRow[x] !== 0) {
						colourRow[x] = 0;
					}
				}
			} else {
				for (x = boxLeft; x <= boxRight; x += 1) {
					if (x < leftX || x > rightX) {
						if (colourRow[x] !== 0) {
							colourRow[x] = 0;
						}
					}
				}
			}
		}

		// now reset the tile map
		this.tileGrid.whole.fill(0);
		this.nextTileGrid.whole.fill(0);
		this.colourTileGrid.whole.fill(0);
		this.colourTileHistoryGrid.whole.fill(0);
		this.setBoundedTiles();

		// recalculate population
		this.population = 0;
		for (y = bottomY; y <= topY; y += 1) {
			colourRow = colourGrid[y];
			for (x = leftX; x <= rightX; x += 1) {
				if (colourRow[x] !== 0) {
					this.population += 1;
				}
			}
		}
	};

	// clear boundary
	Life.prototype.clearBoundary = function(/** @type {number} */ extra) {
		// life grid
		var	/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ colourGrid = null,
			/** @type {number} */ state = 0,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// top left
			/** @type {number} */ leftX = Math.round((this.width - this.boundedGridWidth) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - this.boundedGridHeight) / 2) + boxOffset,

			// bottom right
			/** @type {number} */ rightX = leftX + this.boundedGridWidth - 1,
			/** @type {number} */ topY = bottomY + this.boundedGridHeight - 1,

			// left and right extent in 16bit words
			/** @type {number} */ left16 = (leftX - extra) >> 4,
			/** @type {number} */ right16 = (rightX + extra) >> 4,

			// top and bottom row
			/** @type {Uint16Array} */ topRow = null,
			/** @type {Uint16Array} */ bottomRow = null,
			/** @type {Uint8Array} */ topColourRow = null,
			/** @type {Uint8Array} */ bottomColourRow = null,

			// left and right word
			/** @type {number} */ leftWord = (leftX - extra) >> 4,
			/** @type {number} */ rightWord = (rightX + extra) >> 4,

			// left and right mask
			/** @type {number} */ leftMask = ~(1 << (~(leftX - extra) & 15)),
			/** @type {number} */ rightMask = ~(1 << (~(rightX + extra) & 15)),

			// counters
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,

			// cell population to adjust
			/** @type {Uint8Array} */ bitCounts16 = this.bitCounts16,
			/** @type {number} */ remove = 0,
			/** @type {Uint16Array} */ gridy = null;

		// determine the buffer for current generation
		if (!(this.isPCA || this.isRuleTree || this.isExtended)) {
			if ((this.counter & 1) !== 0) {
				grid = this.nextGrid16;
			} else {
				grid = this.grid16;
			}

			// set top and bottom row
			topRow = grid[topY + extra];
			bottomRow = grid[bottomY - extra];

			// check for infinite width
			if (this.boundedGridWidth === 0) {
				// just clear top and bottom
				left16 = 0;
				right16 = this.width >> 4;
				for (x = left16; x < right16; x += 1) {
					remove += bitCounts16[bottomRow[x]];
					bottomRow[x] = 0;
					remove += bitCounts16[topRow[x]];
					topRow[x] = 0;
				}
			} else {
				// check for infinite height
				if (this.boundedGridHeight === 0) {
					// just clear left and right
					bottomY = 0;
					topY = this.height;
					for (y = 0; y < topY; y += 1) {
						gridy = grid[y];
						if (gridy[leftWord] & ~leftMask) {
							remove += 1;
						}
						gridy[leftWord] &= leftMask;
						if (gridy[rightWord] & ~rightMask) {
							remove += 1;
						}
						gridy[rightWord] &= rightMask;
					}
				} else {
					// clear top and bottom boundary
					for (x = left16; x <= right16; x += 1) {
						remove += bitCounts16[bottomRow[x]];
						bottomRow[x] = 0;
						remove += bitCounts16[topRow[x]];
						topRow[x] = 0;
					}

					// clear left and right boundary
					for (y = bottomY - extra + 1; y <= topY + extra - 1; y += 1) {
						gridy = grid[y];
						if (gridy[leftWord] & ~leftMask) {
							remove += 1;
						}
						gridy[leftWord] &= leftMask;
						if (gridy[rightWord] & ~rightMask) {
							remove += 1;
						}
						gridy[rightWord] &= rightMask;
					}
				}
			}

			// adjust population
			this.population -= remove;
		}

		// check for Generations or HROT
		if (this.multiNumStates !== -1) {
			// clear the colour grid boundary
			colourGrid = this.colourGrid;

			// check for PCA, RuleTree, Super or Extended rules
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				// swap grids every generation
				if ((this.counter & 1) !== 0) {
					colourGrid = this.nextColourGrid;
				}
			}

			// extend to the boundary
			leftX -= extra;
			rightX += extra;
			topY += extra;
			bottomY -= extra;

			// set top and bottom row
			topColourRow = colourGrid[topY];
			bottomColourRow = colourGrid[bottomY];

			// check for infinite width
			if (this.boundedGridWidth === 0) {
				// just clear top and bottom
				for (x = 0; x < this.width; x += 1) {
					state = bottomColourRow[x];
					if (state !== 0) {
						bottomColourRow[x] = 0;
						remove += 1;
					}

					state = topColourRow[x];
					if (state !== 0) {
						topColourRow[x] = 0;
						remove += 1;
					}
				}
			} else {
				// check for infinite height
				if (this.boundedGridHeight === 0) {
					// just clear left and right
					for (y = 0; y < this.height; y += 1) {
						state = colourGrid[y][leftX];
						if (state !== 0) {
							colourGrid[y][leftX] = 0;
							remove += 1;
						}

						state = colourGrid[y][rightX];
						if (state !== 0) {
							colourGrid[y][rightX] = 0;
							remove += 1;
						}
					}
				} else {
					// clear top and bottom boundary
					for (x = 0; x < this.width; x += 1) {
						state = bottomColourRow[x];
						if (state !== 0) {
							bottomColourRow[x] = 0;
							remove += 1;
						}

						state = topColourRow[x];
						if (state !== 0) {
							topColourRow[x] = 0;
							remove += 1;
						}
					}

					// clear left and right boundary
					for (y = bottomY + 1; y <= topY - 1; y += 1) {
						state = colourGrid[y][leftX];
						if (state !== 0) {
							colourGrid[y][leftX] = 0;
							remove += 1;
						}

						state = colourGrid[y][rightX];
						if (state !== 0) {
							colourGrid[y][rightX] = 0;
							remove += 1;
						}
					}
				}
			}

			// update population for RuleTable or Extended rules
			if (this.isRuleTree || this.isExtended) {
				this.population -= remove;
			}
		}
	};

	// delete a single glider
	Life.prototype.deleteGlider = function(/** @type {Array<Array<number>>} */ glider, /** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ orientation, /** @type {number} */ edge) {
		var	/** @type {number} */ xc = 0,
			/** @type {number} */ yc = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ deadCell = 0,
			/** @type {Array<number>} */ gliderRow = null;

		// select the correct history colour for HROT rules
		if (this.isHROT) {
			deadCell = this.view.maxHistoryStates;
		}

		// check if the glider is near bounding box edge
		if (!this.nearEdge(x, y, orientation, edge)) {
			this.numClearedGliders += 1;
			for (yc = 0; yc < glider.length; yc += 1) {
				gliderRow = glider[yc];
				// handle [R]History overlay states
				if (this.isLifeHistory) {
					for (xc = 0; xc < gliderRow.length; xc += 1) {
						if (gliderRow[xc] === 1) {
							state = this.getState(x + xc, y + yc, false);
							if (state === 3 || state === 5) {
								this.setState(x + xc, y + yc, 4, false);
							} else {
								if (state & 1) {
									this.setState(x + xc, y + yc, 0, false);
								}
							}
							this.deaths += 1;
						}
					}
				} else {
					// all other rule types
					for (xc = 0; xc < gliderRow.length; xc += 1) {
						if (gliderRow[xc] === 1) {
							this.setState(x + xc, y + yc, deadCell, false);
							this.deaths += 1;
						}
					}
				}
			}
		}
	};

	// check pattern for glider
	/** @returns {boolean} */
	Life.prototype.findAndDeleteGlider = function(/** @type {Array<Array<number>>} */ glider, /** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ dx, /** @type {number} */ dy, /** @type {number} */ orientation, /** @type {number} */ off, /** @type {number} */ edge) {
		var	/** @type {boolean} */ found = false,
			/** @type {Array<number>} */ gliderRow = null,
			/** @type {number} */ s1 = 0,
			/** @type {number} */ s2 = 0,
			/** @type {number} */ s3 = 0,
			/** @type {number} */ s4 = 0,
			/** @type {number} */ s5 = 0,
			/** @type {number} */ s6 = 0,
			/** @type {number} */ s7 = 0,
			/** @type {number} */ s8 = 0,
			/** @type {number} */ s9 = 0,
			/** @type {number} */ s10 = 0,
			/** @type {number} */ s11 = 0,
			/** @type {number} */ s12 = 0,
			/** @type {number} */ s13 = 0,
			/** @type {number} */ s14 = 0,
			/** @type {number} */ s15 = 0,
			/** @type {number} */ s16 = 0,
			/** @type {number} */ s17 = 0,
			/** @type {number} */ s18 = 0,
			/** @type {number} */ s19 = 0,
			/** @type {number} */ s20 = 0,
			/** @type {number} */ s21 = 0,
			/** @type {number} */ s22 = 0,
			/** @type {number} */ s23 = 0,
			/** @type {number} */ xc = 0,
			/** @type {number} */ yc = 0,
			/** @type {number} */ xFound = 0,
			/** @type {number} */ yFound = 0,
			/** @type {number} */ minX = 0,
			/** @type {number} */ minY = 0,
			/** @type {number} */ maxX = 0,
			/** @type {number} */ maxY = 0,
			/** @type {number} */ dx2 = dx + dx,
			/** @type {number} */ dy2 = dy + dy,
			/** @type {number} */ dx3 = dx2 + dx,
			/** @type {number} */ dy3 = dy2 + dy,
			/** @type {number} */ dx4 = dx3 + dx,
			/** @type {number} */ dy4 = dy3 + dy,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			//** @type {Uint8Array} */ overlayRow = null,
			//** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {BoundingBox} */ box = (this.isHROT ? this.HROTBox : this.zoomBox),
			/** @const {number} */ leftX = box.leftX,
			/** @const {number} */ rightX = box.rightX,
			/** @const {number} */ bottomY = box.bottomY,
			/** @const {number} */ topY = box.topY,
			/** @const {number} */ aliveStart = this.aliveStart,
			/** @type {number} */ cell = 0,
			/** @type {number} */ maxDelta = Math.abs(dx4 + dx4);

		// search grid for glider
		x -= 2;
		y -= 2;
		yc = 0;
		found = true;

		while (found && yc < glider.length) {
			xc = 0;
			gliderRow = glider[yc];
			while (found && xc < gliderRow.length) {
				cell = gliderRow[xc];
				s1 = colourGrid[y + yc][x + xc];
				if (!(s1 < aliveStart && cell === 0 || s1 >= aliveStart && cell === 1)) {
					found = false;
				}
				xc += 1;
			}
			yc += 1;
		}

		// if found then check separation
		if (found) {
			xc = x + 3 + dx;
			yc = y + 3 + dy;
			xc += dx;
			yc += dy;
			minX = leftX;
			maxX = rightX;
			minY = bottomY;
			maxY = topY;

			// handle maximum grid edge
			if (minX < maxDelta) {
				minX = maxDelta;
			}
			if (minY < maxDelta) {
				minY = maxDelta;
			}
			if (maxX > this.width - 1 - maxDelta) {
				maxX = this.width - 1 - maxDelta;
			}
			if (maxY > this.height - 1 - maxDelta) {
				maxY = this.height - 1 - maxDelta;
			}

			found = false;

			while (!found && !(xc < minX || xc > maxX || yc < minY || yc > maxY)) {
				colourRow = colourGrid[yc];

				/* TBD debug search area
				if (overlayGrid) {
					var mark = ViewConstants.stateMap[4] + 128;
					overlayRow = overlayGrid[yc];
					overlayRow[xc] = mark;
					overlayRow[xc + dx] = mark;
					overlayRow[xc + dx2] = mark;
					overlayRow[xc + dx3] = mark;
					overlayRow[xc + dx4] = mark;
					overlayRow[xc + dx4 + dx] = mark;
					overlayRow[xc + dx4 + dx2] = mark;
					overlayRow[xc + dx4 + dx3] = mark;
					overlayGrid[yc + dy][xc] = mark;
					overlayGrid[yc + dy2][xc] = mark;
					overlayGrid[yc + dy3][xc] = mark;
					overlayGrid[yc + dy4][xc] = mark;
					overlayGrid[yc + dy4 + dy][xc] = mark;
					overlayGrid[yc + dy4 + dy2][xc] = mark;
					overlayGrid[yc + dy4 + dy3][xc] = mark;

					overlayGrid[yc - dy4][xc] = mark;
					overlayRow[xc - dx4] = mark;
					overlayGrid[yc - dy4 - dy][xc] = mark;
					overlayRow[xc - dx4 - dx] = mark;
					overlayGrid[yc - dy4 - dy2][xc] = mark;
					overlayRow[xc - dx4 - dx2] = mark;
				}
				*/

				// look diagonal
				s1 = colourRow[xc];
				if (s1 & 64) {
					xFound = xc;
					yFound = yc;
					found = true;
				}

				s2 = colourGrid[yc + dy][xc];
				if (s2 & 64) {
					xFound = xc;
					yFound = yc + dy;
					found = true;
				}

				s3 = colourRow[xc + dx];
				if (s3 & 64) {
					xFound = xc + dx;
					yFound = yc;
					found = true;
				}

				s4 = colourGrid[yc + dy2][xc];
				if (s4 & 64) {
					xFound = xc;
					yFound = yc + dy2;
					found = true;
				}

				s5 = colourRow[xc + dx2];
				if (s5 & 64) {
					xFound = xc + dx2;
					yFound = yc;
					found = true;
				}

				s6 = colourGrid[yc + dy3][xc];
				if (s6 & 64) {
					xFound = xc;
					yFound = yc + dy3;
					found = true;
				}

				s7 = colourRow[xc + dx3];
				if (s7 & 64) {
					xFound = xc + dx3;
					yFound = yc;
					found = true;
				}

				s8 = colourGrid[yc + dy4][xc];
				if (s8 & 64) {
					xFound = xc;
					yFound = yc + dy4;
					found = true;
				}

				s9 = colourRow[xc + dx4];
				if (s9 & 64) {
					xFound = xc + dx4;
					yFound = yc;
					found = true;
				}

				s10 = colourGrid[yc + dy4 + dy][xc];
				if (s10 & 64) {
					xFound = xc;
					yFound = yc + dy4 + dy;
					found = true;
				}

				s11 = colourRow[xc + dx4 + dx];
				if (s11 & 64) {
					xFound = xc + dx4 + dx;
					yFound = yc + dy;
					found = true;
				}

				s12 = colourGrid[yc + dy4 + dy][xc];
				if (s12 & 64) {
					xFound = xc;
					yFound = yc + dy4 + dy;
					found = true;
				}

				s13 = colourRow[xc + dx4 + dx];
				if (s13 & 64) {
					xFound = xc + dx4 + dx;
					yFound = yc;
					found = true;
				}

				s14 = colourGrid[yc + dy4 + dy2][xc];
				if (s14 & 64) {
					xFound = xc;
					yFound = yc + dy4 + dy2;
					found = true;
				}

				s15 = colourRow[xc + dx4 + dx2];
				if (s15 & 64) {
					xFound = xc + dx4 + dx2;
					yFound = yc;
					found = true;
				}

				s16 = colourGrid[yc + dy4 + dy3][xc];
				if (s16 & 64) {
					xFound = xc;
					yFound = yc + dy4 + dy3;
					found = true;
				}

				s17 = colourRow[xc + dx4 + dx3];
				if (s17 & 64) {
					xFound = xc + dx4 + dx3;
					yFound = yc;
					found = true;
				}

				// look ahead
				s18 = colourGrid[yc - dy4][xc];
				if (s18 & 64) {
					xFound = xc;
					yFound = yc - dy4;
					found = true;
				}

				s19 = colourRow[xc - dx4];
				if (s19 & 64) {
					xFound = xc - dx4;
					yFound = yc;
					found = true;
				}

				s20 = colourGrid[yc - dy4 - dy][xc];
				if (s20 & 64) {
					xFound = xc;
					yFound = yc - dy4 - dy;
					found = true;
				}

				s21 = colourRow[xc - dx4 - dx];
				if (s21 & 64) {
					xFound = xc - dx4 - dx;
					yFound = yc;
					found = true;
				}

				s22 = colourGrid[yc - dy4 - dy2][xc];
				if (s22 & 64) {
					xFound = xc;
					yFound = yc - dy4 - dy2;
					found = true;
				}

				s23 = colourRow[xc - dx4 - dx2];
				if (s23 & 64) {
					xFound = xc - dx4 - dx2;
					yFound = yc;
					found = true;
				}

				xc += dx;
				yc += dy;
			}

			// add to potential clear list
			this.potentialClears[this.potentialClears.length] = {glider: glider, x: x, y: y, xc: xFound, yc: yFound, orientation: orientation, detected: found, offset: off, edge: edge};
		}
		return found;
	};

	// check if a single glider is near the bounding box edge
	/** @returns {boolean} */
	Life.prototype.nearEdge = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ orientation, /** @type {number} */ edge) {
		var	/** @type {boolean} */ result = false,
			/** @type {BoundingBox} */ box = (this.isHROT ? this.HROTBox : this.zoomBox),
			/** @type {number} */ leftX = box.leftX,
			/** @type {number} */ rightX = box.rightX,
			/** @type {number} */ bottomY = box.bottomY,
			/** @type {number} */ topY = box.topY,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {number} */ aliveStart = this.aliveStart,
			/** @type {number} */ i = 0,

			// /** @type {number} */ mark = ViewConstants.stateMap[4] + 128,

			/** @type {number} */ j = 0;

		switch (orientation) {
			case LifeConstants.gliderNE:
				// glider is going NE
				if (edge === LifeConstants.gliderE) {
					// glider is at E boundary
					i = y + 1;
					j = x + 1;
					while (i >= bottomY && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i][j + 1] = mark;
						//this.overlayGrid[i][j + 2] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i][j + 1] >= aliveStart || colourGrid[i][j + 2] >= aliveStart) {
							result = true;
						}
						i -= 1;
					}
				} else {
					// glider is at N boundary
					i = y + 5;
					j = x + 5;
					while (j <= rightX && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i - 1][j] = mark;
						//this.overlayGrid[i - 2][j] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i - 1][j] >= aliveStart || colourGrid[i - 2][j] >= aliveStart) {
							result = true;
						}
						j += 1;
					}
				}
				break;

			case LifeConstants.gliderSE:
				// glider is going SE
				if (edge === LifeConstants.gliderE) {
					// glider is at E boundary
					i = y + 5;
					j = x + 1;
					while (i <= topY && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i][j + 1] = mark;
						//this.overlayGrid[i][j + 2] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i][j + 1] >= aliveStart || colourGrid[i][j + 2] >= aliveStart) {
							result = true;
						}
						i += 1;
					}
				} else {
					// glider is at S boundary
					i = y + 1;
					j = x + 5;
					while (j <= rightX && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i + 1][j] = mark;
						//this.overlayGrid[i + 2][j] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i + 1][j] >= aliveStart || colourGrid[i + 2][j] >= aliveStart) {
							result = true;
						}
						j += 1;
					}

				}
				break;

			case LifeConstants.gliderSW:
				// glider is going SW
				if (edge === LifeConstants.gliderW) {
					// glider is at W boundary
					i = y + 5;
					j = x + 5;
					while (i <= topY && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i][j - 1] = mark;
						//this.overlayGrid[i][j - 2] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i][j - 1] >= aliveStart || colourGrid[i][j - 2] >= aliveStart) {
							result = true;
						}
						i += 1;
					}
				} else {
					// glider is at S boundary
					i = y + 1;
					j = x + 1;
					while (j >= leftX && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i + 1][j] = mark;
						//this.overlayGrid[i + 2][j] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i + 1][j] >= aliveStart || colourGrid[i + 2][j] >= aliveStart) {
							result = true;
						}
						j -= 1;
					}

				}
				break;

			case LifeConstants.gliderNW:
				// glider is going NW
				if (edge === LifeConstants.gliderW) {
					// glider is at W boundary
					i = y + 1;
					j = x + 5;
					while (i >= bottomY && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i][j - 1] = mark;
						//this.overlayGrid[i][j - 2] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i][j - 1] >= aliveStart || colourGrid[i][j - 2] >= aliveStart) {
							result = true;
						}
						i -= 1;
					}
				} else {
					// glider is at N boundary
					i = y + 5;
					j = x + 1;
					while (j >= leftX && !result) {

						//this.overlayGrid[i][j] = mark;
						//this.overlayGrid[i - 1][j] = mark;
						//this.overlayGrid[i - 2][j] = mark;

						if (colourGrid[i][j] >= aliveStart || colourGrid[i - 1][j] >= aliveStart || colourGrid[i - 2][j] >= aliveStart) {
							result = true;
						}
						j -= 1;
					}
				}
				break;
		}

		return result;
	};

	// check potential glider clears
	Life.prototype.checkPotentialClears = function() {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ otherX = 0,
			/** @type {number} */ otherY = 0,
			/** @type {number} */ otherOrientation = 0,
			/** @type {number} */ orientation = 0,
			/** @type {number} */ l = this.potentialClears.length,
			/** @type {boolean} */ found = false,
			target = null,
			current = null;

		// check for at least two gliders
		if (l > 1) {
			for (i = 0; i < l; i += 1) {
				// check if the current glider has a target
				current = this.potentialClears[i];
				if (current.detected) {
					// get the hit location
					x = current.xc;
					y = current.yc;
					orientation = current.orientation;

					// check if target was another edge glider
					j = 0;
					found = false;
					while (!found && j < l) {
						// ignore same glider
						if (j !== i) {
							// get the other glider
							target = this.potentialClears[j];

							// get the middle of the other glider
							otherX = target.x + 3;
							otherY = target.y + 3;

							// check if the other glider contains the hit location
							if (x >= otherX - 1 && x <= otherX + 1 && y >= otherY -1 && y <= otherY + 1 && target.orientation === orientation && current.offset === 0) {
								// delete glider
								this.deleteGlider(current.glider, current.x, current.y, current.orientation, current.edge);
								found = true;
							}
						}
						j += 1;
					}
				} else {
					// get the glider location
					x = current.x + 3;
					y = current.y + 3;
					orientation = current.orientation;

					// check if there is another glider on the same axis
					j = 0;
					found = false;
					while (!found && j < l) {
						// ignore same glider
						if (j !== i) {
							// get the other glider
							target = this.potentialClears[j];

							// get the middle of the other glider
							otherX = target.x + 3;
							otherY = target.y + 3;
							otherOrientation = target.orientation;

							switch (orientation) {
								case LifeConstants.gliderNW:
									if (otherOrientation === LifeConstants.gliderNE && y >= otherY - 6 && y <= otherY + 6 && x >= otherX) {
										found = true;
									}

									if (otherOrientation === LifeConstants.gliderSW && x >= otherX - 6 && x <= otherX + 6 && y >= otherY) {
										found = true;
									}
									break;

								case LifeConstants.gliderNE:
									if (otherOrientation === LifeConstants.gliderNW && y >= otherY - 6 && y <= otherY + 6 && x <= otherX) {
										found = true;
									}

									if (otherOrientation === LifeConstants.gliderSE && x >= otherX - 6 && x <= otherX + 6 && y >= otherY) {
										found = true;
									}
									break;

								case LifeConstants.gliderSW:
									if (otherOrientation === LifeConstants.gliderSE && y >= otherY - 6 && y <= otherY + 6 && x >= otherX) {
										found = true;
									}

									if (otherOrientation === LifeConstants.gliderNW && x >= otherX - 6 && x <= otherX + 6 && y <= otherY) {
										found = true;
									}
									break;

								case LifeConstants.gliderSE:
									if (otherOrientation === LifeConstants.gliderSW && y >= otherY - 6 && y <= otherY + 6 && x <= otherX) {
										found = true;
									}

									if (otherOrientation === LifeConstants.gliderNE && x >= otherX - 6 && x <= otherX + 6 && y <= otherY) {
										found = true;
									}
									break;
							}
						}
						j += 1;
					}
					if (!found && current.offset === 0) {
						this.deleteGlider(current.glider, current.x, current.y, current.orientation, current.edge);
					}
				}
			}
		} else {
			// check for a single glider that hasn't detected a potential collision
			if (l === 1) {
				current = this.potentialClears[0];
				if (!current.detected && current.offset === 0) {
					this.deleteGlider(current.glider, current.x, current.y, current.orientation, current.edge);
				}
			}
		}
	};

	// clear escaping gliders
	Life.prototype.clearEscapingGliders = function() {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ off = 0,
			/** @type {BoundingBox} */ box = (this.isHROT ? this.HROTBox : this.zoomBox),
			/** @type {number} */ leftX = box.leftX,
			/** @type {number} */ rightX = box.rightX,
			/** @type {number} */ bottomY = box.bottomY,
			/** @type {number} */ topY = box.topY,
			/** @type {BoundingBox} */ safeBox = new BoundingBox(0, 0, 0, 0),
			/** @const {number} */ safeBorder = 16,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ topRow = colourGrid[topY],
			/** @type {Uint8Array} */ bottomRow = colourGrid[bottomY],
			/** @type {number} */ aliveStart = this.aliveStart,
			/** @type {Uint8Array} */ currentRow = null,
			/** @type {number} */ gSize = 6;

		// do nothing if no cells alive
		if (this.population === 0) {
			return;
		}

		// ignore bounded grids and HROT rules > range 1
		if (!(this.boundedGridType !== -1 || (this.isHROT && this.HROT.xrange > 1))) {
			// clear potential clear list
			this.potentialClears = [];

			// ensure bounding box is not too close to grid boundary
			if (leftX < safeBorder) {
				leftX = safeBorder;
			}
			if (rightX >= this.width - safeBorder) {
				rightX = this.width - safeBorder - 1;
			}
			if (bottomY < safeBorder) {
				bottomY = safeBorder;
			}
			if (topY >= this.height - safeBorder) {
				topY = this.height - safeBorder - 1;
			}

			// remember the safe bounding box
			safeBox.leftX = leftX;
			safeBox.bottomY = bottomY;
			safeBox.rightX = rightX;
			safeBox.topY = topY;

			// check top and bottom rows
			for (x = leftX - gSize; x <= rightX + gSize; x += 1) {
				// check several rows
				for (off = 0; off <= 5; off += 1) {
					bottomY = safeBox.bottomY + off;
					bottomRow = colourGrid[bottomY];

					if (bottomRow[x] >= aliveStart) {
						// NW and NE glider
						if (!this.findAndDeleteGlider(this.gliderNW07x7, x, bottomY, -1, 1, LifeConstants.gliderNW, off, LifeConstants.gliderS)) {
							if (!this.findAndDeleteGlider(this.gliderNW17x7, x - 1, bottomY, -1, 1, LifeConstants.gliderNW, off, LifeConstants.gliderS)) {
								if (!this.findAndDeleteGlider(this.gliderNW27x7, x, bottomY, -1, 1, LifeConstants.gliderNW, off, LifeConstants.gliderS)) {
									if (!this.findAndDeleteGlider(this.gliderNW37x7, x - 1, bottomY, -1, 1, LifeConstants.gliderNW, off, LifeConstants.gliderS)) {
										if (!this.findAndDeleteGlider(this.gliderNE07x7, x, bottomY, 1, 1, LifeConstants.gliderNE, off, LifeConstants.gliderS)) {
											if (!this.findAndDeleteGlider(this.gliderNE17x7, x - 1, bottomY, 1, 1, LifeConstants.gliderNE, off, LifeConstants.gliderS)) {
												if (!this.findAndDeleteGlider(this.gliderNE27x7, x - 1, bottomY, 1, 1, LifeConstants.gliderNE, off, LifeConstants.gliderS)) {
													this.findAndDeleteGlider(this.gliderNE37x7, x, bottomY, 1, 1, LifeConstants.gliderNE, off, LifeConstants.gliderS);
												}
											}
										}
									}
								}
							}
						}
					}

					topY = safeBox.topY - off;
					topRow = colourGrid[topY];

					if (topRow[x] >= aliveStart) {
						// SW and SE glider
						if (!this.findAndDeleteGlider(this.gliderSW07x7, x, topY - 2, -1, -1, LifeConstants.gliderSW, off, LifeConstants.gliderN)) {
							if (!this.findAndDeleteGlider(this.gliderSW17x7, x - 1, topY - 2, -1, -1, LifeConstants.gliderSW, off, LifeConstants.gliderN)) {
								if (!this.findAndDeleteGlider(this.gliderSW27x7, x, topY - 2, -1, -1, LifeConstants.gliderSW, off, LifeConstants.gliderN)) {
									if (!this.findAndDeleteGlider(this.gliderSW37x7, x - 1, topY - 2, -1, -1, LifeConstants.gliderSW, off, LifeConstants.gliderN)) {
										if (!this.findAndDeleteGlider(this.gliderSE07x7, x, topY - 2, 1, -1, LifeConstants.gliderSE, off, LifeConstants.gliderN)) {
											if (!this.findAndDeleteGlider(this.gliderSE17x7, x - 1, topY - 2, 1, -1, LifeConstants.gliderSE, off, LifeConstants.gliderN)) {
												if (!this.findAndDeleteGlider(this.gliderSE27x7, x - 1, topY - 2, 1, -1, LifeConstants.gliderSE, off, LifeConstants.gliderN)) {
													this.findAndDeleteGlider(this.gliderSE37x7, x, topY - 2, 1, -1, LifeConstants.gliderSE, off, LifeConstants.gliderN);
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}

			// check left and right columns
			for (y = bottomY - gSize; y <= topY + gSize; y += 1) {
				// check several columns
				for (off = 0; off <= 5; off += 1) {
					currentRow = colourGrid[y];
					leftX = safeBox.leftX + off;

					if (currentRow[leftX] >= aliveStart) {
						// NW and SW glider
						if (!this.findAndDeleteGlider(this.gliderNW07x7, leftX, y, 1, -1, LifeConstants.gliderNW, off, LifeConstants.gliderW)) {
							if (!this.findAndDeleteGlider(this.gliderNW17x7, leftX, y - 1, 1, -1, LifeConstants.gliderNW, off, LifeConstants.gliderW)) {
								if (!this.findAndDeleteGlider(this.gliderNW27x7, leftX, y, 1, -1, LifeConstants.gliderNW, off, LifeConstants.gliderW)) {
									if (!this.findAndDeleteGlider(this.gliderNW37x7, leftX, y - 1, 1, -1, LifeConstants.gliderNW, off, LifeConstants.gliderW)) {
										if (!this.findAndDeleteGlider(this.gliderSW07x7, leftX, y - 1, 1, 1, LifeConstants.gliderSW, off, LifeConstants.gliderW)) {
											if (!this.findAndDeleteGlider(this.gliderSW17x7, leftX, y, 1, 1, LifeConstants.gliderSW, off, LifeConstants.gliderW)) {
												if (!this.findAndDeleteGlider(this.gliderSW27x7, leftX, y, 1, 1, LifeConstants.gliderSW, off, LifeConstants.gliderW)) {
													this.findAndDeleteGlider(this.gliderSW37x7, leftX, y - 1, 1, 1, LifeConstants.gliderSW, off, LifeConstants.gliderW);
												}
											}
										}
									}
								}
							}
						}
					}

					rightX = safeBox.rightX - off;
					if (currentRow[rightX] >= aliveStart) {
						// NE and SE glider
						if (!this.findAndDeleteGlider(this.gliderNE07x7, rightX - 2, y, -1, -1, LifeConstants.gliderNE, off, LifeConstants.gliderE)) {
							if (!this.findAndDeleteGlider(this.gliderNE17x7, rightX - 2, y - 1, -1, -1, LifeConstants.gliderNE, off, LifeConstants.gliderE)) {
								if (!this.findAndDeleteGlider(this.gliderNE27x7, rightX - 2, y, -1, -1, LifeConstants.gliderNE, off, LifeConstants.gliderE)) {
									if (!this.findAndDeleteGlider(this.gliderNE37x7, rightX - 2, y - 1, -1, -1, LifeConstants.gliderNE, off, LifeConstants.gliderE)) {
										if (!this.findAndDeleteGlider(this.gliderSE07x7, rightX - 2, y - 1, -1, 1, LifeConstants.gliderSE, off, LifeConstants.gliderE)) {
											if (!this.findAndDeleteGlider(this.gliderSE17x7, rightX - 2, y, -1, 1, LifeConstants.gliderSE, off, LifeConstants.gliderE)) {
												if (!this.findAndDeleteGlider(this.gliderSE27x7, rightX - 2, y, -1, 1, LifeConstants.gliderSE, off, LifeConstants.gliderE)) {
													this.findAndDeleteGlider(this.gliderSE37x7, rightX - 2, y - 1, -1, 1, LifeConstants.gliderSE, off, LifeConstants.gliderE);
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}

			// check potential clears
			this.checkPotentialClears();

			// check if shrink needed
			this.doShrink();
		}
	};

	// set bounded grid tiles
	Life.prototype.setBoundedTiles = function() {
		// counters
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ i = 0,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bounded grid dimensions
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ value = 0,

			// rows
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null;

		// handle bounded grid on tile boundary
		for (i = 0; i <= 1; i += 1) {
			leftX = (Math.round((this.width - width) / 2 - 1) + boxOffset + i) >> this.tilePower;
			rightX = (Math.round((this.width + width) / 2) + boxOffset - i) >> this.tilePower;
			bottomY = (Math.round((this.height - height) / 2 - 1 + i) + boxOffset) >> this.tilePower;
			topY = (Math.round((this.height + height) / 2) + boxOffset - i) >> this.tilePower;

			// check for infinite height
			if (height === 0) {
				bottomY = 0;
				topY = (this.height >> this.tilePower) - 1;
			}

			// check for infinite width
			if (width === 0) {
				leftX = 0;
				rightX = (this.width >> this.tilePower) - 1;
			}

			// ensure tiles are on grid
			if (leftX < 0) {
				leftX = 0;
			}
			if (bottomY < 0) {
				bottomY = 0;
			}
			if (rightX >= (this.width >> this.tilePower)) {
				rightX = (this.width >> this.tilePower) - 1;
			}
			if (topY >= (this.height >> this.tilePower)) {
				topY = (this.height >> this.tilePower) - 1;
			}

			// check for RuleTable B0 in Bounded Grid
			if (this.ruleTableB0) {
				// for B0 set all tiles in the bounded grid
				for (y = bottomY; y <= topY; y += 1) {
					tileRow = this.tileGrid[y];
					nextTileRow = this.nextTileGrid[y];
					colourTileRow = this.colourTileGrid[y];
					colourTileHistoryRow = this.colourTileHistoryGrid[y];

					for (x = leftX; x <= rightX; x += 1) {
						value = 1 << (~x & 15);
						tileRow[x >> 4] |= value;
						nextTileRow[x >> 4] |= value;
						colourTileRow[x >> 4] |= value;
						colourTileHistoryRow[x >> 4] |= value;
					}
				}
			} else {
				// set the top and bottom row of the bounded grid in the tile map
				for (x = leftX; x <= rightX; x += 1) {
					value = 1 << (~x & 15);

					// bottom row
					this.tileGrid[bottomY][x >> 4] |= value;
					this.nextTileGrid[bottomY][x >> 4] |= value;
					this.colourTileGrid[bottomY][x >> 4] |= value;
					this.colourTileHistoryGrid[bottomY][x >> 4] |= value;

					// top row
					this.tileGrid[topY][x >> 4] |= value;
					this.nextTileGrid[topY][x >> 4] |= value;
					this.colourTileGrid[topY][x >> 4] |= value;
					this.colourTileHistoryGrid[topY][x >> 4] |= value;
				}

				// set left and right column of the bounded grid in the tile map
				for (y = bottomY; y <= topY; y += 1) {
					// left column
					value = 1 << (~leftX & 15);
					this.tileGrid[y][leftX >> 4] |= value;
					this.nextTileGrid[y][leftX >> 4] |= value;
					this.colourTileGrid[y][leftX >> 4] |= value;
					this.colourTileHistoryGrid[y][leftX >> 4] |= value;

					// right column
					value = 1 << (~rightX & 15);
					this.tileGrid[y][rightX >> 4] |= value;
					this.nextTileGrid[y][rightX >> 4] |= value;
					this.colourTileGrid[y][rightX >> 4] |= value;
					this.colourTileHistoryGrid[y][rightX >> 4] |= value;
				}
			}
		}
	};

	// process torus for [R]History overlay
	Life.prototype.processTorusLH = function(/** @type {Array<Uint8Array>} */ grid) {
		// bounded grid width and height
		var	/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2),
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2),

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// horizontal and vertical shifts
			/** @type {number} */ horizShift = this.boundedGridHorizontalShift,
			/** @type {number} */ vertShift = this.boundedGridVerticalShift,

			// colour tile grid
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileHistoryGrid,

			// whether state 6 found
			/** @type {boolean} */ state6Found = false,

			// state 6 in overlay state map
			/** @type {number} */ state6Value = 129,

			// counters
			/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ i = 0;

		// check for infinite width
		if (this.boundedGridWidth === 0) {
			// process whole width
			leftX = 0;
			rightX = this.width - 1;
			width = this.width;
		}

		// check for infinite height
		if (this.boundedGridHeight === 0) {
			// process whole height
			bottomY = 0;
			topY = this.height - 1;
			height = this.height;
		}

		// check for infinite height
		if (this.boundedGridHeight !== 0) {
			// perform vertical wrap
			for (i = 0; i < width; i += 1) {
				// copy top row to below bottom
				sourceX = leftX + i;
				destX = leftX + ((((i + horizShift) % width) + width) % width);

				// check if cell is state 6
				state = grid[topY][sourceX];
				if (state === state6Value) {
					state6Found = true;

					// copy cell to below bottom row
					grid[bottomY - 1][destX] = state;

					// set tile grid
					colourTileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						colourTileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					}
				}

				// copy bottom row to above top
				destX = leftX + ((((i - horizShift) % width) + width) % width);

				// check if cell is state 6
				state = grid[bottomY][sourceX];
				if (state === state6Value) {
					state6Found = true;

					// copy cell to above top row
					grid[topY + 1][destX] = state;

					// set tile grid
					colourTileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						colourTileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					}
				}
			}
		}

		// check for infinite width
		if (this.boundedGridWidth !== 0) {
			// perform horizontal wrap
			for (i = 0; i < height; i += 1) {
				// copy left column to right of right
				sourceY = bottomY + i;
				destY = bottomY + ((((i - vertShift) % height) + height) % height);

				// check if cell is state 6
				state = grid[sourceY][leftX];
				if (state === state6Value) {
					state6Found = true;

					// copy cell to right of right edge
					grid[destY][rightX + 1] = state;

					// set tile grid
					colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// copy right column to left of left
				destY = bottomY + ((((i + vertShift) % height) + height) % height);

				// check if cell is state 6
				state = grid[sourceY][rightX];
				if (state === state6Value) {
					state6Found = true;

					// copy cell to left of left edge
					grid[destY][leftX - 1] = state;

					// set tile grid
					colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}
			}
		}

		// only process corners if both dimensions are not infinite
		if (this.boundedGridWidth !== 0 && this.boundedGridHeight !== 0) {
			// bottom right corner
			sourceX = leftX + ((-horizShift + width) % width);
			sourceY = bottomY + ((height - 1 + vertShift + height) % height);
			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				grid[bottomY - 1][rightX + 1] = state;
				state6Found = true;
			}

			// bottom left corner
			sourceX = leftX + ((width - 1 - horizShift + width) % width);
			sourceY = bottomY + ((height - 1 - vertShift + height) % height);
			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				grid[bottomY - 1][leftX - 1] = state;
				state6Found = true;
			}

			// top right corner
			sourceX = leftX + ((horizShift + width) % width);
			sourceY = bottomY + ((vertShift + height) % height);
			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				grid[topY + 1][rightX + 1] = state;
				state6Found = true;
			}

			// top left corner
			sourceX = leftX + ((width - 1 + horizShift + width) % width);
			sourceY = bottomY + ((-vertShift + height) % height);
			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				grid[topY + 1][leftX - 1] = state;
				state6Found = true;
			}
		}

		// check if any state 6 cells were found
		if (state6Found) {
			this.populateState6MaskFromColGrid();
		}
	};

	// process klein bottle for [R]History overlay
	Life.prototype.processKleinLH = function(/** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileHistoryGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2),
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2),

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// horizontal and vertical shifts
			/** @type {number} */ horizShift = this.boundedGridHorizontalShift,
			/** @type {number} */ vertShift = this.boundedGridVerticalShift,

			// horizontal and vertical twists
			/** @type {boolean} */ horizTwist = this.boundedGridHorizontalTwist,
			/** @type {boolean} */ vertTwist = this.boundedGridVerticalTwist,

			// whether state 6 found
			/** @type {boolean} */ state6Found = false,

			// state 6 in overlay state map
			/** @type {number} */ state6Value = 129,

			// counters
			/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ i = 0;

		// perform vertical wrap
		for (i = 0; i < width; i += 1) {
			// copy top row to below bottom
			sourceX = leftX + i;

			// check for twist
			if (horizTwist) {
				destX = rightX - ((i + horizShift + width) % width);
			} else {
				destX = leftX + ((i + horizShift + width) % width);
			}

			state = grid[topY][sourceX];
			if (state === state6Value) {
				state6Found = true;

				// copy cell to below bottom
				grid[bottomY - 1][destX] = state;

				// set tile grid
				colourTileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if (((bottomY - 1) & 15) === 15) {
					colourTileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// copy bottom row to above top
			if (horizTwist) {
				destX = rightX - ((i + horizShift + width) % width);
			} else {
				destX = leftX + ((i + horizShift + width) % width);
			}

			state = grid[bottomY][sourceX];
			if (state === state6Value) {
				state6Found = true;

				// copy cell to above top
				grid[topY + 1][destX] = state;

				// set tile grid
				colourTileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if (((topY + 1) & 15) === 0) {
					colourTileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}
		}

		// perform horizontal wrap
		for (i = 0; i < height; i += 1) {
			// copy left column to right of right
			sourceY = bottomY + i;

			// check for vertical twist
			if (vertTwist) {
				destY = topY - ((i + vertShift + height) % height);
			} else {
				destY = bottomY + ((i + vertShift + height) % height);
			}

			state = grid[sourceY][leftX];
			if (state === state6Value) {
				state6Found = true;

				// copy cell to right of right edge
				grid[destY][rightX + 1] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

				// check for tile boundary
				if (((rightX + 1) & 15) === 0) {
					colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
				}
			}

			// copy right column to left of left
			if (vertTwist) {
				destY = topY - ((i + vertShift + height) % height);
			} else {
				destY = bottomY + ((i + vertShift + height) % height);
			}

			state = grid[sourceY][rightX];
			if (state === state6Value) {
				state6Found = true;

				// copy cell to left of left edge
				grid[destY][leftX - 1] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

				// check for tile boundary
				if (((leftX - 1) & 15) === 15) {
					colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
				}
			}
		}

		// only process corners if both dimensions are not infinite
		if (this.boundedGridWidth !== 0 && this.boundedGridHeight !== 0) {
			if (this.processKleinCornersLH(horizTwist, leftX, bottomY, rightX, topY, horizShift, vertShift, grid, colourTileGrid)) {
				state6Found = true;
			}
		}

		// check if any state 6 cells were found
		if (state6Found) {
			this.populateState6MaskFromColGrid();
		}
	};

	// process cross-surface for [R]History overlay
	Life.prototype.processCrossSurfaceLH = function(/** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2),
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2),

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// whether state 6 found
			/** @type {boolean} */ state6Found = false,

			// state 6 in overlay state map
			/** @type {number} */ state6Value = 129,

			// counters
			/** @type {number} */ i = 0,
			/** @type {number} */ source = 0,
			/** @type {number} */ dest = 0,
			/** @type {number} */ state = 0;

		// perform vertical cross surface
		for (i = 0; i < width; i += 1) {
			source = leftX + i;
			dest = rightX - i;

			// copy top row to below bottom inverse order
			state = grid[topY][source];
			if (state === state6Value) {
				state6Found = true;

				grid[bottomY - 1][dest] = state;

				// set tile grid
				colourTileGrid[(bottomY - 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));

				// check for tile boundary
				if (((bottomY - 1) & 15) === 15) {
					colourTileGrid[((bottomY - 1) >> 4) + 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				}
			}

			// copy bottom row to above top inverse order
			state = grid[bottomY][source];
			if (state === state6Value) {
				state6Found = true;

				grid[topY + 1][dest] = state;

				// set tile grid
				colourTileGrid[(topY + 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));

				// check for tile boundary
				if (((topY + 1) & 15) === 0) {
					colourTileGrid[((topY + 1) >> 4) - 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				}
			}
		}

		// perform horizontal cross surface
		for (i = 0; i <= height; i += 1) {
			source = bottomY + i;
			dest = topY - i;

			// copy left column to right of right inverse order
			state = grid[source][leftX];
			if (state === state6Value) {
				state6Found = true;

				grid[dest][rightX + 1] = state;

				// set tile grid
				colourTileGrid[dest >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

				// check for tile boundary
				if (((rightX + 1) & 15) === 0) {
					colourTileGrid[dest >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
				}
			}

			// copy right column to left of left
			state = grid[source][rightX];
			if (state === state6Value) {
				state6Found = true;

				grid[dest][leftX - 1] = state;

				// set tile grid
				colourTileGrid[dest >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

				// check for tile boundary
				if (((leftX - 1) & 15) === 15) {
					colourTileGrid[dest >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
				}
			}
		}

		// top left corner
		state = grid[topY][leftX];
		if (state === state6Value) {
			grid[topY + 1][leftX - 1] = state;
			state6Found = true;
		}

		// top right corner
		state = grid[topY][rightX];
		if (state === state6Value) {
			grid[topY + 1][rightX + 1] = state;
			state6Found = true;
		}

		// bottom left corner
		state = grid[bottomY][leftX];
		if (state === state6Value) {
			grid[bottomY - 1][leftX - 1] = state;
			state6Found = true;
		}

		// bottom right corner
		state = grid[bottomY][rightX];
		if (state === state6Value) {
			grid[bottomY - 1][rightX + 1] = state;
			state6Found = true;
		}

		// check if any state 6 cells were found
		if (state6Found) {
			this.populateState6MaskFromColGrid();
		}
	};

	// process sphere for [R]History overlay
	Life.prototype.processSphereLH = function(/** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2),
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2),

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// whether state 6 found
			/** @type {boolean} */ state6Found = false,

			// state 6 in overlay state map
			/** @type {number} */ state6Value = 129,

			// counters
			/** @type {number} */ i = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ state = 0;

		// check which axis to join
		if (this.boundedGridSphereAxisTopLeft) {
			// copy adjacent edges
			for (i = 0; i < width; i += 1) {
				y = bottomY + i;
				x = leftX + i;

				// copy left column to below bottom row
				state = grid[y][leftX];
				if (state === state6Value) {
					state6Found = true;

					grid[bottomY - 1][x] = state;

					// set tile grid
					colourTileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						colourTileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy right column to above top row
				state = grid[y][rightX];
				if (state === state6Value) {
					state6Found = true;

					grid[topY + 1][x] = state;

					// set tile grid
					colourTileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						colourTileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy bottom row to left of left column
				state = grid[bottomY][x];
				if (state === state6Value) {
					state6Found = true;

					grid[y][leftX - 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}

				// copy top row to right of right column
				state = grid[topY][x];
				if (state === state6Value) {
					state6Found = true;

					grid[y][rightX + 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						colourTileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}
			}

			// top left corner
			state = grid[topY][leftX];
			if (state === state6Value) {
				state6Found = true;
				grid[topY + 1][leftX - 1] = state;
			}

			// top right corner
			state = grid[topY][rightX];
			if (state === state6Value) {
				state6Found = true;
				grid[topY + 1][rightX + 1] = state;
			}

			// bottom left corner
			state = grid[bottomY][leftX];
			if (state === state6Value) {
				state6Found = true;
				grid[bottomY - 1][leftX - 1] = state;
			}

			// bottom right corner
			state = grid[bottomY][rightX];
			if (state === state6Value) {
				state6Found = true;
				grid[bottomY - 1][rightX + 1] = state;
			}
		} else {
			// copy adjacent edges
			for (i = 0; i < width; i += 1) {
				y = bottomY + i;
				x = leftX + (width - i - 1);

				// copy right column to below bottom row
				state = grid[y][rightX];
				if (state === state6Value) {
					state6Found = true;

					grid[bottomY - 1][x] = state;

					// set tile grid
					colourTileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						colourTileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy left column to above top row
				state = grid[y][leftX];
				if (state === state6Value) {
					state6Found = true;

					grid[topY + 1][x] = state;

					// set tile grid
					colourTileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						colourTileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy bottom row to right of right column
				state = grid[bottomY][x];
				if (state === state6Value) {
					state6Found = true;

					grid[y][rightX + 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						colourTileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// copy top row to left of left column
				state = grid[topY][x];
				if (state === state6Value) {
					state6Found = true;

					grid[y][leftX - 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX + 15) & 15) === 15) {
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}
			}

			// top left corner
			state = grid[topY][leftX];
			if (state === state6Value) {
				state6Found = true;
				grid[topY + 1][leftX - 1] = state;
			}

			// top right corner
			state = grid[topY][rightX];
			if (state === state6Value) {
				state6Found = true;
				grid[topY + 1][rightX + 1] = state;
			}

			// bottom left corner
			state = grid[bottomY][leftX];
			if (state === state6Value) {
				state6Found = true;
				grid[bottomY - 1][leftX - 1] = state;
			}

			// bottom right corner
			state = grid[bottomY][rightX];
			if (state === state6Value) {
				state6Found = true;
				grid[bottomY - 1][rightX + 1] = state;
			}
		}

		// check if any state 6 cells were found
		if (state6Found) {
			this.populateState6MaskFromColGrid();
		}
	};

	// process torus for multi-state
	Life.prototype.processTorusMS = function(/** @type {Array<Uint8Array>} */ grid) {
		// bounded grid width and height
		var	/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// horizontal and vertical shifts
			/** @type {number} */ horizShift = this.boundedGridHorizontalShift,
			/** @type {number} */ vertShift = this.boundedGridVerticalShift,

			// colour tile grid
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileHistoryGrid,

			// counters
			/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ i = 0;

		// check for infinite width
		if (this.boundedGridWidth === 0) {
			// process whole width
			leftX = 0;
			rightX = this.width - 1;
			width = this.width;
		}

		// check for infinite height
		if (this.boundedGridHeight === 0) {
			// process whole height
			bottomY = 0;
			topY = this.height - 1;
			height = this.height;
		}

		// check for infinite height
		if (this.boundedGridHeight !== 0) {
			// perform vertical wrap
			for (i = 0; i < width; i += 1) {
				// copy top row to below bottom
				sourceX = leftX + i;
				destX = leftX + ((((i + horizShift) % width) + width) % width);

				// check if cell set
				state = grid[topY][sourceX];
				if (state !== 0) {
					// copy cell to below bottom row
					grid[bottomY - 1][destX] = state;

					// set tile grid
					colourTileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						colourTileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					}
				}

				// copy bottom row to above top
				destX = leftX + ((((i - horizShift) % width) + width) % width);

				// check if cell set
				state = grid[bottomY][sourceX];
				if (state !== 0) {
					// copy cell to above top row
					grid[topY + 1][destX] = state;

					// set tile grid
					colourTileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						colourTileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					}
				}
			}
		}

		// check for infinite width
		if (this.boundedGridWidth !== 0) {
			// perform horizontal wrap
			for (i = 0; i < height; i += 1) {
				// copy left column to right of right
				sourceY = bottomY + i;
				destY = bottomY + ((((i - vertShift) % height) + height) % height);

				// check if cell set
				state = grid[sourceY][leftX];
				if (state !== 0) {
					// copy cell to right of right edge
					grid[destY][rightX + 1] = state;

					// set tile grid
					colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// copy right column to left of left
				destY = bottomY + ((((i + vertShift) % height) + height) % height);

				// check if cell set
				state = grid[sourceY][rightX];
				if (state !== 0) {
					// copy cell to left of left edge
					grid[destY][leftX - 1] = state;

					// set tile grid
					colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}
			}
		}

		// only process corners if both dimensions are not infinite
		if (this.boundedGridWidth !== 0 && this.boundedGridHeight !== 0) {
			// bottom right corner
			sourceX = leftX + ((-horizShift + width) % width);
			sourceY = bottomY + ((height - 1 + vertShift + height) % height);
			state = grid[sourceY][sourceX];
			grid[bottomY - 1][rightX + 1] = state;

			// bottom left corner
			sourceX = leftX + ((width - 1 - horizShift + width) % width);
			sourceY = bottomY + ((height - 1 - vertShift + height) % height);
			state = grid[sourceY][sourceX];
			grid[bottomY - 1][leftX - 1] = state;

			// top right corner
			sourceX = leftX + ((horizShift + width) % width);
			sourceY = bottomY + ((vertShift + height) % height);
			state = grid[sourceY][sourceX];
			grid[topY + 1][rightX + 1] = state;

			// top left corner
			sourceX = leftX + ((width - 1 + horizShift + width) % width);
			sourceY = bottomY + ((-vertShift + height) % height);
			state = grid[sourceY][sourceX];
			grid[topY + 1][leftX - 1] = state;
		}
	};

	// process klein bottle for multi-state
	Life.prototype.processKleinMS = function(/** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileHistoryGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// horizontal and vertical shifts
			/** @type {number} */ horizShift = this.boundedGridHorizontalShift,
			/** @type {number} */ vertShift = this.boundedGridVerticalShift,

			// horizontal and vertical twists
			/** @type {boolean} */ horizTwist = this.boundedGridHorizontalTwist,
			/** @type {boolean} */ vertTwist = this.boundedGridVerticalTwist,

			// counters
			/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ i = 0;

		// perform vertical wrap
		for (i = 0; i < width; i += 1) {
			// copy top row to below bottom
			sourceX = leftX + i;

			// check for twist
			if (horizTwist) {
				destX = rightX - ((i + horizShift + width) % width);
			} else {
				destX = leftX + ((i + horizShift + width) % width);
			}

			state = grid[topY][sourceX];
			if (state !== 0) {
				// copy cell to below bottom
				grid[bottomY - 1][destX] = state;

				// set tile grid
				colourTileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if (((bottomY - 1) & 15) === 15) {
					colourTileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// copy bottom row to above top
			if (horizTwist) {
				destX = rightX - ((i + horizShift + width) % width);
			} else {
				destX = leftX + ((i + horizShift + width) % width);
			}

			state = grid[bottomY][sourceX];
			if (state !== 0) {
				// copy cell to above top
				grid[topY + 1][destX] = state;

				// set tile grid
				colourTileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if (((topY + 1) & 15) === 0) {
					colourTileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}
		}

		// perform horizontal wrap
		for (i = 0; i < height; i += 1) {
			// copy left column to right of right
			sourceY = bottomY + i;

			// check for vertical twist
			if (vertTwist) {
				destY = topY - ((i + vertShift + height) % height);
			} else {
				destY = bottomY + ((i + vertShift + height) % height);
			}

			state = grid[sourceY][leftX];
			if (state !== 0) {
				// copy cell to right of right edge
				grid[destY][rightX + 1] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

				// check for tile boundary
				if (((rightX + 1) & 15) === 0) {
					colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
				}
			}

			// copy right column to left of left
			if (vertTwist) {
				destY = topY - ((i + vertShift + height) % height);
			} else {
				destY = bottomY + ((i + vertShift + height) % height);
			}

			state = grid[sourceY][rightX];
			if (state !== 0) {
				// copy cell to left of left edge
				grid[destY][leftX - 1] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

				// check for tile boundary
				if (((leftX - 1) & 15) === 15) {
					colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
				}
			}
		}

		// only process corners if both dimensions are not infinite
		if (this.boundedGridWidth !== 0 && this.boundedGridHeight !== 0) {
			this.processKleinCornersMS(horizTwist, leftX, bottomY, rightX, topY, horizShift, vertShift, grid, colourTileGrid);
		}
	};

	// process cross-surface for multi-state
	Life.prototype.processCrossSurfaceMS = function(/** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// counters
			/** @type {number} */ i = 0,
			/** @type {number} */ source = 0,
			/** @type {number} */ dest = 0,
			/** @type {number} */ state = 0;

		// perform vertical cross surface
		for (i = 0; i < width; i += 1) {
			source = leftX + i;
			dest = rightX - i;

			// copy top row to below bottom inverse order
			state = grid[topY][source];
			if (state !== 0) {
				grid[bottomY - 1][dest] = state;

				// set tile grid
				colourTileGrid[(bottomY - 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));

				// check for tile boundary
				if (((bottomY - 1) & 15) === 15) {
					colourTileGrid[((bottomY - 1) >> 4) + 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				}
			}

			// copy bottom row to above top inverse order
			state = grid[bottomY][source];
			if (state !== 0) {
				grid[topY + 1][dest] = state;

				// set tile grid
				colourTileGrid[(topY + 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));

				// check for tile boundary
				if (((topY + 1) & 15) === 0) {
					colourTileGrid[((topY + 1) >> 4) - 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				}
			}
		}

		// perform horizontal cross surface
		for (i = 0; i <= height; i += 1) {
			source = bottomY + i;
			dest = topY - i;

			// copy left column to right of right inverse order
			state = grid[source][leftX];
			if (state !== 0) {
				grid[dest][rightX + 1] = state;

				// set tile grid
				colourTileGrid[dest >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

				// check for tile boundary
				if (((rightX + 1) & 15) === 0) {
					colourTileGrid[dest >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
				}
			}

			// copy right column to left of left
			state = grid[source][rightX];
			if (state !== 0) {
				grid[dest][leftX - 1] = state;

				// set tile grid
				colourTileGrid[dest >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

				// check for tile boundary
				if (((leftX - 1) & 15) === 15) {
					colourTileGrid[dest >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
				}
			}
		}

		// top left corner
		state = grid[topY][leftX];
		grid[topY + 1][leftX - 1] = state;

		// top right corner
		state = grid[topY][rightX];
		grid[topY + 1][rightX + 1] = state;

		// bottom left corner
		state = grid[bottomY][leftX];
		grid[bottomY - 1][leftX - 1] = state;

		// bottom right corner
		state = grid[bottomY][rightX];
		grid[bottomY - 1][rightX + 1] = state;
	};

	// process sphere for multi-state
	Life.prototype.processSphereMS = function(/** @type {Array<Uint8Array>} */ grid) {
		var	/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// number of history states
			/** @type {number} */ historyStates = this.historyStates,

			// counters
			/** @type {number} */ i = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ state = 0;

		// check which axis to join
		if (this.boundedGridSphereAxisTopLeft) {
			// copy adjacent edges
			for (i = 0; i < width; i += 1) {
				y = bottomY + i;
				x = leftX + i;

				// copy left column to below bottom row
				state = grid[y][leftX];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state >> 1) & 15) | ((state & 1) << 3)) + historyStates;
					}
					grid[bottomY - 1][x] = state;

					// set tile grid
					colourTileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						colourTileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy right column to above top row
				state = grid[y][rightX];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state >> 1) & 15) | ((state & 1) << 3)) + historyStates;
					}
					grid[topY + 1][x] = state;

					// set tile grid
					colourTileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						colourTileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy bottom row to left of left column
				state = grid[bottomY][x];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state << 1) & 15) | ((state & 8) >> 3)) + historyStates;
					}
					grid[y][leftX - 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}

				// copy top row to right of right column
				state = grid[topY][x];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state << 1) & 15) | ((state & 8) >> 3)) + historyStates;
					}
					grid[y][rightX + 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						colourTileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}
			}

			// top left corner
			state = grid[topY][leftX];
			grid[topY + 1][leftX - 1] = state;

			// top right corner
			state = grid[topY][rightX];
			grid[topY + 1][rightX + 1] = state;

			// bottom left corner
			state = grid[bottomY][leftX];
			grid[bottomY - 1][leftX - 1] = state;

			// bottom right corner
			state = grid[bottomY][rightX];
			grid[bottomY - 1][rightX + 1] = state;
		} else {
			// copy adjacent edges
			for (i = 0; i < width; i += 1) {
				y = bottomY + i;
				x = leftX + (width - i - 1);

				// copy right column to below bottom row
				state = grid[y][rightX];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state << 1) & 15) | ((state & 8) >> 3)) + historyStates;
					}
					grid[bottomY - 1][x] = state;

					// set tile grid
					colourTileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						colourTileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy left column to above top row
				state = grid[y][leftX];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state << 1) & 15) | ((state & 8) >> 3)) + historyStates;
					}
					grid[topY + 1][x] = state;

					// set tile grid
					colourTileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						colourTileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy bottom row to right of right column
				state = grid[bottomY][x];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state >> 1) & 15) | ((state & 1) << 3)) + historyStates;
					}
					grid[y][rightX + 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						colourTileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// copy top row to left of left column
				state = grid[topY][x];
				if (state > historyStates) {
					if (this.isPCA) {
						state -= historyStates;
						state = (((state >> 1) & 15) | ((state & 1) << 3)) + historyStates;
					}
					grid[y][leftX - 1] = state;

					// set tile grid
					colourTileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX + 15) & 15) === 15) {
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}
			}

			// top left corner
			state = grid[topY][leftX];
			grid[topY + 1][leftX - 1] = state;

			// top right corner
			state = grid[topY][rightX];
			grid[topY + 1][rightX + 1] = state;

			// bottom left corner
			state = grid[bottomY][leftX];
			grid[bottomY - 1][leftX - 1] = state;

			// bottom right corner
			state = grid[bottomY][rightX];
			grid[bottomY - 1][rightX + 1] = state;
		}
	};

	// process torus
	Life.prototype.processTorus = function() {
		// life grid
		var	/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// horizontal and vertical shifts
			/** @type {number} */ horizShift = this.boundedGridHorizontalShift,
			/** @type {number} */ vertShift = this.boundedGridVerticalShift,

			// flag if extra horizontal cell is needed for triangular grid
			/** @type {boolean} */ needExtra = (this.isTriangular && leftX > 0 && rightX < this.width - 1),

			// counters
			/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ i = 0;

		// determine the buffer for current generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// check for infinite width
		if (this.boundedGridWidth === 0) {
			// process whole width
			leftX = 0;
			rightX = this.width - 1;
			width = this.width;
		}

		// check for infinite height
		if (this.boundedGridHeight === 0) {
			// process whole height
			bottomY = 0;
			topY = this.height - 1;
			height = this.height;
		}

		// check for infinite height
		if (this.boundedGridHeight !== 0) {
			// perform vertical wrap
			for (i = 0; i < width; i += 1) {
				// copy top row to below bottom
				sourceX = leftX + i;
				destX = leftX + ((((i + horizShift) % width) + width) % width);

				// check if cell set
				if ((grid[topY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					// copy cell to below bottom row
					grid[bottomY - 1][destX >> 4] |= (1 << (~destX & 15));

					// set tile grid
					tileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					colourTileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						tileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
						colourTileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					}
				}

				// copy bottom row to above top
				destX = leftX + ((((i - horizShift) % width) + width) % width);

				// check if cell set
				if ((grid[bottomY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					// copy cell to above top row
					grid[topY + 1][destX >> 4] |= (1 << (~destX & 15));

					// set tile grid
					tileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					colourTileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						tileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
						colourTileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					}
				}
			}
		}

		// check for infinite width
		if (this.boundedGridWidth !== 0) {
			// perform horizontal wrap
			for (i = 0; i < height; i += 1) {
				// copy left column to right of right
				sourceY = bottomY + i;
				destY = bottomY + ((((i - vertShift) % height) + height) % height);

				// check if cell set
				if ((grid[sourceY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					// copy cell to right of right edge
					grid[destY][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

					// set tile grid
					tileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
					colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						tileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
						colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// for triangular grids we have to copy two cells
				if (needExtra) {
					leftX += 1;
					rightX += 1;

					// check if cell set
					if ((grid[sourceY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
						// copy cell to right of right edge
						grid[destY][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

						// set tile grid
						tileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
						colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

						// check for tile boundary
						if (((rightX + 1) & 15) === 0) {
							tileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
							colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
						}
					}

					// restore columns
					leftX -= 1;
					rightX -= 1;
				}

				// copy right column to left of left
				destY = bottomY + ((((i + vertShift) % height) + height) % height);

				// check if cell set
				if ((grid[sourceY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					// copy cell to left of left edge
					grid[destY][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

					// set tile grid
					tileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
					colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						tileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}

				// for triangular grids we have to copy two cells
				if (needExtra) {
					leftX -= 1;
					rightX -= 1;

					// check if cell set
					if ((grid[sourceY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
						// copy cell to left of left edge
						grid[destY][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

						// set tile grid
						tileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
						colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

						// check for tile boundary
						if (((leftX - 1) & 15) === 15) {
							tileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
							colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						}
					}

					// restore columns
					leftX += 1;
					rightX += 1;
				}
			}
		}

		// only process corners if both dimensions are not infinite
		if (this.boundedGridWidth !== 0 && this.boundedGridHeight !== 0) {
			// bottom right corner
			sourceX = leftX + ((-horizShift + width) % width);
			sourceY = bottomY + ((height - 1 + vertShift + height) % height);
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			} else {
				grid[bottomY - 1][(rightX + 1) >> 4] &= ~(1 << (~(rightX + 1) & 15));
			}

			// bottom left corner
			sourceX = leftX + ((width - 1 - horizShift + width) % width);
			sourceY = bottomY + ((height - 1 - vertShift + height) % height);
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			} else {
				grid[bottomY - 1][(leftX - 1) >> 4] &= ~(1 << (~(leftX - 1) & 15));
			}

			// top right corner
			sourceX = leftX + ((horizShift + width) % width);
			sourceY = bottomY + ((vertShift + height) % height);
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			} else {
				grid[topY + 1][(rightX + 1) >> 4] &= ~(1 << (~(rightX + 1) & 15));
			}

			// top left corner
			sourceX = leftX + ((width - 1 + horizShift + width) % width);
			sourceY = bottomY + ((-vertShift + height) % height);
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			} else {
				grid[topY + 1][(leftX - 1) >> 4] &= ~(1 << (~(leftX - 1) & 15));
			}

			if (needExtra) {
				leftX += 1;
				rightX += 1;

				// bottom right corner
				sourceX = leftX + ((-horizShift + width) % width);
				sourceY = bottomY + ((height - 1 + vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				} else {
					grid[bottomY - 1][(rightX + 1) >> 4] &= ~(1 << (~(rightX + 1) & 15));
				}

				// bottom left corner
				sourceX = leftX + ((width - 1 - horizShift + width) % width);
				sourceY = bottomY + ((height - 1 - vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				} else {
					grid[bottomY - 1][(leftX - 1) >> 4] &= ~(1 << (~(leftX - 1) & 15));
				}

				// top right corner
				sourceX = leftX + ((horizShift + width) % width);
				sourceY = bottomY + ((vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				} else {
					grid[topY + 1][(rightX + 1) >> 4] &= ~(1 << (~(rightX + 1) & 15));
				}

				// top left corner
				sourceX = leftX + ((width - 1 + horizShift + width) % width);
				sourceY = bottomY + ((-vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				} else {
					grid[topY + 1][(leftX - 1) >> 4] &= ~(1 << (~(leftX - 1) & 15));
				}

				leftX -= 2;
				rightX -= 2;

				// bottom right corner
				sourceX = leftX + ((-horizShift + width) % width);
				sourceY = bottomY + ((height - 1 + vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				} else {
					grid[bottomY - 1][(rightX + 1) >> 4] &= ~(1 << (~(rightX + 1) & 15));
				}

				// bottom left corner
				sourceX = leftX + ((width - 1 - horizShift + width) % width);
				sourceY = bottomY + ((height - 1 - vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				} else {
					grid[bottomY - 1][(leftX - 1) >> 4] &= ~(1 << (~(leftX - 1) & 15));
				}

				// top right corner
				sourceX = leftX + ((horizShift + width) % width);
				sourceY = bottomY + ((vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				} else {
					grid[topY + 1][(rightX + 1) >> 4] &= ~(1 << (~(rightX + 1) & 15));
				}

				// top left corner
				sourceX = leftX + ((width - 1 + horizShift + width) % width);
				sourceY = bottomY + ((-vertShift + height) % height);
				if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
					grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				} else {
					grid[topY + 1][(leftX - 1) >> 4] &= ~(1 << (~(leftX - 1) & 15));
				}

				leftX += 1;
				rightX += 1;
			}
		}
	};

	// process klein corners for [R]History
	/** @returns {boolean} */
	Life.prototype.processKleinCornersLH = function(/** @type {boolean} */ horizTwist, /** @type {number} */ leftX, /* @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY,
									/** @type {number} */ horizShift, /** @type {number} */ vertShift, /** @type {Array<Uint8Array>} */ grid, /** @type {Array<Uint16Array>} */ colourTileGrid) {
		var	/** @type {boolean} */ result = false,
			/** @type {number} */ state = 0,
			/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ state6Value = 129;

		if (horizTwist) {
			// bottom right corner
			sourceX = rightX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = topY;
			destX = rightX + 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// bottom left corner
			sourceX = leftX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = topY;
			destX = leftX - 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// top right corner
			sourceX = rightX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = bottomY;
			destX = rightX + 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// top left corner
			sourceX = leftX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = bottomY;
			destX = leftX - 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}
		} else {
			// bottom right corner
			sourceX = leftX;
			sourceY = bottomY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = rightX + 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// bottom left corner
			sourceX = rightX;
			sourceY = bottomY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = leftX - 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// top right corner
			sourceX = leftX;
			sourceY = topY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = rightX + 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// top left corner
			sourceX = rightX;
			sourceY = topY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = leftX - 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state === state6Value) {
				result = true;

				// copy cell to below bottom
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if ((destY & 15) === 15) {
					colourTileGrid[(destY >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}
		}

		// return whether state 6 cells were moved
		return result;
	};

	// process klein corners multi-state
	Life.prototype.processKleinCornersMS = function(/** @type {boolean} */ horizTwist, /** @type {number} */ leftX, /* @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY,
									/** @type {number} */ horizShift, /** @type {number} */ vertShift, /** @type {Array<Uint8Array>} */ grid, /** @type {Array<Uint16Array>} */ colourTileGrid) {
		var	/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ state = 0;

		if (horizTwist) {
			// bottom right corner
			sourceX = rightX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = topY;
			destX = rightX + 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}

			// bottom left corner
			sourceX = leftX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = topY;
			destX = leftX - 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}

			// top right corner
			sourceX = rightX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = bottomY;
			destX = rightX + 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}

			// top left corner
			sourceX = leftX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = bottomY;
			destX = leftX - 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}
		} else {
			// bottom right corner
			sourceX = leftX;
			sourceY = bottomY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = rightX + 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}

			// bottom left corner
			sourceX = rightX;
			sourceY = bottomY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = leftX - 1;
			destY = bottomY - 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}

			// top right corner
			sourceX = leftX;
			sourceY = topY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = rightX + 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}

			// top left corner
			sourceX = rightX;
			sourceY = topY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			destX = leftX - 1;
			destY = topY + 1;

			state = grid[sourceY][sourceX];
			if (state !== 0) {
				grid[destY][destX] = state;

				// set tile grid
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
			}
		}
	};

	// process klein corners
	Life.prototype.processKleinCorners = function(/** @type {boolean} */ horizTwist, /** @type {number} */ leftX, /* @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY,
									/** @type {number} */ horizShift, /** @type {number} */ vertShift, /** @type {Array<Uint16Array>} */ grid) {
		var	/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0;

		if (horizTwist) {
			// bottom right corner
			sourceX = rightX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = topY;
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			// bottom left corner
			sourceX = leftX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = topY;
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// top right corner
			sourceX = rightX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = bottomY;
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			// top left corner
			sourceX = leftX - horizShift;
			if (sourceX < leftX) {
				sourceX = rightX;
			}
			sourceY = bottomY;
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}
		} else {
			// bottom right corner
			sourceX = leftX;
			sourceY = bottomY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			// bottom left corner
			sourceX = rightX;
			sourceY = bottomY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// top right corner
			sourceX = leftX;
			sourceY = topY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			// top left corner
			sourceX = rightX;
			sourceY = topY - vertShift;
			if (sourceY < bottomY) {
				sourceY = topY;
			}
			if ((grid[sourceY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}
		}
	};

	// process klein bottle
	Life.prototype.processKlein = function() {
		// life grid
		var	/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// horizontal and vertical shifts
			/** @type {number} */ horizShift = this.boundedGridHorizontalShift,
			/** @type {number} */ vertShift = this.boundedGridVerticalShift,

			// horizontal and vertical twists
			/** @type {boolean} */ horizTwist = this.boundedGridHorizontalTwist,
			/** @type {boolean} */ vertTwist = this.boundedGridVerticalTwist,

			// flag if extra horizontal cell is needed for triangular grid
			/** @type {boolean} */ needExtra = (this.isTriangular && leftX > 0 && rightX < this.width - 1),

			// counters
			/** @type {number} */ sourceX = 0,
			/** @type {number} */ sourceY = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ destY = 0,
			/** @type {number} */ i = 0;

		// determine the buffer for current generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// perform vertical wrap
		for (i = 0; i < width; i += 1) {
			// copy top row to below bottom
			destX = leftX + i;

			// check for twist
			if (horizTwist) {
				sourceX = rightX - ((i + horizShift) % width);
			} else {
				sourceX = destX;
			}

			if ((grid[topY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				// copy cell to below bottom
				grid[bottomY - 1][destX >> 4] |= (1 << (~destX & 15));

				// set tile grid
				tileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				colourTileGrid[(bottomY - 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if (((bottomY - 1) & 15) === 15) {
					tileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					colourTileGrid[((bottomY - 1) >> 4) + 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}

			// copy bottom row to above top
			if ((grid[bottomY][sourceX >> 4] & (1 << (~sourceX & 15))) !== 0) {
				// copy cell to above top
				grid[topY + 1][destX >> 4] |= (1 << (~destX & 15));

				// set tile grid
				tileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				colourTileGrid[(topY + 1) >> 4][destX >> 8] |= (1 << (~(destX >> 4) & 15));

				// check for tile boundary
				if (((topY + 1) & 15) === 0) {
					tileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
					colourTileGrid[((topY + 1) >> 4) - 1][destX >> 8] |= (1 << (~(destX >> 4) & 15));
				}
			}
		}

		// perform horizontal wrap
		for (i = 0; i < height; i += 1) {
			// copy left column to right of right
			destY = bottomY + i;

			// check for vertical twist
			if (vertTwist) {
				sourceY = topY - ((i + vertShift) % height);
			} else {
				sourceY = destY;
			}

			// check if cell set
			if ((grid[sourceY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				// copy cell to right of right edge
				grid[destY][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

				// set tile grid
				tileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
				colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

				// check for tile boundary
				if (((rightX + 1) & 15) === 0) {
					tileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
				}
			}

			// for triangular grids we have to copy two cells
			if (needExtra) {
				leftX += 1;
				rightX += 1;

				// check if cell set
				if ((grid[sourceY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					// copy cell to right of right edge
					grid[destY][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

					// set tile grid
					tileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
					colourTileGrid[destY >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						tileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
						colourTileGrid[destY >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// restore columns
				leftX -= 1;
				rightX -= 1;
			}

			// copy right column to left of left
			// check if cell set
			if ((grid[sourceY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				// copy cell to left of left edge
				grid[destY][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

				// set tile grid
				tileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
				colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

				// check for tile boundary
				if (((leftX - 1) & 15) === 15) {
					tileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
				}
			}

			// for triangular grids we have to copy two cells
			if (needExtra) {
				leftX -= 1;
				rightX -= 1;

				// check if cell set
				if ((grid[sourceY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					// copy cell to left of left edge
					grid[destY][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

					// set tile grid
					tileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
					colourTileGrid[destY >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						tileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						colourTileGrid[destY >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}

				// restore columns
				leftX += 1;
				rightX += 1;
			}
		}

		// only process corners if both dimensions are not infinite
		if (this.boundedGridWidth !== 0 && this.boundedGridHeight !== 0) {
			this.processKleinCorners(horizTwist, leftX, bottomY, rightX, topY, horizShift, vertShift, grid);

			if (needExtra) {
				leftX += 1;
				rightX += 1;
				this.processKleinCorners(horizTwist, leftX, bottomY, rightX, topY, horizShift, vertShift, grid);

				leftX -= 2;
				rightX -= 2;
				this.processKleinCorners(horizTwist, leftX, bottomY, rightX, topY, horizShift, vertShift, grid);

				leftX += 1;
				rightX += 1;
			}
		}
	};

	// process cross-surface
	Life.prototype.processCrossSurface = function() {
		// life grid
		var	/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// flag if extra horizontal cell is needed for triangular grid
			/** @type {boolean} */ needExtra = (this.isTriangular && leftX > 0 && rightX < this.width - 1),

			// counters
			/** @type {number} */ i = 0,
			/** @type {number} */ source = 0,
			/** @type {number} */ dest = 0;

		// determine the buffer for current generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// perform vertical cross surface
		for (i = 0; i < width; i += 1) {
			source = leftX + i;
			dest = rightX - i;

			// copy top row to below bottom inverse order
			if ((grid[topY][source >> 4] & (1 << (~source & 15))) !== 0) {
				grid[bottomY - 1][dest >> 4] |= (1 << (~dest & 15));

				// set tile grid
				tileGrid[(bottomY - 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				colourTileGrid[(bottomY - 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));

				// check for tile boundary
				if (((bottomY - 1) & 15) === 15) {
					tileGrid[((bottomY - 1) >> 4) + 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
					colourTileGrid[((bottomY - 1) >> 4) + 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				}
			}

			// copy bottom row to above top inverse order
			if ((grid[bottomY][source >> 4] & (1 << (~source & 15))) !== 0) {
				grid[topY + 1][dest >> 4] |= (1 << (~dest & 15));

				// set tile grid
				tileGrid[(topY + 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				colourTileGrid[(topY + 1) >> 4][dest >> 8] |= (1 << (~(dest >> 4) & 15));

				// check for tile boundary
				if (((topY + 1) & 15) === 0) {
					tileGrid[((topY + 1) >> 4) - 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
					colourTileGrid[((topY + 1) >> 4) - 1][dest >> 8] |= (1 << (~(dest >> 4) & 15));
				}
			}
		}

		// perform horizontal cross surface
		for (i = 0; i < height; i += 1) {
			source = bottomY + i;
			dest = topY - i;

			// copy left column to right of right inverse order
			if ((grid[source][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				grid[dest][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

				// set tile grid
				tileGrid[dest >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
				colourTileGrid[dest >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

				// check for tile boundary
				if (((rightX + 1) & 15) === 0) {
					tileGrid[dest >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					colourTileGrid[dest >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
				}
			}

			// for triangular grids we have to copy two cells
			if (needExtra) {
				leftX += 1;
				rightX += 1;

				// copy left column to right of right inverse order
				if ((grid[source][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					grid[dest][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

					// set tile grid
					tileGrid[dest >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
					colourTileGrid[dest >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						tileGrid[dest >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
						colourTileGrid[dest >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// restore columns
				leftX -= 1;
				rightX -= 1;
			}

			// copy right column to left of left
			if ((grid[source][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				grid[dest][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

				// set tile grid
				tileGrid[dest >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
				colourTileGrid[dest >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

				// check for tile boundary
				if (((leftX - 1) & 15) === 15) {
					tileGrid[dest >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					colourTileGrid[dest >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
				}
			}

			// for triangular grids we have to copy two cells
			if (needExtra) {
				leftX -= 1;
				rightX -= 1;

				// copy right column to left of left
				if ((grid[source][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					grid[dest][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

					// set tile grid
					tileGrid[dest >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
					colourTileGrid[dest >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						tileGrid[dest >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						colourTileGrid[dest >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}

				// restore columns
				leftX += 1;
				rightX += 1;
			}
		}

		// only process corners if both dimensions are not infinite
		if (this.boundedGridWidth !== 0 && this.boundedGridHeight !== 0) {
			// top left corner
			if ((grid[topY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// top right corner
			if ((grid[topY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			// bottom left corner
			if ((grid[bottomY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// bottom right corner
			if ((grid[bottomY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			if (needExtra) {
				leftX += 1;
				rightX += 1;

				// top left corner
				if ((grid[topY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				}

				// top right corner
				if ((grid[topY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				}

				// bottom left corner
				if ((grid[bottomY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				}

				// bottom right corner
				if ((grid[bottomY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				}

				leftX -=2;
				rightX -= 2;

				// top left corner
				if ((grid[topY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				}

				// top right corner
				if ((grid[topY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				}

				// bottom left corner
				if ((grid[bottomY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
				}

				// bottom right corner
				if ((grid[bottomY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
				}

				leftX += 1;
				rightX += 1;
			}
		}
	};

	// process sphere
	Life.prototype.processSphere = function() {
		// life grid
		var	/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// bottom left
			/** @type {number} */ leftX = Math.round((this.width - width) / 2) + boxOffset,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2) + boxOffset,

			// top right
			/** @type {number} */ rightX = leftX + width - 1,
			/** @type {number} */ topY = bottomY + height - 1,

			// counters
			/** @type {number} */ i = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0;

		// determine the buffer for current generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// check which axis to join
		if (this.boundedGridSphereAxisTopLeft) {
			// copy adjacent edges
			for (i = 0; i < width; i += 1) {
				y = bottomY + i;
				x = leftX + i;

				// copy left column to below bottom row
				if ((grid[y][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					grid[bottomY - 1][x >> 4] |= (1 << (~x & 15));

					// set tile grid
					tileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					colourTileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						tileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
						colourTileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy right column to above top row
				if ((grid[y][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					grid[topY + 1][x >> 4] |= (1 << (~x & 15));

					// set tile grid
					tileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					colourTileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						tileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
						colourTileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy bottom row to left of left column
				if ((grid[bottomY][x >> 4] & (1 << (~x & 15))) !== 0) {
					grid[y][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

					// set tile grid
					tileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
					colourTileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
					// check for tile boundary
					if (((leftX - 1) & 15) === 15) {
						tileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}

				// copy top row to right of right column
				if ((grid[topY][x >> 4] & (1 << (~x & 15))) !== 0) {
					grid[y][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

					// set tile grid
					tileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
					colourTileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));

					// check for tile boundary
					if (((rightX + 1) & 15) === 0) {
						tileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
						colourTileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}
			}

			// top left corner
			if ((grid[topY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// top right corner
			if ((grid[topY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			// bottom left corner
			if ((grid[bottomY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// bottom right corner
			if ((grid[bottomY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}
		} else {
			// copy adjacent edges
			for (i = 0; i < width; i += 1) {
				y = bottomY + i;
				x = leftX + (width - i - 1);

				// copy right column to below bottom row
				if ((grid[y][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
					grid[bottomY - 1][x >> 4] |= (1 << (~x & 15));

					// set tile grid
					tileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					colourTileGrid[(bottomY - 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((bottomY - 1) & 15) === 15) {
						tileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
						colourTileGrid[((bottomY - 1) >> 4) + 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy left column to above top row
				if ((grid[y][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
					grid[topY + 1][x >> 4] |= (1 << (~x & 15));

					// set tile grid
					tileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));
					colourTileGrid[(topY + 1) >> 4][x >> 8] |= (1 << (~(x >> 4) & 15));

					// check for tile boundary
					if (((topY + 1) & 15) === 0) {
						tileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
						colourTileGrid[((topY + 1) >> 4) - 1][x >> 8] |= (1 << (~(x >> 4) & 15));
					}
				}

				// copy bottom row to right of right column
				if ((grid[bottomY][x >> 4] & (1 << (~x & 15))) !== 0) {
					grid[y][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));

					// set tile grid
					tileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
					colourTileGrid[y >> 4][(rightX + 1) >> 8] |= (1 << (~((rightX + 1) >> 4) & 15));
					// check for tile boundary
					if (((rightX + 1) & 15) === 15) {
						tileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
						colourTileGrid[y >> 4][(rightX - 15) >> 8] |= (1 << (~((rightX - 15) >> 4) & 15));
					}
				}

				// copy top row to left of left column
				if ((grid[topY][x >> 4] & (1 << (~x & 15))) !== 0) {
					grid[y][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));

					// set tile grid
					tileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));
					colourTileGrid[y >> 4][(leftX - 1) >> 8] |= (1 << (~((leftX - 1) >> 4) & 15));

					// check for tile boundary
					if (((leftX - 1) & 15) === 0) {
						tileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
						colourTileGrid[y >> 4][(leftX + 15) >> 8] |= (1 << (~((leftX + 15) >> 4) & 15));
					}
				}
			}

			// top left corner
			if ((grid[topY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				grid[topY + 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// top right corner
			if ((grid[topY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				grid[topY + 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}

			// bottom left corner
			if ((grid[bottomY][leftX >> 4] & (1 << (~leftX & 15))) !== 0) {
				grid[bottomY - 1][(leftX - 1) >> 4] |= (1 << (~(leftX - 1) & 15));
			}

			// bottom right corner
			if ((grid[bottomY][rightX >> 4] & (1 << (~rightX & 15))) !== 0) {
				grid[bottomY - 1][(rightX + 1) >> 4] |= (1 << (~(rightX + 1) & 15));
			}
		}
	};

	// post-process bounded grid
	Life.prototype.postProcessBoundedGrid = function() {
		var	/** @type {number} */ i = 1,
			/** @type {number} */ limit = 1;

		// determine grid type
		if (this.boundedGridType !== 0) {
			limit = 2;
		}

		// check for triangular grid
		if (this.isTriangular) {
			limit += 1;
		}

		// clear boundaries
		for (i = 1; i <= limit + 1; i += 1) {
			this.clearBoundary(i);
		}

		// check for [R]History
		if (this.isLifeHistory) {
			this.clearBoundaryOfState6();
		}

		// check for RuleTable B0
		if (this.ruleTableB0) {
			this.clearOutsideRuleTableB0();
		}
	};

	// pre-process bounded grid
	Life.prototype.preProcessBoundedGrid = function() {
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid;

		// alternate colour grids for PCA, RuleTree,  Super and Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}
		this.drawBoundedGridBorder(colourGrid, 0);

		// set tiles on boundary
		this.setBoundedTiles();

		// determine grid type
		switch (this.boundedGridType) {
		case 0:
			// plane so nothing to do
			break;
		case 1:
			// torus
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				this.processTorusMS(colourGrid);
				if (this.isSuper || this.isExtended) {
					this.processTorus();
				}
			} else {
				this.processTorus();
				if (this.isLifeHistory) {
					this.processTorusLH(overlayGrid);
				}
			}
			break;
		case 2:
			// klein bottle
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				this.processKleinMS(colourGrid);
				if (this.isSuper || this.isExtended) {
					this.processKlein();
				}
			} else {
				this.processKlein();
				if (this.isLifeHistory) {
					this.processKleinLH(overlayGrid);
				}
			}
			break;
		case 3:
			// cross-surface
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				this.processCrossSurfaceMS(colourGrid);
				if (this.isSuper || this.isExtended) {
					this.processCrossSurface();
				}
			} else {
				this.processCrossSurface();
				if (this.isLifeHistory) {
					this.processCrossSurfaceLH(overlayGrid);
				}
			}
			break;
		case 4:
			// sphere
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				this.processSphereMS(colourGrid);
				if (this.isSuper || this.isExtended) {
					this.processSphere();
				}
			} else {
				this.processSphere();
				if (this.isLifeHistory) {
					this.processSphereLH(overlayGrid);
				}
			}
			break;
		}

		// set tiles on boundary
		this.setBoundedTiles();
	};

	// save snapshot (called after nextGeneration() and convertToPensTile())
	Life.prototype.saveSnapshotIfNeeded = function(/** @type {View} */ view) {
		if (this.snapshotNeeded) {
			// update the next snapshot generation
			this.nextSnapshotTarget += LifeConstants.snapshotInterval;
			this.saveSnapshot(view);
			this.snapshotNeeded = false;
		}
	};

	// compute the next generation unless rule is none
	Life.prototype.nextGeneration = function(/** @type {boolean} */ noHistory) {
		// do nothing if rule is none
		if (!this.isNone) {
			this.processNextGen(noHistory);
		} else {
			this.counter += 1;
		}
	};

	// clear all but bottom line for Wolfram rules
	Life.prototype.clearAltWolfram = function() {
		var	/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ leftX = zoomBox.leftX >> 4,
			/** @type {number} */ rightX = zoomBox.rightX >> 4,
			/** @type {number} */ bottomY = zoomBox.bottomY,
			/** @type {number} */ x = 0,
			/** @type {number} */ cells = 0,
			/** @type {number} */ delta = 0,
			/** @type {Uint8Array} */ bitCounts = this.bitCounts16;

		if (this.counter & 1) {
			grid = this.grid16;
		} else {
			grid = this.nextGrid16;
		}

		// get the bottom row
		gridRow = grid[bottomY];

		// clear any alive cells and adjust population
		for (x = leftX; x <= rightX; x += 1) {
			cells = gridRow[x];
			if (cells) {
				delta += bitCounts[cells];
				gridRow[x] = 0;
			}
		}

		this.population -= delta;
		this.deaths += delta;
	};

	// compute the next generation with or without statistics
	Life.prototype.processNextGen = function(/** @type {boolean} */ noHistory) {
		var	/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox,
			/** @type {BoundingBox} */ identifyHistoryBox = this.identifyHistoryBox,
			/** @type {number} */ boundarySize = 16,
			/** @type {number} */ currentPop = 0,
			/** @type {number} */ timing = 0;

		// mark no cells killed by hitting boundary
		this.cellsCleared = 0;

		// check if snapshot should be saved
		if (this.counter === this.nextSnapshotTarget - 1 && !noHistory) {
			// save snapshot required
			this.snapshotNeeded = true;
		} else {
			this.snapshotNeeded = false;
		}

		// perform bounded grid pre-processing unless rule is HROT
		if (this.boundedGridType !== -1 && !this.isHROT) {
			this.preProcessBoundedGrid();
		}

		// check if state 6 is on
		if (this.state6Mask) {
			// pre-process for state 6
			this.state6Pre();
		}

		// check for RuleTable rule
		if (this.isRuleTree) {
			if (this.ruleTableOutput === null) {
				this.nextGenerationRuleTreeTile();
			} else {
				this.nextGenerationRuleTableTile();
			}
		} else {
			if (this.isPCA) {
				this.nextGenerationPCATile();
			} else {
				if (this.isHROT) {
					// compute HROT next generation
					this.HROT.nextGenerationHROT(this.counter);
				} else {
					// stats are required if they are on but not for multi-state rules which compute their own stats
					if (this.isMargolus) {
						this.nextGenerationMargolusTile();
					} else {
						if (this.isTriangular) {
							this.nextGenerationTriTile();
						} else {
							// process bit grid for [R]Standard, [R]History and [R]Super - but not for [R]Extended
							if (!this.isExtended) {
								this.nextGenerationTile();

								// for alternating wolfram rules need to clear all but the bottom line
								if (this.altSpecified && this.wolframRule !== -1) {
									this.clearAltWolfram();
								}
							}
						}
					}
				}
			}
		}

		// set the Identify bounding box for [R]History to the alive cells - it will be merged later with any state 6 bounding box
		if (this.isLifeHistory) {
			identifyHistoryBox.leftX = zoomBox.leftX;
			identifyHistoryBox.bottomY = zoomBox.bottomY;
			identifyHistoryBox.rightX = zoomBox.rightX;
			identifyHistoryBox.topY = zoomBox.topY;
		}

		// increment generation count
		this.counter += 1;

		// adjust PCA/Margolus generation based on playback direction
		if (this.isMargolus || this.isPCA) {
			if (this.reverseMargolus) {
				this.counterMargolus -= 1;
			} else {
				this.counterMargolus += 1;
			}
			// update maximum Margolus generation reached (used for PASTE EVERY)
			if (this.counterMargolus > this.maxMargolusGen) {
				this.maxMargolusGen = this.counterMargolus;
			}
		}

		// check for Generations
		if (this.multiNumStates !== -1 && !this.isHROT && !this.isPCA && !this.isRuleTree && !this.isSuper && !this.isExtended) {
			timing = performance.now();

			if (Controller.useWASM && Controller.wasmEnableNextGenerationGenerations && this.view.wasmEnabled) {
				if ((this.counter & 1) !== 0) {
					WASM.nextGenerationGenerations(
						this.colourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.colourTileGrid.whole.byteOffset | 0,
						this.tileY | 0, this.tileX | 0,
						this.tileRows | 0, this.tileCols | 0,
						this.nextGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.sharedBuffer.byteOffset | 0,
						this.historyStates | 0, (this.multiNumStates + this.historyStates - 1) | 0, (this.historyStates > 0 ? 1 : 0) | 0,
						this.width | 0, this.height | 0
					);
				} else {
					WASM.nextGenerationGenerations(
						this.colourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.colourTileGrid.whole.byteOffset | 0,
						this.tileY | 0, this.tileX | 0,
						this.tileRows | 0, this.tileCols | 0,
						this.grid.whole.byteOffset | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.sharedBuffer.byteOffset | 0,
						this.historyStates | 0, (this.multiNumStates + this.historyStates - 1) | 0, (this.historyStates > 0 ? 1 : 0) | 0,
						this.width | 0, this.height | 0
					);
				}

				this.population = this.sharedBuffer[0];
				this.births = this.sharedBuffer[1];
				this.deaths = this.sharedBuffer[2];
				this.zoomBox.leftX = this.sharedBuffer[3];
				this.zoomBox.bottomY = this.sharedBuffer[4];
				this.zoomBox.rightX = this.sharedBuffer[5];
				this.zoomBox.topY = this.sharedBuffer[6];
			} else {
				this.nextGenerationGenerations();
			}

			timing = performance.now() - timing;
			if (Controller.wasmTiming) {
				this.view.menuManager.updateTimingItem("nextGenerations", timing, Controller.useWASM && Controller.wasmEnableNextGenerationGenerations && this.view.wasmEnabled);
			}
		}

		// perform super processing
		if (this.isSuper) {
			this.nextGenerationSuperTile();
		}

		// perform extended processing
		if (this.isExtended) {
			this.nextGenerationExtendedTile();
		}

		// check if state 6 is on
		if (this.state6Mask) {
			// post-process for state 6
			this.state6Post();
		}

		// perform bounded grid post-processing
		if (this.boundedGridType !== -1 && !this.isHROT) {
			this.postProcessBoundedGrid();
		}

		// clear boundary if maximum grid size
		if (this.width === this.maxGridSize || this.height === this.maxGridSize) {
			// check for LtL or HROT
			if (this.isHROT) {
				boundarySize = this.HROT.xrange * 2;
			} else {
				if (this.isExtended || this.isRuleTree) {
					boundarySize = 1;
				} else {
					boundarySize = 16;
				}
			}

			// check if the pattern is near a boundary
			if (zoomBox.leftX < boundarySize || zoomBox.rightX >= (this.maxGridSize - boundarySize) || zoomBox.bottomY < boundarySize || zoomBox.topY >= (this.maxGridSize - boundarySize)) {
				// save current population so we can determine if cells were deleted
				currentPop = this.population;

				// clear grid boundary
				if (this.isHROT) {
					this.clearHRBoundary(this.HROT.xrange * 2 + 1, boundarySize + 1);
				} else {
					if (this.isRuleTree || this.isPCA) {
						this.clearRTBoundary();
					} else {
						if (this.isExtended) {
							this.clearExtendedBoundary();
						} else {
							// clear bit grid boundary (and Generations alive states too)
							this.clearGridBoundary();
						}
					}
				}

				// check if cells were deleted
				if (currentPop !== this.population) {
					this.cellsCleared = currentPop - this.population;
					this.lastClearedGen = this.counter;
					this.lastClearedNumber = this.cellsCleared;

					// notify user
					if (this.view.genNotifications) {
						this.view.menuManager.notification.notify("Cells killed after hitting boundary", 15, 120, 15, true);
					}
				}
			}
		}

		// update history bounding box if non-zero population
		if (this.population > 0) {
			if (zoomBox.leftX < historyBox.leftX) {
				historyBox.leftX = zoomBox.leftX;
			}
			if (zoomBox.rightX > historyBox.rightX) {
				historyBox.rightX = zoomBox.rightX;
			}
			if (zoomBox.bottomY < historyBox.bottomY) {
				historyBox.bottomY = zoomBox.bottomY;
			}
			if (zoomBox.topY > historyBox.topY) {
				historyBox.topY = zoomBox.topY;
			}
		}
	};

	// save population data
	Life.prototype.savePopulationData = function() {
		var	/** @type {number} */ popChunk = this.counter >> LifeConstants.popChunkPower,
			/** @type {number} */ popOffset = this.counter & ((1 << LifeConstants.popChunkPower) - 1);

		// update population graph
		if (this.counter < LifeConstants.maxPopSamples) {
			if (this.population > this.maxPopValue) {
				this.maxPopValue = this.population;
			}
			if (this.births > this.maxBirthsValue) {
				this.maxBirthsValue = this.births;
			}
			if (this.deaths > this.maxDeathsValue) {
				this.maxDeathsValue = this.deaths;
			}

			if (this.popGraphData && this.popGraphData.length > 0) {
				// see if a new chunk needs to be allocated
				if (this.counter >= this.popGraphEntries) {
					// allocate new chunk
					Array.addRow(this.popGraphData, 0, "Life.popGraphData");
					Array.addRow(this.birthGraphData, 0, "Life.birthGraphData");
					Array.addRow(this.deathGraphData, 0, "Life.deathGraphData");
					this.popGraphEntries += (1 << LifeConstants.popChunkPower);
				}
				this.popGraphData[popChunk][popOffset] = this.population;
				this.birthGraphData[popChunk][popOffset] = this.births;
				this.deathGraphData[popChunk][popOffset] = this.deaths;
			}
		}
	};

	// draw graph
	Life.prototype.renderGraph = function(/** @type {CanvasRenderingContext2D} */ ctx, /** @type {string} */ graphCol, /** @type {number} */ displayX, /** @type {number} */ graphHeight, /** @type {number} */ borderX, /** @type {number} */ borderY, /** @type {number} */ borderAxis, /** @type {Array<Uint32Array>} */ graphData, /** @type {boolean} */ lines) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ index = 0,
			/** @type {number} */ next = 0,
			/** @type {number} */ inc = 1,
			/** @type {number} */ minVal = 0,
			/** @type {number} */ maxVal,
			/** @type {number} */ nextVal = 0,
			/** @type {number} */ popChunk = 0,
			/** @type {number} */ popOffset = 0,
			/** @type {number} */ popMask = (1 << LifeConstants.popChunkPower) - 1,
			/** @type {number} */ maxPopSamples = LifeConstants.maxPopSamples,
			/** @type {number} */ dataPoints = this.counter;

		// ensure data points is no bigger than maximum allowed
		if (dataPoints > maxPopSamples) {
			dataPoints = maxPopSamples;
		}

		// check if increment is needed
		if (dataPoints > displayX) {
			inc = displayX / dataPoints;
		}

		// check for lines
		if (lines) {
			ctx.strokeStyle = graphCol;
			ctx.beginPath();
			y = (graphHeight - graphHeight * (graphData[0][0] / this.maxDataValue)) | 0;
			ctx.moveTo(borderX + borderAxis + 0.5, y + borderY + borderAxis + 0.5);
		} else {
			ctx.fillStyle = graphCol;
		}

		// check if there is enough data
		if (graphData.length << LifeConstants.popChunkPower < maxPopSamples) {
			maxPopSamples = graphData.length << LifeConstants.popChunkPower;
		}

		// draw graph
		next = 0;
		index = 1;
		for (i = 1; i < displayX; i += 1) {
			// get the next graph data point
			if (index < maxPopSamples) {
				popChunk = index >> LifeConstants.popChunkPower;
				popOffset = index & popMask;
				minVal = graphData[popChunk][popOffset];
				maxVal = minVal;
				next = next + inc;
				index = index + 1;
				while ((next | 0) === 0 && (index < maxPopSamples)) {
					popChunk = index >> LifeConstants.popChunkPower;
					popOffset = index & popMask;
					nextVal = graphData[popChunk][popOffset];
					index = index + 1;
					if (nextVal > maxVal) {
						maxVal = nextVal;
					}
					if (nextVal < minVal) {
						minVal = nextVal;
					}
					next = next + inc;
				}
				next = next - 1;
			} else {
				minVal = 0;
				maxVal = 0;
			}

			// check if there is data for this point
			if (i <= this.counter) {
				x = borderX + borderAxis + 1 + i;
				y = (graphHeight - graphHeight * (minVal / this.maxDataValue)) | 0;
				// check whether using lines or points
				if (lines) {
					// drawing lines
					ctx.lineTo(x + 0.5, y + borderY + borderAxis + 0.5);

					// check if there was a range of values at this sample point
					if (minVal !== maxVal) {
						y = (graphHeight - graphHeight * (maxVal / this.maxDataValue)) | 0;
						ctx.lineTo(x + 0.5, y + borderY + borderAxis + 0.5);
					}
				} else {
					// drawing points
					if (minVal > 0 && maxVal > 0) {
						// single value so draw point
						if (minVal === maxVal) {
							ctx.fillRect(x + 0.5, y + borderY + borderAxis + 0.5, 1, 1);
						} else {
							y = (graphHeight - graphHeight * (maxVal / this.maxDataValue)) | 0;
							ctx.fillRect(x + 0.5, y + borderY + borderAxis + 0.5, 1, 1);
						}
					}
				}
			}
		}

		if (lines) {
			ctx.stroke();
		}
	};

	// draw population data
	Life.prototype.drawPopGraph = function(/** @type {boolean} */ lines, /** @type {number} */ opacity, /** @type {boolean} */ fullScreen, /** @type {boolean} */ thumbnail, /** @type {View} */ view) {
		var	/** @type {CanvasRenderingContext2D} */ ctx = this.context,
			/** @type {number} */ xScale = view.viewMenu.xScale,
			/** @type {number} */ yScale = view.viewMenu.yScale,
			/** @type {number} */ borderX = 0,
			/** @type {number} */ borderY = 40 * yScale,
			/** @type {number} */ borderAxis = 40 * xScale,
			/** @type {number} */ graphWidth = this.displayWidth - borderX - borderAxis,
			/** @type {number} */ graphHeight = this.displayHeight - borderY - borderAxis,
			/** @type {number} */ displayX = 0,
			/** @type {number} */ i = 0,
			/** @type {number} */ dataPoints = this.counter,
			/** @type {string} */ graphBgColor = "rgb(" + this.graphBgColor[0] + "," + this.graphBgColor[1] + "," + this.graphBgColor[2] + ")",
			/** @type {string} */ graphAxisColor = "rgb(" + this.graphAxisColor[0] + "," + this.graphAxisColor[1] + "," + this.graphAxisColor[2] + ")",
			/** @type {string} */ graphAliveColor = "rgb(" + this.graphAliveColor[0] + "," + this.graphAliveColor[1] + "," + this.graphAliveColor[2] + ")",
			/** @type {string} */ graphBirthColor = "rgb(" + this.graphBirthColor[0] + "," + this.graphBirthColor[1] + "," + this.graphBirthColor[2] + ")",
			/** @type {string} */ graphDeathColor = "rgb(" + this.graphDeathColor[0] + "," + this.graphDeathColor[1] + "," + this.graphDeathColor[2] + ")";

		// save current population
		this.savePopulationData();

		// check if data exists
		if (this.popGraphData && this.popGraphData.length > 0) {
			// check for full screen
			if (fullScreen) {
				graphHeight = this.displayHeight;
				borderY = 0;
			} else {
				// check for thumbnail
				if (thumbnail) {
					graphHeight = this.displayHeight;
					borderY = 0;
					graphWidth = this.displayWidth;
					borderAxis = 0;
				}
			}

			// compute number of samples
			displayX = graphWidth - borderX - borderAxis;

			// ensure data points is no bigger than maximum allowed
			if (dataPoints > LifeConstants.maxPopSamples) {
				dataPoints = LifeConstants.maxPopSamples;
			}

			// compute the maximum data value based on data types selected
			this.maxDataValue = 0;
			if (this.boundedGridType === -1) {
				if (view.graphShowBirths) {
					this.maxDataValue = this.maxBirthsValue;
				}

				if (view.graphShowDeaths) {
					if (this.maxDeathsValue > this.maxDataValue) {
						this.maxDataValue = this.maxDeathsValue;
					}
				}
			}
			if (view.graphShowPopulation) {
				if (this.maxPopValue > this.maxDataValue) {
					this.maxDataValue = this.maxPopValue;
				}
			}

			// draw
			// save context
			ctx.save();

			// draw background
			ctx.fillStyle = graphBgColor;
			ctx.globalAlpha = opacity;
			ctx.fillRect(borderX, borderY, graphWidth + borderAxis, graphHeight);
			ctx.globalAlpha = 1;
			if (fullScreen) {
				graphHeight -= borderAxis;
			}

			// draw labels
			if (!thumbnail) {
				ctx.font = ((16 * xScale) | 0) + "px Arial";
				ctx.textAlign = "center";
				ctx.fillStyle = view.menuManager.bgCol;
				for (i = 2; i >= 0; i -= 2) {
					ctx.save();
					ctx.translate(this.displayWidth / 2, graphHeight + borderAxis / 2 - ((6 * xScale) | 0));
					ctx.fillText("Generation", i, i);
					ctx.restore();
					ctx.save();
					ctx.translate(borderX + borderAxis / 2 + ((6 * xScale) | 0), this.displayHeight / 2);
					ctx.rotate(-90 * Math.PI / 180);
					if (this.displayHeight < 320) {
						ctx.fillText("Pop", i, i);
					} else {
						ctx.fillText("Population", i, i);
					}
					ctx.restore();
					ctx.fillStyle = graphAxisColor;
				}

				// draw axes values
				ctx.fillStyle = view.menuManager.bgCol;
				for (i = 2; i >= 0; i -= 2) {
					ctx.save();
					ctx.translate(borderX + borderAxis - borderAxis / 2 + ((6 * xScale) | 0), borderY + borderAxis);
					ctx.rotate(-90 * Math.PI / 180);
					ctx.fillText(String(this.maxDataValue), i, i);
					ctx.restore();
					ctx.save();
					ctx.translate(borderX + borderAxis - borderAxis / 2 + ((6 * xScale) | 0), graphHeight);
					ctx.rotate(-90 * Math.PI / 180);
					ctx.fillText("0", i, i);
					ctx.restore();
					ctx.save();
					ctx.translate(borderX + borderAxis, graphHeight + borderAxis / 2 - ((6 * xScale) | 0));
					ctx.fillText("0", i, i);
					ctx.restore();
					ctx.save();
					ctx.translate(graphWidth, graphHeight + borderAxis / 2 - ((6 * xScale) | 0));
					ctx.fillText(String(dataPoints > displayX ? dataPoints : (displayX | 0)), i, i);
					ctx.restore();
					ctx.fillStyle = graphAxisColor;
				}
			}

			// only draw births and deaths if grid is not bounded
			if (this.boundedGridType === -1) {
				// draw deaths unless disabled
				if (view.graphShowDeaths) {
					this.renderGraph(ctx, graphDeathColor, displayX, graphHeight - borderY - borderAxis, borderX, borderY, borderAxis, this.deathGraphData, lines);
				}

				// draw births unless disabled
				if (view.graphShowBirths) {
					this.renderGraph(ctx, graphBirthColor, displayX, graphHeight - borderY - borderAxis, borderX, borderY, borderAxis, this.birthGraphData, lines);
				}
			}

			// draw population unless disabled but always show in bounded grids
			if (this.boundedGridType !== -1 || view.graphShowPopulation) {
				this.renderGraph(ctx, graphAliveColor, displayX, graphHeight - borderY - borderAxis, borderX, borderY, borderAxis, this.popGraphData, lines);
			}

			// if nothing selected to draw display a message
			if (this.boundedGridType === -1) {
				if (!(view.graphShowDeaths || view.graphShowBirths || view.graphShowPopulation)) {
					ctx.save();
					ctx.font = ((24 * xScale) | 0) + "px Arial";
					ctx.translate(this.displayWidth / 2, this.displayHeight / 2);
					ctx.fillStyle = view.menuManager.bgCol;
					ctx.fillText("Select Data", 2, 2);
					ctx.fillStyle = graphAxisColor;
					ctx.fillText("Select Data", 0, 0);
					ctx.restore();
				}
			}

			// draw axes
			ctx.strokeStyle = graphAxisColor;
			ctx.lineWidth = 1;
			ctx.beginPath();
			ctx.moveTo(borderX + borderAxis + 0.5, borderY + borderAxis + 0.5);
			ctx.lineTo(borderX + borderAxis + 0.5, graphHeight + 0.5);
			ctx.lineTo(graphWidth + 0.5, graphHeight + 0.5);
			ctx.lineTo(graphWidth + 0.5, graphHeight + 0.5 + 2);
			ctx.moveTo(borderX + borderAxis + 0.5, graphHeight + 0.5);
			ctx.lineTo(borderX + borderAxis + 0.5, graphHeight + 0.5 + 2);
			ctx.moveTo(borderX + borderAxis + 0.5, graphHeight + 0.5);
			ctx.lineTo(borderX + borderAxis + 0.5 - 2, graphHeight + 0.5);
			ctx.moveTo(borderX + borderAxis + 0.5, borderY + borderAxis + 0.5);
			ctx.lineTo(borderX + borderAxis + 0.5 - 2, borderY + borderAxis + 0.5);
			ctx.stroke();

			// restore context
			ctx.restore();
		}
	};

	// reset population data
	Life.prototype.resetPopulationData = function() {
		var	/** @type {number} */ i = 0;

		// clear population graph data
		if (this.popGraphData && this.popGraphData.length > 0) {
			if (this.counter > 0) {
				for (i = 0; i < this.popGraphData.length; i += 1) {
					this.popGraphData[i].fill(0);
					this.birthGraphData[i].fill(0);
					this.deathGraphData[i].fill(0);
				}
			}

			// set initial population
			this.popGraphData[0][0] = this.population;
		}

		// reset maximum population
		this.maxPopValue = this.population;
		this.maxBirthsValue = this.births;
		this.maxDeathsValue = this.deaths;
	};

	// reset history box
	Life.prototype.resetHistoryBox = function() {
		var	/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox;

		historyBox.leftX = zoomBox.leftX;
		historyBox.rightX = zoomBox.rightX;
		historyBox.topY = zoomBox.topY;
		historyBox.bottomY = zoomBox.bottomY;
	};

	// state 6 mask pre function
	Life.prototype.state6Pre = function() {
		var	/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ leftX = 0,

			// 16bit view of grid
			/** @type {Array<Uint16Array>} */ grid16 = null,

			// state6 masks
			/** @type {Array<Uint16Array>} */ mask = this.state6Mask,
			/** @type {Array<Uint16Array>} */ aliveMask = this.state6Alive,
			/** @type {Array<Uint16Array>} */ cells = this.state6Cells,

			// state 6 tile grid
			/** @type {Array<Uint16Array>} */ state6TileGrid = this.state6TileGrid,

			// tile row
			/** @type {Uint16Array} */ tileRow = null,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// current tile group
			/** @type {number} */ tiles = 0,

			// tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16 bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1;

		// set initial tile row
		bottomY = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid16 = this.nextGrid16;
		} else {
			grid16 = this.grid16;
		}

		// check each tile
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the next tile row
			tileRow = state6TileGrid[th];

			// scan the row
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];

				// check if any are occupied
				if (tiles) {
					// process the tiles in the group
					for (b = 15; b >= 0; b -= 1) {
						// check if the tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// process the tile
							for (y = bottomY; y < bottomY + 16; y += 1) {
								aliveMask[y][leftX] = (grid16[y][leftX] | cells[y][leftX]) & mask[y][leftX];
							}
						}

						// next tile column
						leftX += xSize;
					}
				} else {
					// skip tile group
					leftX += xSize << 4;
				}
			}

			// next tile rows
			bottomY += ySize;
		}
	};

	// state 6 mask post function
	Life.prototype.state6Post = function() {
		var	/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ orig = 0,
			/** @type {number} */ nextAlive = 0,
			/** @type {number} */ nextCells = 0,
			/** @type {number} */ popDiff = 0,
			/** @type {number} */ popDiff2 = 0,

			// bit counts
			/** @type {Uint8Array} */ bitCounts16 = this.bitCounts16,

			// 16bit view of grid
			/** @type {Array<Uint16Array>} */ nextGrid16 = null,

			// state6 masks
			/** @type {Array<Uint16Array>} */ aliveMask = this.state6Alive,

			// state6 cells
			/** @type {Array<Uint16Array>} */ cellsMask = this.state6Cells,

			// state 6 tile grid
			/** @type {Array<Uint16Array>} */ state6TileGrid = this.state6TileGrid,

			// tile row
			/** @type {Uint16Array} */ tileRow = null,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// current tile group
			/** @type {number} */ tiles = 0,

			// tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16 bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			nextGrid16 = this.nextGrid16;
		} else {
			nextGrid16 = this.grid16;
		}

		// set initial tile row
		bottomY = 0;

		// check each tile
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the next tile row
			tileRow = state6TileGrid[th];

			// scan the row
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];

				// check if any are occupied
				if (tiles) {
					// process the tiles in the group
					for (b = 15; b >= 0; b -= 1) {
						// check if the tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// process the tile
							for (y = bottomY; y < bottomY + 16; y += 1) {
								orig = nextGrid16[y][leftX];
								nextAlive = orig & ~aliveMask[y][leftX];
								nextCells = orig & ~cellsMask[y][leftX];

								if (nextAlive !== orig) {
									nextGrid16[y][leftX] = nextAlive;
									popDiff = bitCounts16[orig] - bitCounts16[nextAlive];
									popDiff2 = bitCounts16[orig] - bitCounts16[nextCells];

									this.population -= popDiff;
									this.births -= popDiff2;
									this.deaths += (popDiff - popDiff2);
								}
							}
						}

						// next tile column
						leftX += xSize;
					}
				} else {
					// skip tile group
					leftX += xSize << 4;
				}
			}

			// next tile rows
			bottomY += ySize;
		}
	};

	// remove pattern starting at a given cell
	Life.prototype.removePattern = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {Array<Uint16Array>} */ grid16, /** @type {Array<Uint8Array>} */ colourGrid) {
		var	/** @type {number} */ tx = 0,
			/** @type {number} */ ty = 0,

			// number of pages
			/** @type {number} */ pages = this.boundaryPages,

			// current page
			/** @type {number} */ page = 0,

			// boundary cell stack
			/** @type {Int32Array} */ bx = this.boundaryX[page],
			/** @type {Int32Array} */ by = this.boundaryY[page],
			/** @type {number} */ index = 0,

			// maximum buffer size
			/** @type {number} */ max = LifeConstants.removePatternBufferSize,

			// page mask
			/** @type {number} */ mask = max - 1,

			// start and end of current page
			/** @type {number} */ start = 0,
			/** @type {number} */ end = max,

			// boundary cell radius
			/** @type {number} */ radius = this.removePatternRadius,

			// width and height mask
			/** @type {number} */ widthMask = this.widthMask,
			/** @type {number} */ heightMask = this.heightMask,

			// dying state for Generations
			/** @type {number} */ dyingState = this.multiNumStates + this.historyStates - 2,

			// number of cells cleared
			/** @type {number} */ cleared = 0;

		// stack the current cell
		bx[index] = x;
		by[index] = y;
		index += 1;

		// check for 2-state Life-like patterns
		if (this.multiNumStates === -1) {
			// set the dying state to the alive state since the cell will die on the bit
			// grid and the next update of the colour grid will make it die
			dyingState = this.aliveStart;
		}

		// remove the cell
		if (grid16[y][x >> 4] & (1 << (~x & 15))) {
			grid16[y][x >> 4] &= ~(1 << (~x & 15));
			cleared += 1;

			// TBD isExtended

			if (this.isSuper) {
				switch (colourGrid[y][x]) {
					case 3:
					case 4:
						dyingState = 4;
						break;

					case 7:
					case 8:
						dyingState = 8;
						break;

					case 9:
						dyingState = 10;
						break;

					case 11:
						dyingState = 12;
						break;

					case 13:
					case 15:
					case 17:
					case 19:
					case 21:
					case 23:
					case 25:
						dyingState = 0;
						break;

					default:
						dyingState = 2;
						break;
				}
			}
			colourGrid[y][x] = dyingState;
		}

		// keep going until all cells processed
		while (index > 0) {
			// get the next cell
			index -= 1;

			// check for previous page
			if (index < start) {
				// switch to previous page
				page -= 1;
				start -= max;
				end -= max;

				// get the page array
				bx = this.boundaryX[page];
				by = this.boundaryY[page];
			}
			x = bx[index & mask];
			y = by[index & mask];

			// check the surrounding cells
			ty = y - radius;
			while (ty <= y + radius) {
				tx = x - radius;
				while (tx <= x + radius) {
					// check cell is on grid
					if (tx === (tx & widthMask) && ty === (ty & heightMask)) {
						// check if cell set
						if ((grid16[ty][tx >> 4] & (1 << (~tx & 15))) !== 0) {
							// remove the cell
							grid16[ty][tx >> 4] &= ~(1 << (~tx & 15));
							cleared += 1;

							// TBD isExtended

							if (this.isSuper) {
								switch (colourGrid[ty][tx]) {
									case 3:
									case 4:
										dyingState = 4;
										break;

									case 7:
									case 8:
										dyingState = 8;
										break;

									case 9:
										dyingState = 10;
										break;

									case 11:
										dyingState = 12;
										break;

									case 13:
									case 15:
									case 17:
									case 19:
									case 21:
									case 23:
									case 25:
										dyingState = 0;
										break;

									default:
										dyingState = 2;
								}
							}
							colourGrid[ty][tx] = dyingState;

							// stack the cell
							if (index === end) {
								// switch to next page
								page += 1;
								start += max;
								end += max;

								// check if page is allocated
								if (page > pages) {
									// allocate new page
									this.boundaryX[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryX" + page, false));
									this.boundaryY[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryY" + page, false));

									// save new page
									pages += 1;
									this.boundaryPages = pages;
								}

								// get the page array
								bx = this.boundaryX[page];
								by = this.boundaryY[page];
							}

							// save the cell
							bx[index & mask] = tx;
							by[index & mask] = ty;
							index += 1;
						}
					}
					tx += 1;
				}
				ty += 1;
			}
		}

		// update statistics
		this.population -= cleared;
		this.deaths += cleared;
	};

	// remove pattern at top or bottom
	Life.prototype.removePatternY = function(/** @type {number} */ x16, /** @type {number} */ y, /** @type {Array<Uint16Array>} */ grid16, /** @type {Array<Uint8Array>} */ colourGrid) {
		// source word
		var	/** @type {number} */ source = grid16[y][x16],

			// check each bit
			/** @type {number} */ x = 15;

		while (x >= 0) {
			if ((source & (1 << x)) !== 0) {
				// remove pattern
				this.removePattern((x16 << 4) + (~x & 15), y, grid16, colourGrid);
			}
			x -= 1;
		}
	};

	// remove pattern at left
	Life.prototype.removePatternLeft = function(/** @type {number} */ x16, /** @type {number} */ y, /** @type {Array<Uint16Array>} */ grid16, /** @type {Array<Uint8Array>} */ colourGrid) {
		// compute starting x
		var	/** @type {number} */ x = (x16 << 4);

		// remove pattern
		this.removePattern(x, y, grid16, colourGrid);
	};

	// remove pattern at right
	Life.prototype.removePatternRight = function(/** @type {number} */ x16, /** @type {number} */ y, /** @type {Array<Uint16Array>} */ grid16, /** @type {Array<Uint8Array>} */ colourGrid) {
		// compute starting x
		var	/** @type {number} */ x = (x16 << 4) + 15;

		// remove pattern
		this.removePattern(x, y, grid16, colourGrid);
	};

	// remove patterns that touch the boundary
	Life.prototype.clearGridBoundary = function() {
		// 16bit view of grid
		var	/** @type {Array<Uint16Array>} */ grid16 = null,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,

			// height and width
			/** @type {number} */ ht = 0,
			/** @type {number} */ wt = 0,

			// counters
			/** @type {number} */ w = 0,
			/** @type {number} */ h = 0,

			// top and bottom row
			/** @type {Uint16Array} */ topGridRow = null,
			/** @type {Uint16Array} */ bottomGridRow = null,

			// bit masks
			/** @type {number} */ leftMask = 1 << 15,
			/** @type {number} */ rightMask = 1 << 0;

		// check for Super
		if (this.isSuper) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			// use grid
			grid16 = this.nextGrid16;
		} else {
			// use next grid
			grid16 = this.grid16;
		}

		// get the width and height
		ht = grid16.length;
		wt = grid16[0].length;

		// get the top and bottom rows
		topGridRow = grid16[0];
		bottomGridRow = grid16[ht - 1];

		// check the rows
		w = 0;
		while (w < wt) {
			// check the top row
			if (topGridRow[w]) {
				this.removePatternY(w, 0, grid16, colourGrid);
			}

			// check the bottom row
			if (bottomGridRow[w]) {
				this.removePatternY(w, ht - 1, grid16, colourGrid);
			}

			// next column
			w += 1;
		}

		// check the left and right columns
		h = 0;
		while (h < ht) {
			// check the left column
			if ((grid16[h][0] & leftMask) !== 0) {
				this.removePatternLeft(0, h, grid16, colourGrid);
			}

			// check the right column
			if ((grid16[h][wt - 1] & rightMask) !== 0) {
				this.removePatternRight(wt - 1, h, grid16, colourGrid);
			}

			// next row
			h += 1;
		}
	};

	// remove RuleTable pattern starting at a given cell
	Life.prototype.removeRTPattern = function(/** @type {number} */ x, /** @type {number} */ y) {
		var	/** @type {number} */ tx = 0,
			/** @type {number} */ ty = 0,

			// multi-state grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,

			// number of pages
			/** @type {number} */ pages = this.boundaryPages,

			// current page
			/** @type {number} */ page = 0,

			// boundary cell stack
			/** @type {Int32Array} */ bx = this.boundaryX[page],
			/** @type {Int32Array} */ by = this.boundaryY[page],
			/** @type {number} */ index = 0,

			// maximum buffer size
			/** @type {number} */ max = LifeConstants.removePatternBufferSize,

			// page mask
			/** @type {number} */ mask = max - 1,

			// start and end of current page
			/** @type {number} */ start = 0,
			/** @type {number} */ end = max,

			// masks for width and height
			/** @type {number} */ widthMask = this.widthMask,
			/** @type {number} */ heightMask = this.heightMask,

			// boundary cell radius
			/** @type {number} */ radius = this.removePatternRadius,

			// dead cells state
			/** @type {number} */ deadState = this.isPCA ? this.historyStates : 0,

			// number of cells cleared
			/** @type {number} */ cleared = 0;

		// swap grids every generation
		if ((this.counter & 1) !== 0) {
			colourGrid = this.nextColourGrid;
		}

		// stack the current cell
		bx[index] = x;
		by[index] = y;
		index += 1;

		// remove the cell
		if (colourGrid[y][x] > deadState) {
			if (this.isPCA) {
				cleared += this.bitCounts16[colourGrid[y][x] - this.historyStates];
			} else {
				cleared += 1;
			}
			colourGrid[y][x] = deadState;
		}

		// keep going until all cells processed
		while (index > 0) {
			// get the next cell
			index -= 1;

			// check for previous page
			if (index < start) {
				// switch to previous page
				page -= 1;
				start -= max;
				end -= max;

				// get the page array
				bx = this.boundaryX[page];
				by = this.boundaryY[page];
			}
			x = bx[index & mask];
			y = by[index & mask];

			// check the surrounding cells
			ty = y - radius;
			while (ty <= y + radius) {
				tx = x - radius;
				colourRow = colourGrid[ty];
				while (tx <= x + radius) {
					// check cell is on grid
					if (tx === (tx & widthMask) && ty === (ty & heightMask)) {
						// check if cell set
						if (colourRow[tx] > deadState) {
							if (this.isPCA) {
								// remove the cells
								cleared += this.bitCounts16[colourRow[tx] - this.historyStates];
							} else {
								// remove the cell
								cleared += 1;
							}
							colourRow[tx] = deadState;

							// stack the cell
							if (index === end) {
								// switch to next page
								page += 1;
								start += max;
								end += max;

								// check if page is allocated
								if (page > pages) {
									// allocate new page
									this.boundaryX[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryX" + page, false));
									this.boundaryY[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryY" + page, false));

									// save new page
									pages += 1;
									this.boundaryPages = pages;
								}

								// get the page array
								bx = this.boundaryX[page];
								by = this.boundaryY[page];
							}

							// save the cell
							bx[index & mask] = tx;
							by[index & mask] = ty;
							index += 1;
						}
					}
					tx += 1;
				}
				ty += 1;
			}
		}

		// update statistics
		this.population -= cleared;
		this.deaths += cleared;
	};


	// remove [R]Extended pattern starting at a given cell
	Life.prototype.removeExtendedPattern = function(/** @type {number} */ x, /** @type {number} */ y) {
		var	/** @type {number} */ tx = 0,
			/** @type {number} */ ty = 0,

			// multi-state grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,

			// number of pages
			/** @type {number} */ pages = this.boundaryPages,

			// current page
			/** @type {number} */ page = 0,

			// boundary cell stack
			/** @type {Int32Array} */ bx = this.boundaryX[page],
			/** @type {Int32Array} */ by = this.boundaryY[page],
			/** @type {number} */ index = 0,

			// maximum buffer size
			/** @type {number} */ max = LifeConstants.removePatternBufferSize,

			// page mask
			/** @type {number} */ mask = max - 1,

			// start and end of current page
			/** @type {number} */ start = 0,
			/** @type {number} */ end = max,

			// masks for width and height
			/** @type {number} */ widthMask = this.widthMask,
			/** @type {number} */ heightMask = this.heightMask,

			// boundary cell radius
			/** @type {number} */ radius = this.removePatternRadius,

			// number of cells cleared
			/** @type {number} */ cleared = 0;

		// swap grids every generation
		if ((this.counter & 1) !== 0) {
			colourGrid = this.nextColourGrid;
		}

		// stack the current cell
		bx[index] = x;
		by[index] = y;
		index += 1;

		// remove the cell
		if (colourGrid[y][x] === 1) {
			colourGrid[y][x] = 0;
			cleared += 1;
		}

		// keep going until all cells processed
		while (index > 0) {
			// get the next cell
			index -= 1;

			// check for previous page
			if (index < start) {
				// switch to previous page
				page -= 1;
				start -= max;
				end -= max;

				// get the page array
				bx = this.boundaryX[page];
				by = this.boundaryY[page];
			}
			x = bx[index & mask];
			y = by[index & mask];

			// check the surrounding cells
			ty = y - radius;
			while (ty <= y + radius) {
				tx = x - radius;
				colourRow = colourGrid[ty];
				while (tx <= x + radius) {
					// check cell is on grid
					if (tx === (tx & widthMask) && ty === (ty & heightMask)) {
						// check if cell set
						if (colourRow[tx] === 1) {
							// remove the cell
							colourRow[tx] = 0;
							cleared += 1;

							// stack the cell
							if (index === end) {
								// switch to next page
								page += 1;
								start += max;
								end += max;

								// check if page is allocated
								if (page > pages) {
									// allocate new page
									this.boundaryX[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryX" + page, false));
									this.boundaryY[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryY" + page, false));

									// save new page
									pages += 1;
									this.boundaryPages = pages;
								}

								// get the page array
								bx = this.boundaryX[page];
								by = this.boundaryY[page];
							}

							// save the cell
							bx[index & mask] = tx;
							by[index & mask] = ty;
							index += 1;
						}
					}
					tx += 1;
				}
				ty += 1;
			}
		}

		// update statistics
		this.population -= cleared;
		this.deaths += cleared;
	};

	// remove multi-state pattern starting at a given cell
	Life.prototype.removeMSPattern2 = function(/** @type {number} */ x, /** @type {number} */ y) {
		var	/** @type {number} */ tx = 0,
			/** @type {number} */ ty = 0,

			// multi-state grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,

			// number of pages
			/** @type {number} */ pages = this.boundaryPages,

			// current page
			/** @type {number} */ page = 0,

			// boundary cell stack
			/** @type {Int32Array} */ bx = this.boundaryX[page],
			/** @type {Int32Array} */ by = this.boundaryY[page],
			/** @type {number} */ index = 0,

			// maximum buffer size
			/** @type {number} */ max = LifeConstants.removePatternBufferSize,

			// page mask
			/** @type {number} */ mask = max - 1,

			// start and end of current page
			/** @type {number} */ start = 0,
			/** @type {number} */ end = max,

			// masks for width and height
			/** @type {number} */ widthMask = this.widthMask,
			/** @type {number} */ heightMask = this.heightMask,

			// alive state
			/** @type {number} */ alive = LifeConstants.aliveStart,
			/** @type {number} */ dead = LifeConstants.deadStart,

			// boundary cell radius
			/** @type {number} */ radius = this.removePatternRadius,

			// number of cells cleared
			/** @type {number} */ cleared = 0;

		// stack the current cell
		bx[index] = x;
		by[index] = y;
		index += 1;

		// remove the cell
		if (colourGrid[y][x] >= alive) {
			colourGrid[y][x] = dead;
			cleared += 1;
		}

		// keep going until all cells processed
		while (index > 0) {
			// get the next cell
			index -= 1;

			// check for previous page
			if (index < start) {
				// switch to previous page
				page -= 1;
				start -= max;
				end -= max;

				// get the page array
				bx = this.boundaryX[page];
				by = this.boundaryY[page];
			}
			x = bx[index & mask];
			y = by[index & mask];

			// check the surrounding cells
			ty = y - radius;
			while (ty <= y + radius) {
				tx = x - radius;
				colourRow = colourGrid[ty];
				while (tx <= x + radius) {
					// check cell is on grid
					if (tx === (tx & widthMask) && ty === (ty & heightMask)) {
						// check if cell set
						if (colourRow[tx] >= alive) {
							// remove the cell
							colourRow[tx] = dead;
							cleared += 1;

							// stack the cell
							if (index === end) {
								// switch to next page
								page += 1;
								start += max;
								end += max;

								// check if page is allocated
								if (page > pages) {
									// allocate new page
									this.boundaryX[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryX" + page, false));
									this.boundaryY[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryY" + page, false));

									// save new page
									pages += 1;
									this.boundaryPages = pages;
								}

								// get the page array
								bx = this.boundaryX[page];
								by = this.boundaryY[page];
							}

							// save the cell
							bx[index & mask] = tx;
							by[index & mask] = ty;
							index += 1;
						}
					}
					tx += 1;
				}
				ty += 1;
			}
		}

		// update statistics
		this.population -= cleared;
		this.deaths += cleared;
	};


	// remove multi-state pattern starting at a given cell
	Life.prototype.removeMSPatternN = function(/** @type {number} */ x, /** @type {number} */ y) {
		var	/** @type {number} */ tx = 0,
			/** @type {number} */ ty = 0,

			// multi-state grid
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,

			// number of pages
			/** @type {number} */ pages = this.boundaryPages,

			// current page
			/** @type {number} */ page = 0,

			// boundary cell stack
			/** @type {Int32Array} */ bx = this.boundaryX[page],
			/** @type {Int32Array} */ by = this.boundaryY[page],
			/** @type {number} */ index = 0,

			// maximum buffer size
			/** @type {number} */ max = LifeConstants.removePatternBufferSize,

			// page mask
			/** @type {number} */ mask = max - 1,

			// start and end of current page
			/** @type {number} */ start = 0,
			/** @type {number} */ end = max,

			// masks for width and height
			/** @type {number} */ widthMask = this.widthMask,
			/** @type {number} */ heightMask = this.heightMask,

			// alive state
			/** @type {number} */ alive = this.multiNumStates + this.historyStates - 1,
			// dying state for Generations
			/** @type {number} */ dyingState = this.multiNumStates + this.historyStates - 2,

			// boundary cell radius
			/** @type {number} */ radius = this.removePatternRadius,

			// number of cells cleared
			/** @type {number} */ cleared = 0;

		// stack the current cell
		bx[index] = x;
		by[index] = y;
		index += 1;

		// remove the cell
		if (colourGrid[y][x] === alive) {
			colourGrid[y][x] = dyingState;
			cleared += 1;
		}

		// keep going until all cells processed
		while (index > 0) {
			// get the next cell
			index -= 1;

			// check for previous page
			if (index < start) {
				// switch to previous page
				page -= 1;
				start -= max;
				end -= max;

				// get the page array
				bx = this.boundaryX[page];
				by = this.boundaryY[page];
			}
			x = bx[index & mask];
			y = by[index & mask];

			// check the surrounding cells
			ty = y - radius;
			while (ty <= y + radius) {
				tx = x - radius;
				colourRow = colourGrid[ty];
				while (tx <= x + radius) {
					// check cell is on grid
					if (tx === (tx & widthMask) && ty === (ty & heightMask)) {
						// check if cell set
						if (colourRow[tx] === alive) {
							// remove the cell
							colourRow[tx] = dyingState;
							cleared += 1;

							// stack the cell
							if (index === end) {
								// switch to next page
								page += 1;
								start += max;
								end += max;

								// check if page is allocated
								if (page > pages) {
									// allocate new page
									this.boundaryX[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryX" + page, false));
									this.boundaryY[page] = /** @type {!Int32Array} */ (this.allocator.allocate(Type.Int32, max, "Life.boundaryY" + page, false));

									// save new page
									pages += 1;
									this.boundaryPages = pages;
								}

								// get the page array
								bx = this.boundaryX[page];
								by = this.boundaryY[page];
							}

							// save the cell
							bx[index & mask] = tx;
							by[index & mask] = ty;
							index += 1;
						}
					}
					tx += 1;
				}
				ty += 1;
			}
		}

		// update statistics
		this.population -= cleared;
		this.deaths += cleared;
	};

	// remove RuleTable patterns that touch the boundary
	Life.prototype.clearRTBoundary = function() {
		// grid
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,

			// height and width
			/** @type {number} */ ht = colourGrid.length,
			/** @type {number} */ wd = colourGrid[0].length,

			// used grid
			zoomBox = this.zoomBox,
			/** @type {number} */ leftX = zoomBox.leftX,
			/** @type {number} */ rightX = zoomBox.rightX,
			/** @type {number} */ bottomY = zoomBox.bottomY,
			/** @type {number} */ topY = zoomBox.topY,

			// distance from grid edge
			/** @type {number} */ xrange = 1,
			/** @type {number} */ yrange = 1,

			// dead state
			/** @type {number} */ deadState = this.isPCA ? this.historyStates : 0,

			// counters
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0;

		// swap grids every generation
		if ((this.counter & 1) !== 0) {
			colourGrid = this.nextColourGrid;
		}

		// clear top boundary
		if ((ht - topY - 1) <= yrange) {
			for (y = ht - yrange - 1; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= rightX; x += 1) {
					if (colourRow[x] > deadState) {
						this.removeRTPattern(x, y);
					}
				}
			}
			zoomBox.topY = ht - yrange;
		}

		// clear bottom boundary
		if (bottomY <= yrange) {
			for (y = bottomY; y <= yrange; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= rightX; x += 1) {
					if (colourRow[x] > deadState) {
						this.removeRTPattern(x, y);
					}
				}
			}
			zoomBox.bottomY = yrange;
		}

		// clear left boundary
		if (leftX <= xrange) {
			for (y = bottomY; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= xrange; x += 1) {
					if (colourRow[x] > deadState) {
						this.removeRTPattern(x, y);
					}
				}
			}
			zoomBox.leftX = xrange;
		}

		// clear right boundary
		if ((wd - rightX - 1) <= xrange) {
			for (y = bottomY; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = wd - xrange - 1; x <= rightX; x += 1) {
					if (colourRow[x] > deadState) {
						this.removeRTPattern(x, y);
					}
				}
			}
			zoomBox.rightX = wd - xrange;
		}
	};


	// remove [R]Extended patterns that touch the boundary
	Life.prototype.clearExtendedBoundary = function() {
		// grid
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,

			// height and width
			/** @type {number} */ ht = colourGrid.length,
			/** @type {number} */ wd = colourGrid[0].length,

			// used grid
			zoomBox = this.zoomBox,
			/** @type {number} */ leftX = zoomBox.leftX,
			/** @type {number} */ rightX = zoomBox.rightX,
			/** @type {number} */ bottomY = zoomBox.bottomY,
			/** @type {number} */ topY = zoomBox.topY,

			// distance from grid edge
			/** @type {number} */ xrange = 1,
			/** @type {number} */ yrange = 1,

			// counters
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0;

		// swap grids every generation
		if ((this.counter & 1) !== 0) {
			colourGrid = this.nextColourGrid;
		}

		// clear top boundary
		if ((ht - topY - 1) <= yrange) {
			for (y = ht - yrange - 1; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= rightX; x += 1) {
					if (colourRow[x] > 0) {
						this.removeExtendedPattern(x, y);
					}
				}
			}
			zoomBox.topY = ht - yrange;
		}

		// clear bottom boundary
		if (bottomY <= yrange) {
			for (y = bottomY; y <= yrange; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= rightX; x += 1) {
					if (colourRow[x] > 0) {
						this.removeExtendedPattern(x, y);
					}
				}
			}
			zoomBox.bottomY = yrange;
		}

		// clear left boundary
		if (leftX <= xrange) {
			for (y = bottomY; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= xrange; x += 1) {
					if (colourRow[x] > 0) {
						this.removeExtendedPattern(x, y);
					}
				}
			}
			zoomBox.leftX = xrange;
		}

		// clear right boundary
		if ((wd - rightX - 1) <= xrange) {
			for (y = bottomY; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = wd - xrange - 1; x <= rightX; x += 1) {
					if (colourRow[x] > 0) {
						this.removeExtendedPattern(x, y);
					}
				}
			}
			zoomBox.rightX = wd - xrange;
		}
	};

	// remove HROT patterns that touch the boundary
	Life.prototype.clearHRBoundary = function(/** @type {number} */ xrange, /** @type {number} */ yrange) {
		// grid
		var	/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,

			// height and width
			/** @type {number} */ ht = colourGrid.length,
			/** @type {number} */ wd = colourGrid[0].length,

			// used grid
			zoomBox = this.zoomBox,
			/** @type {number} */ leftX = zoomBox.leftX,
			/** @type {number} */ rightX = zoomBox.rightX,
			/** @type {number} */ bottomY = zoomBox.bottomY,
			/** @type {number} */ topY = zoomBox.topY,

			// alive and dead states
			/** @type {number} */ alive = (this.multiNumStates > 2 ? this.multiNumStates + this.historyStates - 1 : LifeConstants.aliveStart),

			// counters
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0;

		// clear top boundary
		if ((ht - topY - 1) <= yrange) {
			for (y = ht - yrange - 1; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= rightX; x += 1) {
					if (colourRow[x] >= alive) {
						if (this.multiNumStates === 2) {
							this.removeMSPattern2(x, y);
						} else {
							this.removeMSPatternN(x, y);
						}
					}
				}
			}
			zoomBox.topY = ht - yrange - 1;
		}

		// clear bottom boundary
		if (bottomY <= yrange) {
			for (y = bottomY; y <= yrange; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= rightX; x += 1) {
					if (colourRow[x] >= alive) {
						if (this.multiNumStates === 2) {
							this.removeMSPattern2(x, y);
						} else {
							this.removeMSPatternN(x, y);
						}
					}
				}
			}
			zoomBox.bottomY = yrange;
		}

		// clear left boundary
		if (leftX <= xrange) {
			for (y = bottomY; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = leftX; x <= xrange; x += 1) {
					if (colourRow[x] >= alive) {
						if (this.multiNumStates === 2) {
							this.removeMSPattern2(x, y);
						} else {
							this.removeMSPatternN(x, y);
						}
					}
				}
			}
			zoomBox.leftX = xrange;
		}

		// clear right boundary
		if ((wd - rightX - 1) <= xrange) {
			for (y = bottomY; y <= topY; y += 1) {
				colourRow = colourGrid[y];
				for (x = wd - xrange - 1; x <= rightX; x += 1) {
					if (colourRow[x] >= alive) {
						if (this.multiNumStates === 2) {
							this.removeMSPattern2(x, y);
						} else {
							this.removeMSPatternN(x, y);
						}
					}
				}
			}
			zoomBox.rightX = wd - xrange - 1;
		}
	};

	// update the life grid region using tiles for triangular grid
	Life.prototype.nextGenerationTriTile = function() {
		var	/** @type {Uint8Array} */ indexLookup = this.indexLookupTri1,
			/** @type {Uint16Array} */ gridRow0 = null,
			/** @type {Uint16Array} */ gridRow1 = null,
			/** @type {Uint16Array} */ gridRow2 = null,
			/** @type {number} */ h = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ swap = 0,
			/** @type {number} */ val0 = 0,
			/** @type {number} */ val1 = 0,
			/** @type {number} */ val2 = 0,
			/** @type {number} */ output = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ nextGrid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// which cells were set in source
			/** @type {number} */ origValue = 0,

			// whether cells were set in the tile
			/** @type {number} */ tileCells = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// width of grid in 16 bit chunks
			/** @type {number} */ width16 = width >> 4,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16 bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows,

			// left, middle and right bitmasks for rule lookup
			/** @const{number} */ maskL = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8),
			/** @const{number} */ maskC = (1 << 7) | (1 << 6) | (1 << 5),
			/** @const{number} */ maskR = (1 << 4) | (1 << 3) | (1 << 2) | (1 << 1) | (1 << 0),

			// bit counts for population
			/** @type {Uint8Array} */ bitCounts16 = this.bitCounts16,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			nextGrid = this.grid16;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;

			// get alternate lookup buffer if specified
			if (this.altSpecified) {
				indexLookup = this.indexLookupTri2;
			}
		} else {
			grid = this.grid16;
			nextGrid = this.nextGrid16;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// check start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << b)) !== 0) {
							// mark no cells in this column
							colOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							h = bottomY;

							// deal with bottom row of the grid
							if (h === 0) {
								gridRow0 = this.blankRow16;
							} else {
								gridRow0 = grid[h - 1];
							}

							// current row
							gridRow1 = grid[h];

							// next row
							gridRow2 = grid[h + 1];

							// get original value (used for top row only and then to determine if any cells were alive in tile)
							origValue = gridRow1[leftX];

							// mix of original cells and cells computed in this tile
							tileCells = origValue;

							// process normal tile
							val0 = (gridRow0[leftX - 1] << 18) | (gridRow0[leftX] << 2) | (gridRow0[leftX + 1] >> 14);
							val1 = (gridRow1[leftX - 1] << 18) | (origValue << 2) | (gridRow1[leftX + 1] >> 14);
							val2 = (gridRow2[leftX - 1] << 18) | (gridRow2[leftX] << 2) | (gridRow2[leftX + 1] >> 14);

							// get output
							output = indexLookup[((val0 >> 7) & maskL) | ((val2 >> 11) & maskC) | ((val1 >> 15) & maskR)] << 15;
							output |= indexLookup[((val2 >> 6) & maskL) | ((val0 >> 10) & maskC) | ((val1 >> 14) & maskR)] << 14;
							output |= indexLookup[((val0 >> 5) & maskL) | ((val2 >> 9) & maskC) | ((val1 >> 13) & maskR)] << 13;
							output |= indexLookup[((val2 >> 4) & maskL) | ((val0 >> 8) & maskC) | ((val1 >> 12) & maskR)] << 12;
							output |= indexLookup[((val0 >> 3) & maskL) | ((val2 >> 7) & maskC) | ((val1 >> 11) & maskR)] << 11;
							output |= indexLookup[((val2 >> 2) & maskL) | ((val0 >> 6) & maskC) | ((val1 >> 10) & maskR)] << 10;
							output |= indexLookup[((val0 >> 1) & maskL) | ((val2 >> 5) & maskC) | ((val1 >> 9) & maskR)] << 9;
							output |= indexLookup[((val2 >> 0) & maskL) | ((val0 >> 4) & maskC) | ((val1 >> 8) & maskR)] << 8;
							output |= indexLookup[((val0 << 1) & maskL) | ((val2 >> 3) & maskC) | ((val1 >> 7) & maskR)] << 7;
							output |= indexLookup[((val2 << 2) & maskL) | ((val0 >> 2) & maskC) | ((val1 >> 6) & maskR)] << 6;
							output |= indexLookup[((val0 << 3) & maskL) | ((val2 >> 1) & maskC) | ((val1 >> 5) & maskR)] << 5;
							output |= indexLookup[((val2 << 4) & maskL) | ((val0 >> 0) & maskC) | ((val1 >> 4) & maskR)] << 4;
							output |= indexLookup[((val0 << 5) & maskL) | ((val2 << 1) & maskC) | ((val1 >> 3) & maskR)] << 3;
							output |= indexLookup[((val2 << 6) & maskL) | ((val0 << 2) & maskC) | ((val1 >> 2) & maskR)] << 2;
							output |= indexLookup[((val0 << 7) & maskL) | ((val2 << 3) & maskC) | ((val1 >> 1) & maskR)] << 1;
							output |= indexLookup[((val2 << 8) & maskL) | ((val0 << 4) & maskC) | ((val1 >> 0) & maskR)] << 0;

							// save output 16bits
							nextGrid[h][leftX] = output;

							// update statistics
							population += bitCounts16[output];
							births += bitCounts16[output & ~origValue];
							deaths += bitCounts16[origValue & ~output];

							// check if any cells are set
							if (output) {
								// update column occupied flag
								colOccupied |= output;

								// update min and max row
								if (h < newBottomY) {
									newBottomY = h;
								}
								if (h > newTopY) {
									newTopY = h;
								}

								// check for left column now set (need two cells)
								if ((output & 49152) !== 0) {
									neighbours |= LifeConstants.bottomLeftSet;
								}

								// check for right column now set (need two cells)
								if ((output & 3) !== 0) {
									neighbours |= LifeConstants.bottomRightSet;
								}

								// bottom row set
								neighbours |= LifeConstants.bottomSet;
							}

							// process middle rows of the tile
							h += 1;
							while (h < topY - 1) {
								// get original value for next row
								origValue = gridRow2[leftX];
								tileCells |= origValue;

								// next row
								gridRow2 = grid[h + 1];

								// read three rows
								val0 = val1;
								val1 = val2;
								val2 = (gridRow2[leftX - 1] << 18) | (gridRow2[leftX] << 2) | (gridRow2[leftX + 1] >> 14);

								// check for odd/even
								if ((h & 1) !== 0) {
									swap = val0;
									val0 = val2;
									val2 = swap;
								}

								// get output
								output = indexLookup[((val0 >> 7) & maskL) | ((val2 >> 11) & maskC) | ((val1 >> 15) & maskR)] << 15;
								output |= indexLookup[((val2 >> 6) & maskL) | ((val0 >> 10) & maskC) | ((val1 >> 14) & maskR)] << 14;
								output |= indexLookup[((val0 >> 5) & maskL) | ((val2 >> 9) & maskC) | ((val1 >> 13) & maskR)] << 13;
								output |= indexLookup[((val2 >> 4) & maskL) | ((val0 >> 8) & maskC) | ((val1 >> 12) & maskR)] << 12;
								output |= indexLookup[((val0 >> 3) & maskL) | ((val2 >> 7) & maskC) | ((val1 >> 11) & maskR)] << 11;
								output |= indexLookup[((val2 >> 2) & maskL) | ((val0 >> 6) & maskC) | ((val1 >> 10) & maskR)] << 10;
								output |= indexLookup[((val0 >> 1) & maskL) | ((val2 >> 5) & maskC) | ((val1 >> 9) & maskR)] << 9;
								output |= indexLookup[((val2 >> 0) & maskL) | ((val0 >> 4) & maskC) | ((val1 >> 8) & maskR)] << 8;
								output |= indexLookup[((val0 << 1) & maskL) | ((val2 >> 3) & maskC) | ((val1 >> 7) & maskR)] << 7;
								output |= indexLookup[((val2 << 2) & maskL) | ((val0 >> 2) & maskC) | ((val1 >> 6) & maskR)] << 6;
								output |= indexLookup[((val0 << 3) & maskL) | ((val2 >> 1) & maskC) | ((val1 >> 5) & maskR)] << 5;
								output |= indexLookup[((val2 << 4) & maskL) | ((val0 >> 0) & maskC) | ((val1 >> 4) & maskR)] << 4;
								output |= indexLookup[((val0 << 5) & maskL) | ((val2 << 1) & maskC) | ((val1 >> 3) & maskR)] << 3;
								output |= indexLookup[((val2 << 6) & maskL) | ((val0 << 2) & maskC) | ((val1 >> 2) & maskR)] << 2;
								output |= indexLookup[((val0 << 7) & maskL) | ((val2 << 3) & maskC) | ((val1 >> 1) & maskR)] << 1;
								output |= indexLookup[((val2 << 8) & maskL) | ((val0 << 4) & maskC) | ((val1 >> 0) & maskR)] << 0;

								// update statistics
								population += bitCounts16[output];
								births += bitCounts16[output & ~origValue];
								deaths += bitCounts16[origValue & ~output];

								// check for odd/even
								if ((h & 1) !== 0) {
									swap = val0;
									val0 = val2;
									val2 = swap;
								}

								// save output 16bits
								nextGrid[h][leftX] = output;

								// check if any cells are set
								if (output) {
									// update column occupied flag
									colOccupied |= output;

									// update min and max row
									if (h < newBottomY) {
										newBottomY = h;
									}
									if (h > newTopY) {
										newTopY = h;
									}
								}

								// next row
								h += 1;
							}

							// get original value
							origValue = gridRow2[leftX];
							tileCells |= origValue;

							// deal with top row
							if (h === this.height - 1) {
								gridRow2 = this.blankRow16;
							} else {
								gridRow2 = grid[h + 1];
							}

							// read three rows
							val0 = val1;
							val1 = val2;
							val2 = (gridRow2[leftX - 1] << 18) | (gridRow2[leftX] << 2) | (gridRow2[leftX + 1] >> 14);

							// check for odd/even
							if ((h & 1) !== 0) {
								swap = val0;
								val0 = val2;
								val2 = swap;
							}

							// get output
							output = indexLookup[((val0 >> 7) & maskL) | ((val2 >> 11) & maskC) | ((val1 >> 15) & maskR)] << 15;
							output |= indexLookup[((val2 >> 6) & maskL) | ((val0 >> 10) & maskC) | ((val1 >> 14) & maskR)] << 14;
							output |= indexLookup[((val0 >> 5) & maskL) | ((val2 >> 9) & maskC) | ((val1 >> 13) & maskR)] << 13;
							output |= indexLookup[((val2 >> 4) & maskL) | ((val0 >> 8) & maskC) | ((val1 >> 12) & maskR)] << 12;
							output |= indexLookup[((val0 >> 3) & maskL) | ((val2 >> 7) & maskC) | ((val1 >> 11) & maskR)] << 11;
							output |= indexLookup[((val2 >> 2) & maskL) | ((val0 >> 6) & maskC) | ((val1 >> 10) & maskR)] << 10;
							output |= indexLookup[((val0 >> 1) & maskL) | ((val2 >> 5) & maskC) | ((val1 >> 9) & maskR)] << 9;
							output |= indexLookup[((val2 >> 0) & maskL) | ((val0 >> 4) & maskC) | ((val1 >> 8) & maskR)] << 8;
							output |= indexLookup[((val0 << 1) & maskL) | ((val2 >> 3) & maskC) | ((val1 >> 7) & maskR)] << 7;
							output |= indexLookup[((val2 << 2) & maskL) | ((val0 >> 2) & maskC) | ((val1 >> 6) & maskR)] << 6;
							output |= indexLookup[((val0 << 3) & maskL) | ((val2 >> 1) & maskC) | ((val1 >> 5) & maskR)] << 5;
							output |= indexLookup[((val2 << 4) & maskL) | ((val0 >> 0) & maskC) | ((val1 >> 4) & maskR)] << 4;
							output |= indexLookup[((val0 << 5) & maskL) | ((val2 << 1) & maskC) | ((val1 >> 3) & maskR)] << 3;
							output |= indexLookup[((val2 << 6) & maskL) | ((val0 << 2) & maskC) | ((val1 >> 2) & maskR)] << 2;
							output |= indexLookup[((val0 << 7) & maskL) | ((val2 << 3) & maskC) | ((val1 >> 1) & maskR)] << 1;
							output |= indexLookup[((val2 << 8) & maskL) | ((val0 << 4) & maskC) | ((val1 >> 0) & maskR)] << 0;

							// update statistics
							population += bitCounts16[output];
							births += bitCounts16[output & ~origValue];
							deaths += bitCounts16[origValue & ~output];

							// check for odd/even
							if ((h & 1) !== 0) {
								swap = val0;
								val0 = val2;
								val2 = swap;
							}

							// save output 16bits
							nextGrid[h][leftX] = output;

							// check if any cells are set
							if (output) {
								// update column occupied flag
								colOccupied |= output;

								// update min and max row
								if (h < newBottomY) {
									newBottomY = h;
								}
								if (h > newTopY) {
									newTopY = h;
								}

								// check for left column now set
								if ((output & 49152) !== 0) {
									neighbours |= LifeConstants.topLeftSet;
								}

								// check for right column now set
								if ((output & 3) !== 0) {
									neighbours |= LifeConstants.topRightSet;
								}

								// top row set
								neighbours |= LifeConstants.topSet;
							}

							// check which columns contained cells
							if (colOccupied) {
								// check for left column set (need two cells)
								if ((colOccupied & 49152) !== 0) {
									neighbours |= LifeConstants.leftSet;
								}

								// check for right column set (need two cells)
								if ((colOccupied & 3) !== 0) {
									neighbours |= LifeConstants.rightSet;
								}
							}

							// save the column occupied cells
							columnOccupied16[leftX] |= colOccupied;

							// check if the source or output were alive
							if (colOccupied || tileCells) {
								// update
								nextTiles |= (1 << b);

								// check for neighbours
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (b < 15) {
											nextTiles |= (1 << (b + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (b > 0) {
											nextTiles |= (1 << (b - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << b);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << b);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (b < 15) {
											belowNextTiles |= (1 << (b + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (b > 0) {
											belowNextTiles |= (1 << (b - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (b < 15) {
											aboveNextTiles |= (1 << (b + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (b > 0) {
											aboveNextTiles |= (1 << (b - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		for (tw = 0; tw < width16; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for Margolus grid
	Life.prototype.nextGenerationMargolusTile = function() {
		var	/** @type {Uint16Array} */ indexLookup = this.margolusLookup1,
			/** @type {number} */ h = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ val0 = 0,
			/** @type {number} */ val1 = 0,
			/** @type {number} */ output = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ output0 = 0,
			/** @type {number} */ output1 = 0,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ nextGrid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// which cells were set in source
			/** @type {number} */ origValue = 0,

			// whether on right tile column
			/** @type {boolean} */ rightTile = false,

			// column occupied
			columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// width of grid in 16 bit chunks
			/** @type {number} */ width16 = width >> 4,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16 bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0,

			// bit counts for population
			/** @type {Uint8Array} */ bitCounts16 = this.bitCounts16,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// counter for even/odd
			/** @type {number} */ counter = this.counter & 1;

		// check for change in playback direction
		if (this.reversePending) {
			this.reverseMargolus = !this.reverseMargolus;
			this.reversePending = false;
		}

		// switch buffers each generation
		if (counter !== 0) {
			grid = this.nextGrid16;
			nextGrid = this.grid16;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;

			// check for reverse playback
			if (this.reverseMargolus) {
				if (this.altSpecified) {
					// use reverse 1
					indexLookup = this.margolusReverseLookup1;
				} else {
					// always use reverse 1 if alt not specified
					indexLookup = this.margolusReverseLookup1;
				}
			}
		} else {
			grid = this.grid16;
			nextGrid = this.nextGrid16;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
			if (this.altSpecified) {
				indexLookup = this.margolusLookup2;
			}

			// check for reverse playback
			if (this.reverseMargolus) {
				if (this.altSpecified) {
					// use reverse 2
					indexLookup = this.margolusReverseLookup2;
				} else {
					// always use reverse 1 if alt not specified
					indexLookup = this.margolusReverseLookup1;
				}
			}
		}

		// invert odd/even for reverse playback
		if (this.reverseMargolus) {
			counter = 1 - counter;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// set the initial tile row
		bottomY = 1 - counter;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (b = 15; b >= 0; b -= 1) {
						// check if on right tile
						if (b === 0 && tw === tileCols16 - 1) {
							rightTile = true;
						} else {
							rightTile = false;
						}

						// check if this tile needs computing
						if ((tiles & (1 << b)) !== 0) {
							// mark no cells in this column
							colOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// check for even/odd phase
							if (counter !== 0) {
								// even phase
								// process bottom row
								h = bottomY;

								// get original value for next two rows
								val0 = grid[h][leftX];
								val1 = grid[h + 1][leftX];
								origValue = (val0 | val1);

								// get output
								output = indexLookup[(val0 & 65280) | (val1 >> 8)];
								output0 = output & 65280;
								output1 = (output & 255) << 8;
								output = indexLookup[(val0 & 255) << 8 | (val1 & 255)];
								output0 |= (output >> 8);
								output1 |= (output & 255);

								// save output 16bits
								nextGrid[h][leftX] = output0;
								nextGrid[h + 1][leftX] = output1;

								// update statistics
								population += bitCounts16[output0];
								births += bitCounts16[output0 & ~val0];
								deaths += bitCounts16[val0 & ~output0];
								population += bitCounts16[output1];
								births += bitCounts16[output1 & ~val1];
								deaths += bitCounts16[val1 & ~output1];

								// check if any cells are set
								output = output0 | output1;
								if (output) {
									// update column occupied flag
									colOccupied |= output;

									// update min and max row
									if (output0) {
										if (h < newBottomY) {
											newBottomY = h;
										}
										if (h > newTopY) {
											newTopY = h;
										}
									}
									if (output1) {
										if (h + 1 < newBottomY) {
											newBottomY = h + 1;
										}
										if (h + 1 > newTopY) {
											newTopY = h + 1;
										}
									}

									// check for left column now set
									if ((output & 49152) !== 0) {
										neighbours |= LifeConstants.bottomLeftSet;
									}

									// check for right column now set
									if ((output & 3) !== 0) {
										neighbours |= LifeConstants.bottomRightSet;
									}

									// bottom row set
									neighbours |= LifeConstants.bottomSet;
								}

								// process middle rows of the tile
								h += 2;
								while (h < topY - 2) {
									// get original value for next two rows
									val0 = grid[h][leftX];
									val1 = grid[h + 1][leftX];
									origValue |= (val0 | val1);

									// get output
									output = indexLookup[(val0 & 65280) | (val1 >> 8)];
									output0 = output & 65280;
									output1 = (output & 255) << 8;
									output = indexLookup[(val0 & 255) << 8 | (val1 & 255)];
									output0 |= (output >> 8);
									output1 |= (output & 255);

									// save output 16bits
									nextGrid[h][leftX] = output0;
									nextGrid[h + 1][leftX] = output1;

									// update statistics
									population += bitCounts16[output0];
									births += bitCounts16[output0 & ~val0];
									deaths += bitCounts16[val0 & ~output0];
									population += bitCounts16[output1];
									births += bitCounts16[output1 & ~val1];
									deaths += bitCounts16[val1 & ~output1];

									// check if any cells are set
									output = output0 | output1;
									if (output) {
										// update column occupied flag
										colOccupied |= output;

										// update min and max row
										if (output0) {
											if (h < newBottomY) {
												newBottomY = h;
											}
											if (h > newTopY) {
												newTopY = h;
											}
										}
										if (output1) {
											if (h + 1 < newBottomY) {
												newBottomY = h + 1;
											}
											if (h + 1 > newTopY) {
												newTopY = h + 1;
											}
										}
									}

									// next row
									h += 2;
								}

								// process top row
								val0 = grid[h][leftX];
								val1 = grid[h + 1][leftX];
								origValue |= (val0 | val1);

								// get output
								output = indexLookup[(val0 & 65280) | (val1 >> 8)];
								output0 = output & 65280;
								output1 = (output & 255) << 8;
								output = indexLookup[(val0 & 255) << 8 | (val1 & 255)];
								output0 |= (output >> 8);
								output1 |= (output & 255);

								// save output 16bits
								nextGrid[h][leftX] = output0;
								nextGrid[h + 1][leftX] = output1;

								// update statistics
								population += bitCounts16[output0];
								births += bitCounts16[output0 & ~val0];
								deaths += bitCounts16[val0 & ~output0];
								population += bitCounts16[output1];
								births += bitCounts16[output1 & ~val1];
								deaths += bitCounts16[val1 & ~output1];

								// check if any cells are set
								output = output0 | output1;
								if (output) {
									// update column occupied flag
									colOccupied |= output;

									// update min and max row
									if (output0) {
										if (h < newBottomY) {
											newBottomY = h;
										}
										if (h > newTopY) {
											newTopY = h;
										}
									}
									if (output1) {
										if (h + 1 < newBottomY) {
											newBottomY = h + 1;
										}
										if (h + 1 > newTopY) {
											newTopY = h + 1;
										}
									}

									// check for left column now set
									if (output) {
										if ((output & 49152) !== 0) {
											neighbours |= LifeConstants.topLeftSet;
										}

										// check for right column now set
										if ((output & 3) !== 0) {
											neighbours |= LifeConstants.topRightSet;
										}

										// top row set
										neighbours |= LifeConstants.topSet;
									}
								}

								// check which columns contained cells
								if (colOccupied) {
									// check for right column set in this tile or left column set in right hand tile
									if ((colOccupied & 49152) !== 0) {
										neighbours |= LifeConstants.leftSet;
									}
									if ((colOccupied & 3) !== 0) {
										neighbours |= LifeConstants.rightSet;
									}

									// save the column occupied cells
									columnOccupied16[leftX] |= colOccupied;
								}

								// if there were new cells or original cells then ensure right and top right get processed next generation
								if (colOccupied || origValue) {
									neighbours |= LifeConstants.topRightSet;
									neighbours |= LifeConstants.rightSet;
								}
							} else {
								// odd phase
								// process bottom row
								h = bottomY;

								// check for right hand tile
								if (rightTile) {
									// get original value for next two rows
									val0 = (grid[h][leftX] << 1) & 65535;
									val1 = (grid[h + 1][leftX] << 1) & 65535;
								} else {
									// get original value for next two rows
									val0 = ((grid[h][leftX] << 1) | (grid[h][leftX + 1] >> 15)) & 65535;
									val1 = ((grid[h + 1][leftX] << 1) | (grid[h + 1][leftX + 1] >> 15)) & 65535;
								}
								origValue = (val0 | val1);

								// get output
								output = indexLookup[(val0 & 65280) | (val1 >> 8)];
								output0 = output & 65280;
								output1 = (output & 255) << 8;
								output = indexLookup[(val0 & 255) << 8 | (val1 & 255)];
								output0 |= (output >> 8);
								output1 |= (output & 255);

								// save output 16bits
								if (rightTile) {
									nextGrid[h][leftX] = (nextGrid[h][leftX] & 32768) | (output0 >> 1);
									nextGrid[h + 1][leftX] = (nextGrid[h + 1][leftX] & 32768) | (output1 >> 1);
								} else {
									nextGrid[h][leftX] = (nextGrid[h][leftX] & 32768) | (output0 >> 1);
									nextGrid[h][leftX + 1] = (nextGrid[h][leftX + 1] & 32767) | ((output0 & 1) << 15);
									nextGrid[h + 1][leftX] = (nextGrid[h + 1][leftX] & 32768) | (output1 >> 1);
									nextGrid[h + 1][leftX + 1] = (nextGrid[h + 1][leftX + 1] & 32767) | ((output1 & 1) << 15);
								}

								// update statistics
								population += bitCounts16[output0];
								births += bitCounts16[output0 & ~val0];
								deaths += bitCounts16[val0 & ~output0];
								population += bitCounts16[output1];
								births += bitCounts16[output1 & ~val1];
								deaths += bitCounts16[val1 & ~output1];

								// check if any cells are set
								output = output0 | output1;
								if (output) {
									// update column occupied flag
									colOccupied |= output;

									// update min and max row
									if (output0) {
										if (h < newBottomY) {
											newBottomY = h;
										}
										if (h > newTopY) {
											newTopY = h;
										}
									}
									if (output1) {
										if (h + 1 < newBottomY) {
											newBottomY = h + 1;
										}
										if (h + 1 > newTopY) {
											newTopY = h + 1;
										}
									}

									// there is no neighbour below since this is the odd phase
								}

								// process middle rows of the tile
								h += 2;
								while (h < topY - 2) {
									// get original value for next two rows
									if (rightTile) {
										val0 = (grid[h][leftX] << 1) & 65535;
										val1 = (grid[h + 1][leftX] << 1)  & 65535;
									} else {
										val0 = ((grid[h][leftX] << 1) | (grid[h][leftX + 1] >> 15)) & 65535;
										val1 = ((grid[h + 1][leftX] << 1) | (grid[h + 1][leftX + 1] >> 15)) & 65535;
									}
									origValue |= (val0 | val1);

									// get output
									output = indexLookup[(val0 & 65280) | (val1 >> 8)];
									output0 = output & 65280;
									output1 = (output & 255) << 8;
									output = indexLookup[(val0 & 255) << 8 | (val1 & 255)];
									output0 |= (output >> 8);
									output1 |= (output & 255);

									// save output 16bits
									if (rightTile) {
										nextGrid[h][leftX] = (nextGrid[h][leftX] & 32768) | (output0 >> 1);
										nextGrid[h + 1][leftX] = (nextGrid[h + 1][leftX] & 32768) | (output1 >> 1);
									} else {
										nextGrid[h][leftX] = (nextGrid[h][leftX] & 32768) | (output0 >> 1);
										nextGrid[h][leftX + 1] = (nextGrid[h][leftX + 1] & 32767) | ((output0 & 1) << 15);
										nextGrid[h + 1][leftX] = (nextGrid[h + 1][leftX] & 32768) | (output1 >> 1);
										nextGrid[h + 1][leftX + 1] = (nextGrid[h + 1][leftX + 1] & 32767) | ((output1 & 1) << 15);
									}

									// update statistics
									population += bitCounts16[output0];
									births += bitCounts16[output0 & ~val0];
									deaths += bitCounts16[val0 & ~output0];
									population += bitCounts16[output1];
									births += bitCounts16[output1 & ~val1];
									deaths += bitCounts16[val1 & ~output1];

									// check if any cells are set
									output = output0 | output1;
									if (output) {
										// update column occupied flag
										colOccupied |= output;

										// update min and max row
										if (output0) {
											if (h < newBottomY) {
												newBottomY = h;
											}
											if (h > newTopY) {
												newTopY = h;
											}
										}
										if (output1) {
											if (h + 1 < newBottomY) {
												newBottomY = h + 1;
											}
											if (h + 1 > newTopY) {
												newTopY = h + 1;
											}
										}
									}

									// next row
									h += 2;
								}

								// process top row
								if (rightTile) {
									val0 = (grid[h][leftX] << 1) & 65535;
									val1 = (grid[h + 1][leftX] << 1) & 65535;
								} else {
									val0 = ((grid[h][leftX] << 1) | (grid[h][leftX + 1] >> 15)) & 65535;
									val1 = ((grid[h + 1][leftX] << 1) | (grid[h + 1][leftX + 1] >> 15)) & 65535;
								}
								origValue |= (val0 | val1);

								// get output
								output = indexLookup[(val0 & 65280) | (val1 >> 8)];
								output0 = output & 65280;
								output1 = (output & 255) << 8;
								output = indexLookup[(val0 & 255) << 8 | (val1 & 255)];
								output0 |= (output >> 8);
								output1 |= (output & 255);

								// save output 16bits
								if (rightTile) {
									nextGrid[h][leftX] = (nextGrid[h][leftX] & 32768) | (output0 >> 1);
									nextGrid[h + 1][leftX] = (nextGrid[h + 1][leftX] & 32768) | (output1 >> 1);
								} else {
									nextGrid[h][leftX] = (nextGrid[h][leftX] & 32768) | (output0 >> 1);
									nextGrid[h][leftX + 1] = (nextGrid[h][leftX + 1] & 32767) | ((output0 & 1) << 15);
									nextGrid[h + 1][leftX] = (nextGrid[h + 1][leftX] & 32768) | (output1 >> 1);
									nextGrid[h + 1][leftX + 1] = (nextGrid[h + 1][leftX + 1] & 32767) | ((output1 & 1) << 15);
								}

								// update statistics
								population += bitCounts16[output0];
								births += bitCounts16[output0 & ~val0];
								deaths += bitCounts16[val0 & ~output0];
								population += bitCounts16[output1];
								births += bitCounts16[output1 & ~val1];
								deaths += bitCounts16[val1 & ~output1];

								// check if any cells are set
								output = output0 | output1;
								if (output) {
									// update column occupied flag
									colOccupied |= output;

									// update min and max row
									if (output0) {
										if (h < newBottomY) {
											newBottomY = h;
										}
										if (h > newTopY) {
											newTopY = h;
										}
									}
									if (output1) {
										if (h + 1 < newBottomY) {
											newBottomY = h + 1;
										}
										if (h + 1 > newTopY) {
											newTopY = h + 1;
										}
									}

									// check for right column set in this tile or left column set in right hand tile
									if ((output & 3) !== 0) {
										neighbours |= LifeConstants.topRightSet;
									}

									// top row set
									neighbours |= LifeConstants.topSet;
								}

								if (val0 | val1) {
									if (((val0 | val1) & 3) !== 0) {
										neighbours |= LifeConstants.topRightSet;
									}
									neighbours |= LifeConstants.topSet;
								}

								// check which columns contained cells
								if (colOccupied) {
									// check for right column set in this tile or left column set in right hand tile
									if ((colOccupied & 3) !== 0) {
										neighbours |= LifeConstants.rightSet;
									}

									// save the column occupied cells
									columnOccupied16[leftX] |= colOccupied >> 1;
									columnOccupied16[leftX + 1] |= ((colOccupied & 1) << 15);
								}

								if ((origValue & 3) !== 0) {
									neighbours |= LifeConstants.rightSet;
								}
							}

							// check if the source or output were alive
							if (colOccupied || origValue) {
								// update
								nextTiles |= (1 << b);

								// check for neighbours
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (b < 15) {
											nextTiles |= (1 << (b + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (b > 0) {
											nextTiles |= (1 << (b - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << b);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << b);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (b < 15) {
											belowNextTiles |= (1 << (b + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (b > 0) {
											belowNextTiles |= (1 << (b - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (b < 15) {
											aboveNextTiles |= (1 << (b + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (b > 0) {
											aboveNextTiles |= (1 << (b - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							}

							// update tiles that died
							if (colOccupied === 0 && origValue) {
								diedTiles |= 1 << b;
							}
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}


				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
			if (topY >= height) {
				topY = height - 1;
			}
		}

		// update bounding box
		for (tw = 0; tw < width16; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied16(grid);

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid for Life-like rules
	Life.prototype.nextGenerationTile = function() {
		var	/** @type {number} */ timing = performance.now();

		if (Controller.useWASM && Controller.wasmEnableNextGeneration && this.view.wasmEnabled) {
			WASM.nextGeneration(
				this.grid16.whole.byteOffset | 0,
				this.nextGrid16.whole.byteOffset | 0,
				this.grid16[0].length | 0,
				this.tileGrid.whole.byteOffset | 0,
				this.nextTileGrid.whole.byteOffset | 0,
				this.tileGrid[0].length | 0,
				this.indexLookup63.byteOffset | 0,
				(this.altSpecified ? this.indexLookup632.byteOffset : 0) | 0,
				(this.altSpecified ? 1 : 0) | 0,
				this.columnOccupied16.byteOffset | 0,
				this.columnOccupied16.length | 0,
				this.rowOccupied16.byteOffset | 0,
				this.rowOccupied16.length | 0,
				this.width | 0,
				this.height | 0,
				this.tileX | 0,
				this.tileY | 0,
				this.tileRows | 0,
				this.tileCols | 0,
				this.blankTileRow.byteOffset | 0,
				this.blankTileRow.length | 0,
				this.blankRow16.byteOffset | 0,
				this.boundedGridWidth | 0,
				this.boundedGridHeight | 0,
				this.boundedGridType | 0,
				this.counter | 0,
				LifeConstants.bottomRightSet | 0,
				LifeConstants.bottomSet | 0,
				LifeConstants.topRightSet | 0,
				LifeConstants.topSet | 0,
				LifeConstants.bottomLeftSet | 0,
				LifeConstants.topLeftSet | 0,
				LifeConstants.leftSet | 0,
				LifeConstants.rightSet | 0,
				this.tileGrid.whole.byteLength | 0,
				this.sharedBuffer.byteOffset | 0
			);

			this.population = this.sharedBuffer[0];
			this.births = this.sharedBuffer[1];
			this.deaths = this.sharedBuffer[2];
			this.zoomBox.leftX = this.sharedBuffer[3];
			this.zoomBox.bottomY = this.sharedBuffer[4];
			this.zoomBox.rightX = this.sharedBuffer[5];
			this.zoomBox.topY = this.sharedBuffer[6];
		} else {
			this.nextGenerationTileJS();
		}

		timing = performance.now() - timing;
		if (Controller.wasmTiming) {
			this.view.menuManager.updateTimingItem("nextGeneration", timing, Controller.useWASM && Controller.wasmEnableNextGeneration && this.view.wasmEnabled);
		}
	};

	// update the life grid region using tiles Javascript version
	Life.prototype.nextGenerationTileJS = function() {
		var	/** @type {Uint8Array} */ indexLookup63 = this.indexLookup63,
			/** @type {Uint16Array} */ gridRow0 = null,
			/** @type {Uint16Array} */ gridRow1 = null,
			/** @type {Uint16Array} */ gridRow2 = null,
			/** @type {number} */ h = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ val0 = 0,
			/** @type {number} */ val1 = 0,
			/** @type {number} */ val2 = 0,
			/** @type {number} */ output = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Array<Uint16Array>} */ nextGrid = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// which cells were set in source
			/** @type {number} */ origValue = 0,

			// whether cells were set in the tile
			/** @type {number} */ tileCells = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {Uint8Array} */ bitCounts16 = this.bitCounts16,
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// width of grid in 16 bit chunks
			/** @type {number} */ width16 = width >> 4,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16 bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0,

			// bounded grid width and height
			/** @type {number} */ bWidth = this.boundedGridWidth,
			/** @type {number} */ bHeight = this.boundedGridHeight,

			// bottom left
			/** @type {number} */ bLeftX = Math.round((this.width - bWidth) / 2),
			/** @type {number} */ bBottomY = Math.round((this.height - bHeight) / 2),

			// top right
			/** @type {number} */ bRightX = bLeftX + bWidth - 1,
			/** @type {number} */ bTopY = bBottomY + bHeight - 1;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			nextGrid = this.grid16;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;

			// get alternate lookup buffer if specified
			if (this.altSpecified) {
				indexLookup63 = this.indexLookup632;
			}
		} else {
			grid = this.grid16;
			nextGrid = this.nextGrid16;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << b)) !== 0) {
							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							h = bottomY;
							rowIndex = 32768;

							// deal with bottom row of the grid
							if (h === 0) {
								gridRow0 = this.blankRow16;
							} else {
								gridRow0 = grid[h - 1];
							}

							// current row
							gridRow1 = grid[h];

							// next row
							gridRow2 = grid[h + 1];

							// get original value (used for top row only and then to determine if any cells were alive in tile)
							origValue = gridRow1[leftX];

							// mix of original cells and cells computed in this tile
							tileCells = origValue;

							// check if at left edge of grid
							if (!leftX) {
								// process left edge tile first row
								val0 = (gridRow0[leftX] << 1) | (gridRow0[leftX + 1] >> 15);
								val1 = (origValue << 1) | (gridRow1[leftX + 1] >> 15);
								val2 = (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
								output = val0 | val1 | val2;
								if (output) {
									// get first 4 bits
									output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

									// add three sets of 4 bits
									output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
									output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
									output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

									// check if any cells are set
									if (output) {
										// update row and column occupied flags
										colOccupied |= output;
										rowOccupied |= rowIndex;

										// check for right column now set
										if ((output & 1) !== 0) {
											neighbours |= LifeConstants.bottomRightSet;
										}

										// bottom row set
										neighbours |= LifeConstants.bottomSet;

										// update population
										population += bitCounts16[output];
									}
								}

								// save output 16bits
								nextGrid[h][leftX] = output;

								// update statistics
								if (output | origValue) {
									births += bitCounts16[output & ~origValue];
									deaths += bitCounts16[origValue & ~output];
								}

								// process left edge tile middle rows
								h += 1;
								rowIndex >>= 1;
								while (h < topY - 1) {
									// get original value for next row
									origValue = gridRow2[leftX];
									tileCells |= origValue;

									// next row
									gridRow2 = grid[h + 1];

									// read three rows
									val0 = val1;
									val1 = val2;
									val2 = (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										// get first 4 bits
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

										// add three sets of 4 bits
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

										// check if any cells are set
										if (output) {
											// update row and column occupied flags
											colOccupied |= output;
											rowOccupied |= rowIndex;

											// update population
											population += bitCounts16[output];
										}
									}

									// save output 16bits
									nextGrid[h][leftX] = output;

									// update statistics
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}

									// next row
									h += 1;
									rowIndex >>= 1;
								}

								// process left edge last row
								origValue = gridRow2[leftX];
								tileCells |= origValue;

								// deal with top row
								if (h === this.height - 1) {
									gridRow2 = this.blankRow16;
								} else {
									gridRow2 = grid[h + 1];
								}

								// read three rows
								val0 = val1;
								val1 = val2;
								val2 = (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
								output = val0 | val1 | val2;
								if (output) {
									// get first 4 bits
									output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

									// get next 4 bits
									output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
									output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
									output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

									// check if any cells are set
									if (output) {
										// update row and column occupied flags
										colOccupied |= output;
										rowOccupied |= rowIndex;

										// update population
										population += bitCounts16[output];

										// check for right column now set
										if ((output & 1) !== 0) {
											neighbours |= LifeConstants.topRightSet;
										}

										// top row set
										neighbours |= LifeConstants.topSet;
									}
								}

								// save output 16bits
								nextGrid[h][leftX] = output;

								// update statistics
								if (output | origValue) {
									births += bitCounts16[output & ~origValue];
									deaths += bitCounts16[origValue & ~output];
								}
							} else {
								// check if at right edge
								if (leftX >= width16 - 1) {
									// process right edge tile first row
									val0 = ((gridRow0[leftX - 1] & 1) << 17) | (gridRow0[leftX] << 1);
									val1 = ((gridRow1[leftX - 1] & 1) << 17) | (origValue << 1);
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1);
									output = val0 | val1 | val2;
									if (output) {
										// get first 4 bits
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

										// add three sets of 4 bits
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

										// check if any cells are set
										if (output) {
											// update row and column occupied flags
											colOccupied |= output;
											rowOccupied |= rowIndex;

											// update population
											population += bitCounts16[output];

											// check for left column now set
											if ((output & 32768) !== 0) {
												neighbours |= LifeConstants.bottomLeftSet;
											}

											// bottom row set
											neighbours |= LifeConstants.bottomSet;
										}
									}

									// save output 16bits
									nextGrid[h][leftX] = output;

									// update statistics
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}

									// process left edge tile middle rows
									h += 1;
									rowIndex >>= 1;
									while (h < topY - 1) {
										// get original value for next row
										origValue = gridRow2[leftX];
										tileCells |= origValue;

										// next row
										gridRow2 = grid[h + 1];

										// read three rows
										val0 = val1;
										val1 = val2;
										val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1);
										output = val0 | val1 | val2;
										if (output) {
											// get first 4 bits
											output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

											// add three sets of 4 bits
											output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
											output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
											output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

											// check if any cells are set
											if (output) {
												// update row and column occupied flags
												colOccupied |= output;
												rowOccupied |= rowIndex;

												// update population
												population += bitCounts16[output];
											}
										}

										// save output 16bits
										nextGrid[h][leftX] = output;

										// update statistics
										if (output | origValue) {
											births += bitCounts16[output & ~origValue];
											deaths += bitCounts16[origValue & ~output];
										}

										// next row
										h += 1;
										rowIndex >>= 1;
									}

									// process left edge last row
									origValue = gridRow2[leftX];
									tileCells |= origValue;

									// deal with top row
									if (h === this.height - 1) {
										gridRow2 = this.blankRow16;
									} else {
										gridRow2 = grid[h + 1];
									}

									// read three rows
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1);
									output = val0 | val1 | val2;
									if (output) {
										// get first 4 bits
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

										// get next 4 bits
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

										// check if any cells are set
										if (output) {
											// update row and column occupied flags
											colOccupied |= output;
											rowOccupied |= rowIndex;

											// update population
											population += bitCounts16[output];

											// check for left column now set
											if ((output & 32768) !== 0) {
													neighbours |= LifeConstants.topLeftSet;
											}

											// top row set
											neighbours |= LifeConstants.topSet;
										}
									}

									// save output 16bits
									nextGrid[h][leftX] = output;

									// update statistics
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
								} else {
									// process normal tile
									val0 = ((gridRow0[leftX - 1] & 1) << 17) | (gridRow0[leftX] << 1) | (gridRow0[leftX + 1] >> 15);
									val1 = ((gridRow1[leftX - 1] & 1) << 17) | (origValue << 1) | (gridRow1[leftX + 1] >> 15);
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										// get first 4 bits
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

										// add three sets of 4 bits
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

										// check if any cells are set
										if (output) {
											// update row and column occupied flags
											colOccupied |= output;
											rowOccupied |= rowIndex;

											// update population
											population += bitCounts16[output];

											// check for left column now set
											if ((output & 32768) !== 0) {
												neighbours |= LifeConstants.bottomLeftSet;
											}

											// check for right column now set
											if ((output & 1) !== 0) {
												neighbours |= LifeConstants.bottomRightSet;
											}

											// bottom row set
											neighbours |= LifeConstants.bottomSet;
										}
									}

									// save output 16bits
									nextGrid[h][leftX] = output;

									// update statistics
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}

									// process middle rows of the tile
									h += 1;
									rowIndex >>= 1;

									// get original value for next row
									origValue = gridRow2[leftX];
									tileCells |= origValue;

									// next row
									gridRow2 = grid[h + 1];

									// read three rows
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										// get first 4 bits
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

										// get next 4 bits
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

										// check if any cells are set
										if (output) {
											// update row and column occupied flags
											colOccupied |= output;
											rowOccupied |= rowIndex;

											// update population
											population += bitCounts16[output];
										}
									}

									// save output 16bits
									nextGrid[h][leftX] = output;

									// update statistics
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}

									// next row
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// loop unroll
									origValue = gridRow2[leftX];
									tileCells |= origValue;
									gridRow2 = grid[h + 1];
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];
										if (output) {
											colOccupied |= output;
											rowOccupied |= rowIndex;
											population += bitCounts16[output];
										}
									}
									nextGrid[h][leftX] = output;
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
									h += 1;
									rowIndex >>= 1;

									// get original value
									origValue = gridRow2[leftX];
									tileCells |= origValue;

									// deal with top row
									if (h === this.height - 1) {
										gridRow2 = this.blankRow16;
									} else {
										gridRow2 = grid[h + 1];
									}

									// read three rows
									val0 = val1;
									val1 = val2;
									val2 = ((gridRow2[leftX - 1] & 1) << 17) | (gridRow2[leftX] << 1) | (gridRow2[leftX + 1] >> 15);
									output = val0 | val1 | val2;
									if (output) {
										// get first 4 bits
										output = indexLookup63[(val0 >> 12) | (val1 >> 12) << 6 | (val2 & 258048)] << 12;

										// get next 4 bits
										output |= indexLookup63[((val0 >> 8) & 63) | ((val1 >> 2) & 4032) | ((val2 << 4) & 258048)] << 8;
										output |= indexLookup63[((val0 >> 4) & 63) | ((val1 << 2) & 4032) | ((val2 << 8) & 258048)] << 4;
										output |= indexLookup63[(val0 & 63) | (val1 & 63) << 6 | (val2 & 63) << 12];

										// check if any cells are set
										if (output) {
											// update row and column occupied flag
											colOccupied |= output;
											rowOccupied |= rowIndex;

											// update population
											population += bitCounts16[output];

											// check for left column now set
											if ((output & 32768) !== 0) {
												neighbours |= LifeConstants.topLeftSet;
											}

											// check for right column now set
											if ((output & 1) !== 0) {
												neighbours |= LifeConstants.topRightSet;
											}

											// top row set
											neighbours |= LifeConstants.topSet;
										}
									}

									// save output 16bits
									nextGrid[h][leftX] = output;

									// update statistics
									if (output | origValue) {
										births += bitCounts16[output & ~origValue];
										deaths += bitCounts16[origValue & ~output];
									}
								}
							}

							// check which columns contained cells
							if (colOccupied) {
								// check for left column set
								if ((colOccupied & 32768) !== 0) {
									neighbours |= LifeConstants.leftSet;
								}

								// check for right column set
								if ((colOccupied & 1) !== 0) {
									neighbours |= LifeConstants.rightSet;
								}
							}

							// save the column occupied cells
							columnOccupied16[leftX] |= colOccupied;

							// check if the source or output were alive
							if (colOccupied || tileCells) {
								// update
								nextTiles |= (1 << b);

								// check for neighbours
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (b < 15) {
											nextTiles |= (1 << (b + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (b > 0) {
											nextTiles |= (1 << (b - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << b);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << b);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (b < 15) {
											belowNextTiles |= (1 << (b + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (b > 0) {
											belowNextTiles |= (1 << (b - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (b < 15) {
											aboveNextTiles |= (1 << (b + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (b > 0) {
											aboveNextTiles |= (1 << (b - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width16 - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// remove bounded grid column and row entries (+1 in all directions)
		if (this.boundedGridType !== -1) {
			if (bWidth !== 0) {
				columnOccupied16[(bLeftX - 1) >> 4] &= ~(1 << (~(bLeftX - 1) & 15));
				columnOccupied16[(bRightX + 1) >> 4] &= ~(1 << (~(bRightX + 1) & 15));
				columnOccupied16[(bLeftX - 2) >> 4] &= ~(1 << (~(bLeftX - 2) & 15));
				columnOccupied16[(bRightX + 2) >> 4] &= ~(1 << (~(bRightX + 2) & 15));
			}
			if (bHeight !== 0) {
				rowOccupied16[(bBottomY - 1) >> 4] &= ~(1 << (~(bBottomY - 1) & 15));
				rowOccupied16[(bTopY + 1) >> 4] &= ~(1 << (~(bTopY + 1) & 15));
				rowOccupied16[(bBottomY - 2) >> 4] &= ~(1 << (~(bBottomY - 2) & 15));
				rowOccupied16[(bTopY + 2) >> 4] &= ~(1 << (~(bTopY + 2) & 15));
			}
		}

		// update bounding box
		for (tw = 0; tw < width16; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}
		}

		for (th = 0; th < rowOccupied16.length; th += 1) {
			if (rowOccupied16[th]) {
				if (th < newBottomY) {
					newBottomY = th;
				}
				if (th > newTopY) {
					newTopY = th;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// convert new height to pixels
		newBottomY = (newBottomY << 4) + this.leftBitOffset16(rowOccupied16[newBottomY]);
		newTopY = (newTopY << 4) + this.rightBitOffset16(rowOccupied16[newTopY]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// create 2x2 colour grid for 0.5 <= zoom < 1 for [R]Super patterns
	Life.prototype.create2x2ColourGrid16Super = function(/** @type {Array<Uint16Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ cr = 0,
			/** @type {number} */ h = 0,
			/** @type {Uint16Array} */ sourceRow = null,
			/** @type {Uint16Array} */ sourceRow1 = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ value = 0,
			/** @type {number} */ smallValue = 0,
			/** @type {number} */ temp = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							for (h = bottomY; h < topY; h += 2) {
								// get destination row
								destRow = smallColourGrid[h];

								// get the next two rows
								sourceRow = colourGrid[h];
								sourceRow1 = colourGrid[h + 1];
								cr = (leftX << 3);

								// get the maximum of 4 pixels
								// first two pixels in first row
								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));

								// next two pixels in next row
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

								// loop unroll x 7
								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

								value = sourceRow[cr];
								value |= (value & 0x0101) << 5;
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								value |= (value & 0x0101) << 5;
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = (smallValue - (temp & (temp >> 255))) & 31;
								cr += 1;

							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 2x2 colour grid for 0.5 <= zoom < 1
	Life.prototype.create2x2ColourGrid16 = function(/** @type {Array<Uint16Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ cr = 0,
			/** @type {number} */ h = 0,
			/** @type {Uint16Array} */ sourceRow = null,
			/** @type {Uint16Array} */ sourceRow1 = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ value = 0,
			/** @type {number} */ smallValue = 0,
			/** @type {number} */ temp = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							for (h = bottomY; h < topY; h += 2) {
								// get destination row
								destRow = smallColourGrid[h];

								// get the next two rows
								sourceRow = colourGrid[h];
								sourceRow1 = colourGrid[h + 1];
								cr = (leftX << 3);

								// get the maximum of 4 pixels
								// first two pixels in first row
								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));

								// next two pixels in next row
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;

								// loop unroll x 7
								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;

								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;

								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;

								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;

								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;

								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;

								value = sourceRow[cr];
								smallValue = value & 255;
								temp = smallValue - (value >> 8);
								smallValue -= (temp & (temp >> 255));
								value = sourceRow1[cr];
								temp = smallValue - (value & 255);
								smallValue -= (temp & (temp >> 255));
								temp = smallValue - (value >> 8);
								destRow[cr + cr] = smallValue - (temp & (temp >> 255));
								cr += 1;
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 4x4 colour grid for 0.25 <= zoom < 0.5
	Life.prototype.create4x4ColourGrid32Super = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {number} */ i = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint32Array} */ sourceRow1 = null,
			/** @type {Uint32Array} */ sourceRow2 = null,
			/** @type {Uint32Array} */ sourceRow3 = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,
			/** @type {number} */ temp = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							for (h = bottomY; h < topY; h += 4) {
								// get destination row
								destRow = smallColourGrid[h];

								// get the next four rows
								sourceRow = colourGrid[h];
								sourceRow1 = colourGrid[h + 1];
								sourceRow2 = colourGrid[h + 2];
								sourceRow3 = colourGrid[h + 3];
								cr = (leftX << 2);
								dr = (leftX << 4);

								// get the maximum of each 4x4 block
								for (i = 0; i < 4; i += 1) {
									smallValue = 0;

									value = sourceRow[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow1[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow2[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow3[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									destRow[dr] = smallValue & 31;
									cr += 1;
									dr += 4;
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 4x4 colour grid for 0.25 <= zoom < 0.5
	Life.prototype.create4x4ColourGrid32 = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {number} */ i = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint32Array} */ sourceRow1 = null,
			/** @type {Uint32Array} */ sourceRow2 = null,
			/** @type {Uint32Array} */ sourceRow3 = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							for (h = bottomY; h < topY; h += 4) {
								// get destination row
								destRow = smallColourGrid[h];

								// get the next four rows
								sourceRow = colourGrid[h];
								sourceRow1 = colourGrid[h + 1];
								sourceRow2 = colourGrid[h + 2];
								sourceRow3 = colourGrid[h + 3];
								cr = (leftX << 2);
								dr = (leftX << 4);

								// get the maximum of each 4x4 block
								for (i = 0; i < 4; i += 1) {
									smallValue = 0;

									value = sourceRow[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow1[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow2[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow3[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									destRow[dr] = smallValue;
									cr += 1;
									dr += 4;
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 8x8 colour grid for 0.125 <= zoom < 0.25
	Life.prototype.create8x8ColourGrid32Super = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {number} */ i = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint32Array} */ sourceRow1 = null,
			/** @type {Uint32Array} */ sourceRow2 = null,
			/** @type {Uint32Array} */ sourceRow3 = null,
			/** @type {Uint32Array} */ sourceRow4 = null,
			/** @type {Uint32Array} */ sourceRow5 = null,
			/** @type {Uint32Array} */ sourceRow6 = null,
			/** @type {Uint32Array} */ sourceRow7 = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,
			/** @type {number} */ temp = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							for (h = bottomY; h < topY; h += 8) {
								// get destination row
								destRow = smallColourGrid[h];

								// get the next 8 rows
								sourceRow = colourGrid[h];
								sourceRow1 = colourGrid[h + 1];
								sourceRow2 = colourGrid[h + 2];
								sourceRow3 = colourGrid[h + 3];
								sourceRow4 = colourGrid[h + 4];
								sourceRow5 = colourGrid[h + 5];
								sourceRow6 = colourGrid[h + 6];
								sourceRow7 = colourGrid[h + 7];
								cr = (leftX << 2);
								dr = (leftX << 4);

								// get the maximum of each 8x8 block
								for (i = 0; i < 2; i += 1) {
									smallValue = 0;
									value = sourceRow[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow1[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow1[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow2[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow2[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow3[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow3[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow4[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow4[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow5[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow5[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow6[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow6[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow7[cr];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									value = sourceRow7[cr + 1];
									if (value) {
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
										value >>= 8;
										temp = smallValue - ((value & 255) | ((value & 1) << 5));
										smallValue -= (temp & (temp >> 255));
									}

									destRow[dr] = smallValue & 31;
									cr += 2;
									dr += 8;
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 8x8 colour grid for 0.125 <= zoom < 0.25
	Life.prototype.create8x8ColourGrid32 = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {number} */ i = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint32Array} */ sourceRow1 = null,
			/** @type {Uint32Array} */ sourceRow2 = null,
			/** @type {Uint32Array} */ sourceRow3 = null,
			/** @type {Uint32Array} */ sourceRow4 = null,
			/** @type {Uint32Array} */ sourceRow5 = null,
			/** @type {Uint32Array} */ sourceRow6 = null,
			/** @type {Uint32Array} */ sourceRow7 = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							for (h = bottomY; h < topY; h += 8) {
								// get destination row
								destRow = smallColourGrid[h];

								// get the next 8 rows
								sourceRow = colourGrid[h];
								sourceRow1 = colourGrid[h + 1];
								sourceRow2 = colourGrid[h + 2];
								sourceRow3 = colourGrid[h + 3];
								sourceRow4 = colourGrid[h + 4];
								sourceRow5 = colourGrid[h + 5];
								sourceRow6 = colourGrid[h + 6];
								sourceRow7 = colourGrid[h + 7];
								cr = (leftX << 2);
								dr = (leftX << 4);

								// get the maximum of each 8x8 block
								for (i = 0; i < 2; i += 1) {
									smallValue = 0;
									value = sourceRow[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow1[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow1[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow2[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow2[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow3[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow3[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow4[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow4[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow5[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow5[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow6[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow6[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow7[cr];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									value = sourceRow7[cr + 1];
									if (value) {
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
										value >>= 8;
										smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									}

									destRow[dr] = smallValue;
									cr += 2;
									dr += 8;
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 16x16 colour grid for 0.0625 <= zoom < 0.125
	Life.prototype.create16x16ColourGrid32Super = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,
			/** @type {number} */ temp = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							smallValue = 0;
							for (h = bottomY; h < topY; h += 1) {
								// get the next row
								sourceRow = colourGrid[h];
								cr = (leftX << 2);

								// get the maximum of 16 cells
								value = sourceRow[cr];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 1];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 2];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 3];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}
							}

							// get the destination row
							destRow = smallColourGrid[bottomY];
							dr = (leftX << 4);
							destRow[dr] = smallValue & 31;
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 16x16 colour grid for 0.0625 <= zoom < 0.125
	Life.prototype.create16x16ColourGrid32 = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// update the small colour grid
							smallValue = 0;
							for (h = bottomY; h < topY; h += 1) {
								// get the next row
								sourceRow = colourGrid[h];
								cr = (leftX << 2);

								// get the maximum of 16 cells
								value = sourceRow[cr];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 1];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 2];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 3];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}
							}

							// get the destination row
							destRow = smallColourGrid[bottomY];
							dr = (leftX << 4);
							destRow[dr] = smallValue;
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// create 16x16 colour grid for zoom < 0.0625
	Life.prototype.create32x32ColourGrid32Super = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow1 = null,
			/** @type {Uint16Array} */ colourTileHistoryRow2 = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,
			/** @type {number} */ temp =  0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial two tile rows
		bottomY = 0;
		topY = bottomY + ySize + ySize;

		// scan each row pair of tiles
		for (th = 0; th < tileRows; th += 2) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow1 = colourTileHistoryGrid[th];
			colourTileHistoryRow2 = colourTileHistoryGrid[th + 1];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next two tile groups (16 tiles each)
				tiles = colourTileHistoryRow1[tw] | colourTileHistoryRow2[tw];

				// check if any are occupied
				if (tiles !== 0) {
					// compute next colour for each tile in the set
					for (b = (1 << 15) | (1 << 14); b > 0; b >>= 2) {
						// check if this tile is occupied
						if ((tiles & b) !== 0) {
							// update the small colour grid
							smallValue = 0;
							for (h = bottomY; h < topY; h += 1) {
								// get the next row
								sourceRow = colourGrid[h];
								cr = (leftX << 2);

								// get the maximum of 32 cells
								value = sourceRow[cr];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 1];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 2];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 3];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 4];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 5];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 6];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}

								value = sourceRow[cr + 7];
								if (value) {
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
									value >>= 8;
									temp = smallValue - ((value & 255) | ((value & 1) << 5));
									smallValue -= (temp & (temp >> 255));
								}
							}

							// get the destination row
							destRow = smallColourGrid[bottomY];
							dr = (leftX << 4);
							destRow[dr] = smallValue & 31;
						}

						// next tile columns
						leftX += xSize + xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row pair
			bottomY += ySize + ySize;
			topY += ySize + ySize;
		}
	};

	// create 16x16 colour grid for zoom < 0.0625
	Life.prototype.create32x32ColourGrid32 = function(/** @type {Array<Uint32Array>} */ colourGrid, /** @type {Array<Uint8Array>} */ smallColourGrid) {
		var	/** @type {number} */ cr = 0,
			/** @type {number} */ dr = 0,
			/** @type {Uint32Array} */ sourceRow = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Uint16Array} */ colourTileHistoryRow1 = null,
			/** @type {Uint16Array} */ colourTileHistoryRow2 = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ smallValue = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4;

		// set the initial two tile rows
		bottomY = 0;
		topY = bottomY + ySize + ySize;

		// scan each row pair of tiles
		for (th = 0; th < tileRows; th += 2) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow1 = colourTileHistoryGrid[th];
			colourTileHistoryRow2 = colourTileHistoryGrid[th + 1];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next two tile groups (16 tiles each)
				tiles = colourTileHistoryRow1[tw] | colourTileHistoryRow2[tw];

				// check if any are occupied
				if (tiles !== 0) {
					// compute next colour for each tile in the set
					for (b = (1 << 15) | (1 << 14); b > 0; b >>= 2) {
						// check if this tile is occupied
						if ((tiles & b) !== 0) {
							// update the small colour grid
							smallValue = 0;
							for (h = bottomY; h < topY; h += 1) {
								// get the next row
								sourceRow = colourGrid[h];
								cr = (leftX << 2);

								// get the maximum of 32 cells
								value = sourceRow[cr];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 1];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 2];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 3];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 4];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 5];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 6];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}

								value = sourceRow[cr + 7];
								if (value) {
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
									value >>= 8;
									smallValue = smallValue - ((smallValue - (value & 255)) & ((smallValue - (value & 255)) >> 255));
								}
							}

							// get the destination row
							destRow = smallColourGrid[bottomY];
							dr = (leftX << 4);
							destRow[dr] = smallValue;
						}

						// next tile columns
						leftX += xSize + xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row pair
			bottomY += ySize + ySize;
			topY += ySize + ySize;
		}
	};

	// clear the small colour grid if last zoom < 1/16x
	Life.prototype.clearSmallGridOnZoom = function(/** @type {Array<Uint8Array>} */ smallGrid) {
		if (this.lastZoom16) {
			smallGrid.whole.fill(0);
		}
	};

	// create the small colour grids based on zoom level
	Life.prototype.createSmallColourGrids = function(/** @type {Array<Uint16Array>} */ colourGrid16, /** @type {Array<Uint32Array>} */ colourGrid32, /** @type {number} */ camZoom) {
		var	/** @type {number} */ timing = performance.now(),
			/** @type {string} */ fName = "";

		// check if 0.5 <= zoom < 1
		if (camZoom >= 0.5 && camZoom < 1) {
			// create 2x2 colour grid
			fName = "max02x02";
			this.clearSmallGridOnZoom(this.smallColourGrid);

			if (this.isSuper) {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create2x2ColourGridSuper(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create2x2ColourGrid16Super(colourGrid16, this.smallColourGrid);
				}
			} else {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create2x2ColourGrid(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create2x2ColourGrid16(colourGrid16, this.smallColourGrid);
				}
			}
		} else if (camZoom >= 0.25 && camZoom < 0.5) {
			// create 4x4 colour grid
			fName = "max04x04";
			this.clearSmallGridOnZoom(this.smallColourGrid);

			if (this.isSuper) {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create4x4ColourGridSuper(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create4x4ColourGrid32Super(colourGrid32, this.smallColourGrid);
				}
			} else {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create4x4ColourGrid(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create4x4ColourGrid32(colourGrid32, this.smallColourGrid);
				}
			}
		} else if (camZoom >= 0.125 && camZoom < 0.25) {
			// create 8x8 colour grid
			fName = "max08x08";
			this.clearSmallGridOnZoom(this.smallColourGrid);

			if (this.isSuper) {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create8x8ColourGridSuper(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create8x8ColourGrid32Super(colourGrid32, this.smallColourGrid);
				}
			} else {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create8x8ColourGrid(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create8x8ColourGrid32(colourGrid32, this.smallColourGrid);
				}
			}
		} else if (camZoom >= 0.0625 && camZoom < 0.125) {
			// create 16x16 colour grid
			fName = "max16x16";
			this.clearSmallGridOnZoom(this.smallColourGrid);

			if (this.isSuper) {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create16x16ColourGridSuper(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create16x16ColourGrid32Super(colourGrid32, this.smallColourGrid);
				}
			} else {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create16x16ColourGrid(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create16x16ColourGrid32(colourGrid32, this.smallColourGrid);
				}
			}
		} else {
			// zoom < 0.0625
			fName = "max32x32";

			if (this.isSuper) {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create32x32ColourGridSuper(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create32x32ColourGrid32Super(colourGrid32, this.smallColourGrid);
				}
			} else {
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create32x32ColourGrid(
						colourGrid32.whole.byteOffset | 0,
						this.smallColourGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						colourGrid32[0].length | 0
					);
				} else {
					this.create32x32ColourGrid32(colourGrid32, this.smallColourGrid);
				}
			}
		}

		// check for overlay
		if (this.drawOverlay) {
			// check if 0.5 <= zoom < 1
			if (camZoom >= 0.5 && camZoom < 1) {
				// create 2x2 colour grid
				this.clearSmallGridOnZoom(this.smallOverlayGrid);
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create2x2ColourGrid(
						this.overlayGrid32.whole.byteOffset | 0,
						this.smallOverlayGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.overlayGrid32[0].length | 0
					);
				} else {
					this.create2x2ColourGrid16(this.overlayGrid16, this.smallOverlayGrid);
				}
			} else if (camZoom >= 0.25 && camZoom < 0.5) {
				// create 4x4 colour grid
				this.clearSmallGridOnZoom(this.smallOverlayGrid);
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create4x4ColourGrid(
						this.overlayGrid32.whole.byteOffset | 0,
						this.smallOverlayGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.overlayGrid32[0].length | 0
					);
				} else {
					this.create4x4ColourGrid32(this.overlayGrid32, this.smallOverlayGrid);
				}
			} else if (camZoom >= 0.125 && camZoom < 0.25) {
				// create 8x8 colour grid
				this.clearSmallGridOnZoom(this.smallOverlayGrid);
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create8x8ColourGrid(
						this.overlayGrid32.whole.byteOffset | 0,
						this.smallOverlayGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.overlayGrid32[0].length | 0
					);
				} else {
					this.create8x8ColourGrid32(this.overlayGrid32, this.smallOverlayGrid);
				}
			} else if (camZoom >= 0.0625 && camZoom < 0.125) {
				// create 16x16 colour grid
				this.clearSmallGridOnZoom(this.smallOverlayGrid);
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create16x16ColourGrid(
						this.overlayGrid32.whole.byteOffset | 0,
						this.smallOverlayGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.overlayGrid32[0].length | 0
					);
				} else {
					this.create16x16ColourGrid32(this.overlayGrid32, this.smallOverlayGrid);
				}
			} else {
				// createa 32x32 colour grid
				if (Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled) {
					WASM.create32x32ColourGrid(
						this.overlayGrid32.whole.byteOffset | 0,
						this.smallOverlayGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileY | 0,
						this.tileX | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.overlayGrid32[0].length | 0
					);
				} else {
					this.create32x32ColourGrid32(this.overlayGrid32, this.smallOverlayGrid);
				}
			}
		}

		timing = performance.now() - timing;
		if (Controller.wasmTiming) {
			this.view.menuManager.updateTimingItem(fName, timing, Controller.useWASM && Controller.wasmEnableCreateSmallGrids && this.view.wasmEnabled);
		}

		// if zoom was 32x then mark it as so for clear when it changes
		if (camZoom < 0.0625) {
			this.lastZoom16 = true;
		} else {
			this.lastZoom16 = false;
		}
	};

	// update bounding box after next generation for RuleLoader and Extended rules
	Life.prototype.updateBoundingBox = function(/** @type {Uint16Array} */ columnOccupied16, /** @type {Uint16Array} */ rowOccupied16, /** @type {number} */ newLeftX, /** @type {number} */ newBottomY, /** @type {number} */ newRightX, /** @type {number} */ newTopY, /** @type {BoundingBox} */ zoomBox) {
		var	/** @type {number} */ tw = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ width = this.width,
			/** @type {number} */ height = this.height;

		// update bounding box
		for (tw = 0; tw < columnOccupied16.length; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}
		}

		for (th = 0; th < rowOccupied16.length; th += 1) {
			if (rowOccupied16[th]) {
				if (th < newBottomY) {
					newBottomY = th;
				}
				if (th > newTopY) {
					newTopY = th;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// convert new height to pixels
		newBottomY = (newBottomY << 4) + this.leftBitOffset16(rowOccupied16[newBottomY]);
		newTopY = (newTopY << 4) + this.rightBitOffset16(rowOccupied16[newTopY]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;
	};

	// clear tiles that died
	Life.prototype.clearTilesThatDied = function(/** @type {Array<Uint32Array>} */ grid32) {
		var	/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @const {number} */ xSize = this.tileY,  // use height since we need bytes
			/** @const {number} */ ySize = this.tileY,
			/** @const {number} */ tileCols16 = this.tileCols >> 4,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {Uint32Array} */ gridRow32 = null,
			/** @type {Uint16Array} */ diedRow,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ bit = 0;

		// clear tiles that died in source
		bottomY = 0;
		topY = bottomY + ySize;

		// process each tile row
		for (th = 0; th < diedGrid.length; th += 1) {
			leftX = 0;
			diedRow = diedGrid[th];

			// process each tile group in the row
			for (tw = 0; tw < tileCols16; tw += 1) {
				diedTiles = diedRow[tw];

				// process each tile in the group
				if (diedTiles) {
					for (bit = 15; bit >= 0; bit -= 1) {
						if (diedTiles & (1 << bit)) {
							// clear source cells for double buffering
							x = leftX >> 2;
							for (y = bottomY; y < topY; y += 1) {
								gridRow32 = grid32[y];

								// clear 16 cells
								gridRow32[x] = 0;
								gridRow32[x + 1] = 0;
								gridRow32[x + 2] = 0;
								gridRow32[x + 3] = 0;
							}

						}
						leftX += xSize;
					}
				} else {
					leftX += xSize << 4;
				}
			}
			bottomY += ySize;
			topY += ySize;
		}
	};

	// clear tiles that died on the bit grid
	Life.prototype.clearTilesThatDied16 = function(/** @type {Array<Uint16Array>} */ grid16) {
		var	/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @const {number} */ xSize = this.tileY,  // use height since we need bytes
			/** @const {number} */ ySize = this.tileY,
			/** @const {number} */ tileCols16 = this.tileCols >> 4,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {Uint16Array} */ gridRow16 = null,
			/** @type {Uint16Array} */ diedRow,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ bit = 0;

		// clear tiles that died in source
		bottomY = 0;
		topY = bottomY + ySize;

		// process each tile row
		for (th = 0; th < diedGrid.length; th += 1) {
			leftX = 0;
			diedRow = diedGrid[th];

			// process each tile group in the row
			for (tw = 0; tw < tileCols16; tw += 1) {
				diedTiles = diedRow[tw];

				// process each tile in the group
				if (diedTiles) {
					for (bit = 15; bit >= 0; bit -= 1) {
						if (diedTiles & (1 << bit)) {
							// clear source cells for double buffering
							x = leftX;
							for (y = bottomY; y < topY; y += 1) {
								gridRow16 = grid16[y];

								// clear 16 cells
								gridRow16[x] = 0;
							}

						}
						leftX += 1;
					}
				} else {
					leftX += xSize;
				}
			}
			bottomY += ySize;
			topY += ySize;
		}
	};

	// compute extended rule next generation for von Neumann neighbourhood
	Life.prototype.nextGenerationExtendedTileVN = function() {
		var	/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ state = 0,
			/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ width = this.width,
			/** @type {number} */ height = this.height,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newBottomY = height,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,
			/** @type {number} */ population = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,

			// mask of types in the neighbourhood
			/** @type {number} */ typeMask = 0,

			// cells in the neighbourhood
			/** @type {number} */ n = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ e = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// lookup table
			/** @type {Uint8Array} */ ruleArray = this.manager.ruleArray,
			/** @type {number} */ treat = 0,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0,

			// flags if cells are alive
			/** @type {number} */ oldCellWasAlive = 0,

			// constants
			/** @const {number} */ deadForcer = (1 << 2) | (1 << 3) | (1 << 6) | (1 << 7) | (1 << 14) | (1 << 16),
			/** @const {number} */ birthForcer = (1 << 8) | (1 << 9) | (1 << 12) | (1 << 13) | (1 << 14),
			/** @const {number} */ requireState1 = (1 << 15) | (1 << 16),
			/** @const {number} */ treatIfDead = (1 << 1) | (1 << 2) | (1 << 4) | (1 << 6) | (1 << 8) | (1 << 10) | (1 << 12) | (1 << 15) |  (1 << 16) | (1 << 17) | (1 << 19),
			/** @const {number} */ treatIfAlive = treatIfDead ^ ((1 << 17) | (1 << 18) | (1 << 19) | (1 << 20)),
			/** @const {Array<number>} */ nextState = this.nextStateExtended;

		// select the correct grid
		if ((this.counter & 1) === 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;

			// get alternate lookup buffer if specified
			if (this.altSpecified) {
				ruleArray = this.manager.ruleAltArray;
			}
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;
			rightX = leftX + xSize;

			// get the colour tile rows
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next colour for each tile in the set
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// get initial neighbours
								if (x === 0) {
									c = 0;
								} else {
									c = gridRow1[x - 1];
								}
								e = gridRow1[x];

								// process each cell in the chunk
								while (x < rightX - 1) {
									// shift neighbourhood left
									n = gridRow0[x];
									w = c;
									c = e;
									e = gridRow1[x + 1];
									s = gridRow2[x];

									// check for higher states
									if (c >= 2) {
										state = nextState[c];
									} else {
										// typemask has a bit set per state in the neighbouring cells
										typeMask = (1 << n) | (1 << w) | (1 << e) | (1 << s);

										// check for all cells dead
										if (typeMask === 1 && c === 0) {
											state = 0;
										} else {
											if (typeMask & (c ? deadForcer : birthForcer)) {
												state = 1 - c;
											} else {
												if (!c && (typeMask & requireState1) && !(typeMask & 2)) {
													state = 0;
												} else {
													treat = c ? treatIfAlive : treatIfDead;
													state = ruleArray[
															(((treat >> s) & 1) << 7) |
															(((treat >> w) & 1) << 5) |
															(c << 4) |
															(((treat >> e) & 1) << 3) |
															(((treat >> n) & 1) << 1)];
												}
											}
										}
									}

									// check if state is alive
									nextRow[x] = state;
									oldCellWasAlive = c ? 1 : 0;
									if (state > 0) {
										population += 1;

										// update births
										births += 1 - oldCellWasAlive;
									} else {
										// update deaths
										deaths += oldCellWasAlive;
									}

									// upadte tile occupancy
									if (state > 0) {
										rowOccupied |= rowIndex;
										colOccupied |= colIndex;
									}

									// check if any cell was alive in the source
									anyAlive |= c;

									// next column
									colIndex >>= 1;
									x += 1;
								}

								// handle right edge
								n = gridRow0[x];
								w = c;
								c = e;
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];

								// check for higher states
								if (c >= 2) {
									state = nextState[c];
								} else {
									// typemask has a bit set per state in the neighbouring cells
									typeMask = (1 << n) | (1 << w) | (1 << e) | (1 << s);

									// check for all cells dead
									if (typeMask === 1 && c === 0) {
										state = 0;
									} else {
										if (typeMask & (c ? deadForcer : birthForcer)) {
											state = 1 - c;
										} else {
											if (!c && (typeMask & requireState1) && !(typeMask & 2)) {
												state = 0;
											} else {
												treat = c ? treatIfAlive : treatIfDead;
												state = ruleArray[
														(((treat >> s) & 1) << 7) |
														(((treat >> w) & 1) << 5) |
														(c << 4) |
														(((treat >> e) & 1) << 3) |
														(((treat >> n) & 1) << 1)];
											}
										}
									}
								}

								// check if state is alive
								nextRow[x] = state;
								oldCellWasAlive = c ? 1 : 0;
								if (state > 0) {
									population += 1;

									// update births
									births += 1 - oldCellWasAlive;
								} else {
									// update deaths
									deaths += oldCellWasAlive;
								}

								// update tile occupancy
								if (state > 0) {
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								}

								// check if any cell was alive in the source
								anyAlive |= c;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// update the column and row occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any source cells were alive
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
						rightX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
					rightX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// compute extended rule next generation for Hex neighbourhood
	Life.prototype.nextGenerationExtendedTileHex = function() {
		var	/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ state = 0,
			/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ width = this.width,
			/** @type {number} */ height = this.height,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newBottomY = height,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,
			/** @type {number} */ population = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,

			// mask of types in the neighbourhood
			/** @type {number} */ typeMask = 0,

			// cells in the neighbourhood
			/** @type {number} */ nw = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// lookup table
			/** @type {Uint8Array} */ ruleArray = this.manager.ruleArray,
			/** @type {number} */ treat = 0,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0,

			// flags if cells are alive
			/** @type {number} */ oldCellWasAlive = 0,

			// constants
			/** @const {number} */ deadForcer = (1 << 2) | (1 << 3) | (1 << 6) | (1 << 7) | (1 << 14) | (1 << 16),
			/** @const {number} */ birthForcer = (1 << 8) | (1 << 9) | (1 << 12) | (1 << 13) | (1 << 14),
			/** @const {number} */ requireState1 = (1 << 15) | (1 << 16),
			/** @const {number} */ treatIfDead = (1 << 1) | (1 << 2) | (1 << 4) | (1 << 6) | (1 << 8) | (1 << 10) | (1 << 12) | (1 << 15) |  (1 << 16) | (1 << 17) | (1 << 19),
			/** @const {number} */ treatIfAlive = treatIfDead ^ ((1 << 17) | (1 << 18) | (1 << 19) | (1 << 20)),
			/** @const {Array<number>} */ nextState = [0, 1, 2, 3, 4, 5, 7, 6, 8, 9, 11, 10, 13, 12, 14, 15, 16, 17, 18, 20, 19];

		// select the correct grid
		if ((this.counter & 1) === 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;

			// get alternate lookup buffer if specified
			if (this.altSpecified) {
				ruleArray = this.manager.ruleArray;
			}
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;

			if (this.altSpecified) {
				ruleArray = this.manager.ruleAltArray;
			}
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;
			rightX = leftX + xSize;

			// get the colour tile rows
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next colour for each tile in the set
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// get initial neighbours
								if (x === 0) {
									c = 0;
									n = 0;
								} else {
									c = gridRow1[x - 1];
									n = gridRow0[x - 1];
								}
								se = gridRow2[x];
								e = gridRow1[x];

								// process each cell in the chunk
								while (x < rightX - 1) {
									// shift neighbourhood left
									nw = n;
									n = gridRow0[x];
									w = c;
									c = e;
									e = gridRow1[x + 1];
									s = se;
									se = gridRow2[x + 1];

									// check for higher states
									if (c >= 2) {
										state = nextState[c];
									} else {
										// typemask has a bit set per state in the neighbouring cells
										typeMask = (1 << nw) | (1 << n) | (1 << w) | (1 << e) | (1 << s) | (1 << se);

										// check for all cells dead
										if (typeMask === 1 && c === 0) {
											state = 0;
										} else {
											if (typeMask & (c ? deadForcer : birthForcer)) {
												state = 1 - c;
											} else {
												if (!c && (typeMask & requireState1) && !(typeMask & 2)) {
													state = 0;
												} else {
													treat = c ? treatIfAlive : treatIfDead;
													state = ruleArray[
															(((treat >> s) & 1) << 7) |
															(((treat >> se) & 1) << 6) |
															(((treat >> w) & 1) << 5) |
															(c << 4) |
															(((treat >> e) & 1) << 3) |
															(((treat >> nw) & 1) << 2) |
															(((treat >> n) & 1) << 1)];
												}
											}
										}
									}

									// check if state is alive
									nextRow[x] = state;
									oldCellWasAlive = c ? 1 : 0;
									if (state > 0) {
										population += 1;

										// update births
										births += 1 - oldCellWasAlive;
									} else {
										// update deaths
										deaths += oldCellWasAlive;
									}

									// update tile occupancy
									if (state > 0) {
										rowOccupied |= rowIndex;
										colOccupied |= colIndex;
									}

									// check if any cell was alive in the source
									anyAlive |= c;

									// next column
									colIndex >>= 1;
									x += 1;
								}

								// handle right edge
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								s = se;
								if (x === width - 1) {
									e = 0;
									se = 0;
								} else {
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}

								// check for higher states
								if (c >= 2) {
									state = nextState[c];
								} else {
									// typemask has a bit set per state in the neighbouring cells
									typeMask = (1 << nw) | (1 << n) | (1 << w) | (1 << e) | (1 << s) | (1 << se);

									// check for all cells dead
									if (typeMask === 1 && c === 0) {
										state = 0;
									} else {
										if (typeMask & (c ? deadForcer : birthForcer)) {
											state = 1 - c;
										} else {
											if (!c && (typeMask & requireState1) && !(typeMask & 2)) {
												state = 0;
											} else {
												treat = c ? treatIfAlive : treatIfDead;
												state = ruleArray[
														(((treat >> s) & 1) << 7) |
														(((treat >> se) & 1) << 6) |
														(((treat >> w) & 1) << 5) |
														(c << 4) |
														(((treat >> e) & 1) << 3) |
														(((treat >> nw) & 1) << 2) |
														(((treat >> n) & 1) << 1)];
											}
										}
									}
								}

								// check if state is alive
								nextRow[x] = state;
								oldCellWasAlive = c ? 1 : 0;
								if (state > 0) {
									population += 1;

									// update births
									births += 1 - oldCellWasAlive;
								} else {
									// update deaths
									deaths += oldCellWasAlive;
								}

								// update tile occupancy
								if (state > 0) {
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								}

								// check if any cell was alive in the source
								anyAlive |= c;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// update the column and row occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any source cells were alive
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
						rightX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
					rightX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// compute extended rule next generation for Moore neighbourhood
	Life.prototype.nextGenerationExtendedTileMoore = function() {
		var	/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ state = 0,
			/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ width = this.width,
			/** @type {number} */ height = this.height,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newBottomY = height,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,
			/** @type {number} */ population = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,

			// mask of types in the neighbourhood
			/** @type {number} */ typeMask = 0,

			// cells in the neighbourhood
			/** @type {number} */ nw = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ sw = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// lookup table
			/** @type {Uint8Array} */ ruleArray = this.manager.ruleArray,
			/** @type {number} */ treat = 0,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0,

			// flags if cells are alive
			/** @type {number} */ oldCellWasAlive = 0,

			// constants
			/** @const {number} */ deadForcer = (1 << 2) | (1 << 3) | (1 << 6) | (1 << 7) | (1 << 14) | (1 << 16),
			/** @const {number} */ birthForcer = (1 << 8) | (1 << 9) | (1 << 12) | (1 << 13) | (1 << 14),
			/** @const {number} */ requireState1 = (1 << 15) | (1 << 16),
			/** @const {number} */ treatIfDead = (1 << 1) | (1 << 2) | (1 << 4) | (1 << 6) | (1 << 8) | (1 << 10) | (1 << 12) | (1 << 15) |  (1 << 16) | (1 << 17) | (1 << 19),
			/** @const {number} */ treatIfAlive = treatIfDead ^ ((1 << 17) | (1 << 18) | (1 << 19) | (1 << 20)),
			/** @const {Array<number>} */ nextState = [0, 1, 2, 3, 4, 5, 7, 6, 8, 9, 11, 10, 13, 12, 14, 15, 16, 17, 18, 20, 19];

		// select the correct grid
		if ((this.counter & 1) === 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;

			// get alternate lookup buffer if specified
			if (this.altSpecified) {
				ruleArray = this.manager.ruleArray;
			}
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;

			// get alternate lookup buffer if specified
			if (this.altSpecified) {
				ruleArray = this.manager.ruleAltArray;
			}
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;
			rightX = leftX + xSize;

			// get the colour tile rows
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next colour for each tile in the set
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// get initial neighbours
								if (x === 0) {
									n = 0;
									c = 0;
									s = 0;
								} else {
									n = gridRow0[x - 1];
									c = gridRow1[x - 1];
									s = gridRow2[x - 1];
								}
								ne = gridRow0[x];
								e = gridRow1[x];
								se = gridRow2[x];

								// process each cell in the chunk
								while (x < rightX - 1) {
									// shift neighbourhood left
									nw = n;
									w = c;
									sw = s;
									n = ne;
									c = e;
									s = se;
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];

									// check for higher states
									if (c >= 2) {
										state = nextState[c];
									} else {
										// typemask has a bit set per state in the neighbouring cells
										typeMask = (1 << nw) | (1 << n) | (1 << ne) | (1 << w) | (1 << e) | (1 << sw) | (1 << s) | (1 << se);

										// check for all cells dead
										if (typeMask === 1 && c === 0) {
											state = 0;
										} else {
											if (typeMask & (c ? deadForcer : birthForcer)) {
												state = 1 - c;
											} else {
												if (!c && (typeMask & requireState1) && !(typeMask & 2)) {
													state = 0;
												} else {
													treat = c ? treatIfAlive : treatIfDead;
													state = ruleArray[
															(((treat >> sw) & 1) << 8) |
															(((treat >> s) & 1) << 7) |
															(((treat >> se) & 1) << 6) |
															(((treat >> w) & 1) << 5) |
															(c << 4) |
															(((treat >> e) & 1) << 3) |
															(((treat >> nw) & 1) << 2) |
															(((treat >> n) & 1) << 1) |
															((treat >> ne) & 1)];
												}
											}
										}
									}

									// check if state is alive
									nextRow[x] = state;
									oldCellWasAlive = c ? 1 : 0;
									if (state > 0) {
										population += 1;

										// update births
										births += 1 - oldCellWasAlive;
									} else {
										// update deaths
										deaths += oldCellWasAlive;
									}

									// update tile occupancy
									if (state > 0) {
										rowOccupied |= rowIndex;
										colOccupied |= colIndex;
									}

									// check if any cell was alive in the source
									anyAlive |= c;

									// next column
									colIndex >>= 1;
									x += 1;
								}

								// handle right edge
								nw = n;
								w = c;
								sw = s;
								n = ne;
								c = e;
								s = se;
								if (x === width - 1) {
									ne = 0;
									e = 0;
									se = 0;
								} else {
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}

								// check for higher states
								if (c >= 2) {
									state = nextState[c];
								} else {
									// typemask has a bit set per state in the neighbouring cells
									typeMask = (1 << nw) | (1 << n) | (1 << ne) | (1 << w) | (1 << e) | (1 << sw) | (1 << s) | (1 << se);

									// check for all cells dead
									if (typeMask === 1 && c === 0) {
										state = 0;
									} else {
										if (typeMask & (c ? deadForcer : birthForcer)) {
											state = 1 - c;
										} else {
											if (!c && (typeMask & requireState1) && !(typeMask & 2)) {
												state = 0;
											} else {
												treat = c ? treatIfAlive : treatIfDead;
												state = ruleArray[
														(((treat >> sw) & 1) << 8) |
														(((treat >> s) & 1) << 7) |
														(((treat >> se) & 1) << 6) |
														(((treat >> w) & 1) << 5) |
														(c << 4) |
														(((treat >> e) & 1) << 3) |
														(((treat >> nw) & 1) << 2) |
														(((treat >> n) & 1) << 1) |
														((treat >> ne) & 1)];
											}
										}
									}
								}

								// check if state is alive
								nextRow[x] = state;
								oldCellWasAlive = c ? 1 : 0;
								if (state > 0) {
									population += 1;

									// update births
									births += 1 - oldCellWasAlive;
								} else {
									// update death
									deaths += oldCellWasAlive;
								}

								// update tile occupancy
								if (state > 0) {
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								}

								// check if any cell was alive in the source
								anyAlive |= c;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// update the column and row occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any source cells were alive
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
						rightX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
					rightX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// compute super rule next generation (after state 0 and 1)
	Life.prototype.nextGenerationSuperTileHex = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = null,
			/** @type {Array<Uint8Array>} */ outputGrid = null,
			/** @type {Uint8Array} */ colourGridRow = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint8Array} */ aboveRow = null,
			/** @type {Uint8Array} */ belowRow = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Uint8Array} */ blankColourRow = this.blankColourRow,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ calc = 0,
			/** @type {boolean} */ process = true,
			/** @type {number} */ width = this.width,
			/** @type {number} */ width16 = width >> 4,
			/** @type {number} */ height = this.height,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newBottomY = height,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ newAliveLeftX = width,
			/** @type {number} */ newAliveRightX = -1,
			/** @type {number} */ newAliveTopY = -1,
			/** @type {number} */ newAliveBottomY = height,
			/** @type {BoundingBox} */ aliveBox = this.aliveBox,

			// mask of types in the neighbourhood
			/** @type {number} */ typeMask = 0,

			// cells in the neighbourhood
			/** @type {number} */ nw = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ s = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {Uint16Array} */ columnAliveOccupied16 = this.columnAliveOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colAliveOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {Uint16Array} */ rowAliveOccupied16 = this.rowAliveOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowAliveOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows,

			// bit counts
			/** @type {Uint8Array} */ bitCounts = this.bitCounts16,

			// constants
			/** @const {number} */ aliveWith14 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 7) | (1 << 9) | (1 << 11) | (1 << 13) | (1 << 14) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ aliveWith14or18 = aliveWith14 | (1 << 18),
			/** @const {number} */ alive1or3or5or7 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 7),
			/** @const {number} */ alive9to25 = (1 << 9) | (1 << 11) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive1or3or5or9or11 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 9) | (1 << 11),
			/** @const {number} */ alive7or13or15or17or19or21or23or25 = (1 << 7) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive1or5or7or9or11 = (1 << 1) | (1 << 5) | (1 << 7) | (1 << 9) | (1 << 11),
			/** @const {number} */ alive13or15or17or19or21or23or25 = (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive9or11 = (1 << 9) | (1 << 11),
			/** @const {number} */ alive1or3or5or13or15or17or19or21or23or25 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25);

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
			colourGrid = this.colourGrid;
			outputGrid = this.nextColourGrid;

		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
			colourGrid = this.nextColourGrid;
			outputGrid = this.colourGrid;
		}

		// check the start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);
		columnAliveOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);
		rowAliveOccupied16.fill(0);

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// mark no cells in this column
							colOccupied = 0;
							colAliveOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;
							rowAliveOccupied = 0;
							rowIndex = 32768;

							// clear type mask
							typeMask = 0;

							// process each row
							for (h = bottomY; h < topY; h += 1) {
								// get the grid and colour grid row
								gridRow = grid[h];
								colourGridRow = colourGrid[h];
								if (h === 0) {
									aboveRow = blankColourRow;
								} else {
									aboveRow = colourGrid[h - 1];
								}
								if (h === this.height - 1) {
									belowRow = blankColourRow;
								} else {
									belowRow = colourGrid[h + 1];
								}
								destRow = outputGrid[h];

								// get correct starting colour index
								cr = (leftX << 4);

								// get initial neighbours
								if (cr === 0) {
									c = 0;
									n = 0;
								} else {
									c = colourGridRow[cr - 1];
									n = aboveRow[cr - 1];
								}
								se = belowRow[cr];
								e = colourGridRow[cr];

								// process each 16bit chunk (16 cells) along the row
								nextCell = gridRow[leftX];

								// process each cell in the chunk
								colIndex = 1 << 15;
								while (colIndex > 0) {
									// shift neighbourhood left
									nw = n;
									n = aboveRow[cr];
									w = c;
									c = e;
									e = colourGridRow[cr + 1];
									s = se;
									se = belowRow[cr + 1];
									value = c;

									// typemask has a bit set per state in the neighbouring cells
									typeMask = (1 << nw) | (1 << n) | (1 << e) | (1 << w) | (1 << s) | (1 << se);

									// handle state 6
									process = true;
									if ((typeMask & (1 << 6)) !== 0) {
										process = false;
										if (c === 7 || c === 8 || c >= 13) {
											value = 0;
										} else {
											switch (c) {
												case 1:
													value = 2;
													break;

												case 3:
												case 5:
													value = 4;
													break;

												case 9:
													value = 10;
													break;

												case 11:
													value = 12;
													break;

												default:
													// not handled here so process below
													process = true;
													break;
											}
										}

										// clear cell in bit grid
										if (!process && ((c & 1) !== 0)) {
											if (nextCell & colIndex) {
												gridRow[leftX] &= ~colIndex;
												this.deaths += 1;
												this.population -= 1;
											}
										}
									}

									// check whether state still needs processing
									if (process) {
										// get cell state from bit grid
										if ((nextCell & colIndex) !== 0) {
											// cell alive
											// was cell alive in this generation
											if ((c & 1) === 0) {
												// cell was dead so has been born now
												switch (c) {
													case 4:
														value = 3;
														break;

													case 6:
														// clear cell in bit grid
														gridRow[leftX] &= ~colIndex;
														this.births -= 1;
														this.population -=1;
														break;

													case 8:
														value = 7;
														break;

													default:
														value = 1;
														calc = typeMask & alive9to25;
														if (((typeMask & alive1or3or5or7) === 0) && (bitCounts[calc & 65535] + bitCounts[calc >> 16] === 1)) {
															// the bit index gives the cell state
															value = 0;
															if (calc > 65535) {
																calc >>= 16;
																value += 16;
															}
															if (calc > 255) {
																calc >>= 8;
																value += 8;
															}
															if (calc > 15) {
																calc >>= 4;
																value += 4;
															}
															if (calc > 3) {
																calc >>= 2;
																value += 2;
															}
															value += (calc >> 1);
														} else {
															calc = typeMask & alive13or15or17or19or21or23or25;
															if (((typeMask & (1 << 3)) !== 0) && ((typeMask & alive1or5or7or9or11) === 0) && (bitCounts[calc & 65535] + bitCounts[calc >> 16] === 1)) {
																value = 0;
																if (calc > 65535) {
																	calc >>= 16;
																	value += 16;
																}
																if (calc > 255) {
																	calc >>= 8;
																	value += 8;
																}
																if (calc > 15) {
																	calc >>= 4;
																	value += 4;
																}
																if (calc > 3) {
																	calc >>= 2;
																	value += 2;
																}
																value += (calc >> 1);
															} else {
																calc = typeMask & alive9or11;
																if (((typeMask & (1 << 7)) !== 0) && ((typeMask & alive1or3or5or13or15or17or19or21or23or25) === 0) && (bitCounts[calc & 65535] + bitCounts[calc >> 16] === 1)) {
																	value = 0;
																	if (calc > 65535) {
																		calc >>= 16;
																		value += 16;
																	}
																	if (calc > 255) {
																		calc >>= 8;
																		value += 8;
																	}
																	if (calc > 15) {
																		calc >>= 4;
																		value += 4;
																	}
																	if (calc > 3) {
																		calc >>= 2;
																		value += 2;
																	}
																	value += (calc >> 1);
																} else {
																	calc = typeMask & alive7or13or15or17or19or21or23or25;
																	if (calc && ((typeMask & alive1or3or5or9or11) === 0)) {
																		value = 13;
																	}
																}
															}
														}
														break;
												}
											}
										} else {
											// cell dead
											// was cell alive in this generation
											if ((c & 1) !== 0) {
												// cell was alive so has died
												if (c <= 11) {
													if (c === 5) {
														value = 4;
													} else {
														value = c + 1;
													}
												} else {
													value = 0;
												}
											} else {
												// cell is still dead
												if (c === 14) {
													value = 0;
												} else {
													if (c > 14) {
														switch (c) {
															case 16:
																if ((typeMask & aliveWith14) !== 0) {
																	value = 14;
																}
																break;

															case 18:
																if ((typeMask & (1 << 22)) !== 0) {
																	value = 22;
																}
																break;

															case 20:
																if ((typeMask & (1 << 18)) !== 0) {
																	value = 18;
																}
																break;

															case 22:
																if ((typeMask & (1 << 20)) !== 0) {
																	value = 20;
																}
																break;

															case 24:
																if ((typeMask & aliveWith14or18) !== 0) {
																	value = 18;
																}
																break;
														}
													}
												}
											}
										}
									}

									// output new cell state
									destRow[cr] = value;
									if (value > 0) {
										colOccupied |= colIndex;
										rowOccupied |= rowIndex;

										// update alive tracker
										if (value & 1) {
											colAliveOccupied |= colIndex;
											rowAliveOccupied |= rowIndex;
										}
									}

									// next bit cell
									colIndex >>= 1;
									cr += 1;
								}

								// next row
								rowIndex >>= 1;
							}
							columnOccupied16[leftX] |= colOccupied;
							rowOccupied16[th] |= rowOccupied;
							columnAliveOccupied16[leftX] |= colAliveOccupied;
							rowAliveOccupied16[th] |= rowAliveOccupied;
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = tiles;
				colourTileHistoryRow[tw] |= tiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		for (tw = 0; tw < width16; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}

			if (columnAliveOccupied16[tw]) {
				if (tw < newAliveLeftX) {
					newAliveLeftX = tw;
				}
				if (tw > newAliveRightX) {
					newAliveRightX = tw;
				}
			}
		}

		for (th = 0; th < rowOccupied16.length; th += 1) {
			if (rowOccupied16[th]) {
				if (th < newBottomY) {
					newBottomY = th;
				}
				if (th > newTopY) {
					newTopY = th;
				}
			}

			if (rowAliveOccupied16[th]) {
				if (th < newAliveBottomY) {
					newAliveBottomY = th;
				}
				if (th > newAliveTopY) {
					newAliveTopY = th;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// convert new height to pixels
		newBottomY = (newBottomY << 4) + this.leftBitOffset16(rowOccupied16[newBottomY]);
		newTopY = (newTopY << 4) + this.rightBitOffset16(rowOccupied16[newTopY]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// convert new width to pixels
		newAliveLeftX = (newAliveLeftX << 4) + this.leftBitOffset16(columnAliveOccupied16[newAliveLeftX]);
		newAliveRightX = (newAliveRightX << 4) + this.rightBitOffset16(columnAliveOccupied16[newAliveRightX]);

		// convert new height to pixels
		newAliveBottomY = (newAliveBottomY << 4) + this.leftBitOffset16(rowAliveOccupied16[newAliveBottomY]);
		newAliveTopY = (newAliveTopY << 4) + this.rightBitOffset16(rowAliveOccupied16[newAliveTopY]);

		// ensure the alive box is not blank
		if (newAliveTopY < 0) {
			newAliveTopY = height - 1;
		}
		if (newAliveBottomY >= height) {
			newAliveBottomY = 0;
		}
		if (newAliveLeftX >= width) {
			newAliveLeftX = 0;
		}
		if (newAliveRightX < 0) {
			newAliveRightX = width - 1;
		}

		// clip to the screen
		if (newAliveTopY > height - 1) {
			newAliveTopY = height - 1;
		}
		if (newAliveBottomY < 0) {
			newAliveBottomY = 0;
		}
		if (newAliveLeftX < 0) {
			newAliveLeftX = 0;
		}
		if (newAliveRightX > width - 1) {
			newAliveRightX = width - 1;
		}

		// save to alive box
		aliveBox.topY = newAliveTopY;
		aliveBox.bottomY = newAliveBottomY;
		aliveBox.leftX = newAliveLeftX;
		aliveBox.rightX = newAliveRightX;
	};

	// compute super rule next generation (after state 0 and 1)
	Life.prototype.nextGenerationSuperTileVN = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = null,
			/** @type {Array<Uint8Array>} */ outputGrid = null,
			/** @type {Uint8Array} */ colourGridRow = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint8Array} */ aboveRow = null,
			/** @type {Uint8Array} */ belowRow = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Uint8Array} */ blankColourRow = this.blankColourRow,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ calc = 0,
			/** @type {boolean} */ process = true,
			/** @type {number} */ width = this.width,
			/** @type {number} */ width16 = width >> 4,
			/** @type {number} */ height = this.height,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newBottomY = height,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ newAliveLeftX = width,
			/** @type {number} */ newAliveRightX = -1,
			/** @type {number} */ newAliveTopY = -1,
			/** @type {number} */ newAliveBottomY = height,
			/** @type {BoundingBox} */ aliveBox = this.aliveBox,

			// mask of types in the neighbourhood
			/** @type {number} */ typeMask = 0,

			// cells in the neighbourhood
			/** @type {number} */ n = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {Uint16Array} */ columnAliveOccupied16 = this.columnAliveOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colAliveOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {Uint16Array} */ rowAliveOccupied16 = this.rowAliveOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowAliveOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows,

			// bit counts
			/** @type {Uint8Array} */ bitCounts = this.bitCounts16,

			// constants
			/** @const {number} */ aliveWith14 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 7) | (1 << 9) | (1 << 11) | (1 << 13) | (1 << 14) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ aliveWith14or18 = aliveWith14 | (1 << 18),
			/** @const {number} */ alive1or3or5or7 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 7),
			/** @const {number} */ alive9to25 = (1 << 9) | (1 << 11) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive1or3or5or9or11 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 9) | (1 << 11),
			/** @const {number} */ alive7or13or15or17or19or21or23or25 = (1 << 7) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive1or5or7or9or11 = (1 << 1) | (1 << 5) | (1 << 7) | (1 << 9) | (1 << 11),
			/** @const {number} */ alive13or15or17or19or21or23or25 = (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive9or11 = (1 << 9) | (1 << 11),
			/** @const {number} */ alive1or3or5or13or15or17or19or21or23or25 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25);

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
			colourGrid = this.colourGrid;
			outputGrid = this.nextColourGrid;

		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
			colourGrid = this.nextColourGrid;
			outputGrid = this.colourGrid;
		}

		// check the start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);
		columnAliveOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);
		rowAliveOccupied16.fill(0);

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// mark no cells in this column
							colOccupied = 0;
							colAliveOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;
							rowAliveOccupied = 0;
							rowIndex = 32768;

							// clear type mask
							typeMask = 0;

							// process each row
							for (h = bottomY; h < topY; h += 1) {
								// get the grid and colour grid row
								gridRow = grid[h];
								colourGridRow = colourGrid[h];
								if (h === 0) {
									aboveRow = blankColourRow;
								} else {
									aboveRow = colourGrid[h - 1];
								}
								if (h === this.height - 1) {
									belowRow = blankColourRow;
								} else {
									belowRow = colourGrid[h + 1];
								}
								destRow = outputGrid[h];

								// get correct starting colour index
								cr = (leftX << 4);

								// get initial neighbours
								if (cr === 0) {
									c = 0;
								} else {
									c = colourGridRow[cr - 1];
								}
								e = colourGridRow[cr];

								// process each 16bit chunk (16 cells) along the row
								nextCell = gridRow[leftX];

								// process each cell in the chunk
								colIndex = 1 << 15;
								while (colIndex > 0) {
									// shift neighbourhood left
									n = aboveRow[cr];
									w = c;
									c = e;
									e = colourGridRow[cr + 1];
									s = belowRow[cr];
									value = c;

									// typemask has a bit set per state in the neighbouring cells
									typeMask = (1 << n) | (1 << e) | (1 << w) | (1 << s);

									// handle state 6
									process = true;
									if ((typeMask & (1 << 6)) !== 0) {
										process = false;
										if (c === 7 || c === 8 || c >= 13) {
											value = 0;
										} else {
											switch (c) {
												case 1:
													value = 2;
													break;

												case 3:
												case 5:
													value = 4;
													break;

												case 9:
													value = 10;
													break;

												case 11:
													value = 12;
													break;

												default:
													// not handled here so process below
													process = true;
													break;
											}
										}

										// clear cell in bit grid
										if (!process && ((c & 1) !== 0)) {
											if (nextCell & colIndex) {
												gridRow[leftX] &= ~colIndex;
												this.deaths += 1;
												this.population -= 1;
											}
										}
									}

									// check whether state still needs processing
									if (process) {
										// get cell state from bit grid
										if ((nextCell & colIndex) !== 0) {
											// cell alive
											// was cell alive in this generation
											if ((c & 1) === 0) {
												// cell was dead so has been born now
												switch (c) {
													case 4:
														value = 3;
														break;

													case 6:
														// clear cell in bit grid
														gridRow[leftX] &= ~colIndex;
														this.births -= 1;
														this.population -=1;
														break;

													case 8:
														value = 7;
														break;

													default:
														value = 1;
														calc = typeMask & alive9to25;
														if (((typeMask & alive1or3or5or7) === 0) && (bitCounts[calc & 65535] + bitCounts[calc >> 16] === 1)) {
															// the bit index gives the cell state
															value = 0;
															if (calc > 65535) {
																calc >>= 16;
																value += 16;
															}
															if (calc > 255) {
																calc >>= 8;
																value += 8;
															}
															if (calc > 15) {
																calc >>= 4;
																value += 4;
															}
															if (calc > 3) {
																calc >>= 2;
																value += 2;
															}
															value += (calc >> 1);
														} else {
															calc = typeMask & alive13or15or17or19or21or23or25;
															if (((typeMask & (1 << 3)) !== 0) && ((typeMask & alive1or5or7or9or11) === 0) && (bitCounts[calc & 65535] + bitCounts[calc >> 16] === 1)) {
																value = 0;
																if (calc > 65535) {
																	calc >>= 16;
																	value += 16;
																}
																if (calc > 255) {
																	calc >>= 8;
																	value += 8;
																}
																if (calc > 15) {
																	calc >>= 4;
																	value += 4;
																}
																if (calc > 3) {
																	calc >>= 2;
																	value += 2;
																}
																value += (calc >> 1);
															} else {
																calc = typeMask & alive9or11;
																if (((typeMask & (1 << 7)) !== 0) && ((typeMask & alive1or3or5or13or15or17or19or21or23or25) === 0) && (bitCounts[calc & 65535] + bitCounts[calc >> 16] === 1)) {
																	value = 0;
																	if (calc > 65535) {
																		calc >>= 16;
																		value += 16;
																	}
																	if (calc > 255) {
																		calc >>= 8;
																		value += 8;
																	}
																	if (calc > 15) {
																		calc >>= 4;
																		value += 4;
																	}
																	if (calc > 3) {
																		calc >>= 2;
																		value += 2;
																	}
																	value += (calc >> 1);
																} else {
																	calc = typeMask & alive7or13or15or17or19or21or23or25;
																	if (calc && ((typeMask & alive1or3or5or9or11) === 0)) {
																		value = 13;
																	}
																}
															}
														}
														break;
												}
											}
										} else {
											// cell dead
											// was cell alive in this generation
											if ((c & 1) !== 0) {
												// cell was alive so has died
												if (c <= 11) {
													if (c === 5) {
														value = 4;
													} else {
														value = c + 1;
													}
												} else {
													value = 0;
												}
											} else {
												// cell is still dead
												if (c === 14) {
													value = 0;
												} else {
													if (c > 14) {
														switch (c) {
															case 16:
																if ((typeMask & aliveWith14) !== 0) {
																	value = 14;
																}
																break;

															case 18:
																if ((typeMask & (1 << 22)) !== 0) {
																	value = 22;
																}
																break;

															case 20:
																if ((typeMask & (1 << 18)) !== 0) {
																	value = 18;
																}
																break;

															case 22:
																if ((typeMask & (1 << 20)) !== 0) {
																	value = 20;
																}
																break;

															case 24:
																if ((typeMask & aliveWith14or18) !== 0) {
																	value = 18;
																}
																break;
														}
													}
												}
											}
										}
									}

									// output new cell state
									destRow[cr] = value;
									if (value > 0) {
										colOccupied |= colIndex;
										rowOccupied |= rowIndex;

										// update alive tracker
										if (value & 1) {
											colAliveOccupied |= colIndex;
											rowAliveOccupied |= rowIndex;
										}
									}

									// next bit cell
									colIndex >>= 1;
									cr += 1;
								}

								// bext row
								rowIndex >>= 1;
							}
							columnOccupied16[leftX] |= colOccupied;
							rowOccupied16[th] |= rowOccupied;
							columnAliveOccupied16[leftX] |= colAliveOccupied;
							rowAliveOccupied16[th] |= rowAliveOccupied;
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = tiles;
				colourTileHistoryRow[tw] |= tiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		for (tw = 0; tw < width16; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}

			if (columnAliveOccupied16[tw]) {
				if (tw < newAliveLeftX) {
					newAliveLeftX = tw;
				}
				if (tw > newAliveRightX) {
					newAliveRightX = tw;
				}
			}
		}

		for (th = 0; th < rowOccupied16.length; th += 1) {
			if (rowOccupied16[th]) {
				if (th < newBottomY) {
					newBottomY = th;
				}
				if (th > newTopY) {
					newTopY = th;
				}
			}

			if (rowAliveOccupied16[th]) {
				if (th < newAliveBottomY) {
					newAliveBottomY = th;
				}
				if (th > newAliveTopY) {
					newAliveTopY = th;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// convert new height to pixels
		newBottomY = (newBottomY << 4) + this.leftBitOffset16(rowOccupied16[newBottomY]);
		newTopY = (newTopY << 4) + this.rightBitOffset16(rowOccupied16[newTopY]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// convert new width to pixels
		newAliveLeftX = (newAliveLeftX << 4) + this.leftBitOffset16(columnAliveOccupied16[newAliveLeftX]);
		newAliveRightX = (newAliveRightX << 4) + this.rightBitOffset16(columnAliveOccupied16[newAliveRightX]);

		// convert new height to pixels
		newAliveBottomY = (newAliveBottomY << 4) + this.leftBitOffset16(rowAliveOccupied16[newAliveBottomY]);
		newAliveTopY = (newAliveTopY << 4) + this.rightBitOffset16(rowAliveOccupied16[newAliveTopY]);

		// ensure the alive box is not blank
		if (newAliveTopY < 0) {
			newAliveTopY = height - 1;
		}
		if (newAliveBottomY >= height) {
			newAliveBottomY = 0;
		}
		if (newAliveLeftX >= width) {
			newAliveLeftX = 0;
		}
		if (newAliveRightX < 0) {
			newAliveRightX = width - 1;
		}

		// clip to the screen
		if (newAliveTopY > height - 1) {
			newAliveTopY = height - 1;
		}
		if (newAliveBottomY < 0) {
			newAliveBottomY = 0;
		}
		if (newAliveLeftX < 0) {
			newAliveLeftX = 0;
		}
		if (newAliveRightX > width - 1) {
			newAliveRightX = width - 1;
		}

		// save to alive box
		aliveBox.topY = newAliveTopY;
		aliveBox.bottomY = newAliveBottomY;
		aliveBox.leftX = newAliveLeftX;
		aliveBox.rightX = newAliveRightX;
	};

	// compute super rule next generation (after state 0 and 1)
	Life.prototype.nextGenerationSuperTileMoore = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = null,
			/** @type {Array<Uint8Array>} */ outputGrid = null,
			/** @type {Uint8Array} */ colourGridRow = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint8Array} */ aboveRow = null,
			/** @type {Uint8Array} */ belowRow = null,
			/** @type {Uint8Array} */ destRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Uint8Array} */ blankColourRow = this.blankColourRow,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ calc = 0,
			/** @type {boolean} */ process = true,
			/** @type {number} */ width = this.width,
			/** @type {number} */ width16 = width >> 4,
			/** @type {number} */ height = this.height,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newBottomY = height,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ newAliveLeftX = width,
			/** @type {number} */ newAliveRightX = -1,
			/** @type {number} */ newAliveTopY = -1,
			/** @type {number} */ newAliveBottomY = height,
			/** @type {BoundingBox} */ aliveBox = this.aliveBox,

			// mask of types in the neighbourhood
			/** @type {number} */ typeMask = 0,

			// cells in the neighbourhood
			/** @type {number} */ lcol = 0,
			/** @type {number} */ ccol = 0,
			/** @type {number} */ rcol = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ e = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {Uint16Array} */ columnAliveOccupied16 = this.columnAliveOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colAliveOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {Uint16Array} */ rowAliveOccupied16 = this.rowAliveOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowAliveOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows,

			// constants
			/** @const {number} */ aliveWith14 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 7) | (1 << 9) | (1 << 11) | (1 << 13) | (1 << 14) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ aliveWith14or18 = aliveWith14 | (1 << 18),
			/** @const {number} */ alive1or3or5or7 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 7),
			/** @const {number} */ alive9to25 = (1 << 9) | (1 << 11) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive1or3or5or9or11 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 9) | (1 << 11),
			/** @const {number} */ alive7or13or15or17or19or21or23or25 = (1 << 7) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive1or5or7or9or11 = (1 << 1) | (1 << 5) | (1 << 7) | (1 << 9) | (1 << 11),
			/** @const {number} */ alive13or15or17or19or21or23or25 = (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25),
			/** @const {number} */ alive9or11 = (1 << 9) | (1 << 11),
			/** @const {number} */ alive1or3or5or13or15or17or19or21or23or25 = (1 << 1) | (1 << 3) | (1 << 5) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 21) | (1 << 23) | (1 << 25);

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
			colourGrid = this.colourGrid;
			outputGrid = this.nextColourGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
			colourGrid = this.nextColourGrid;
			outputGrid = this.colourGrid;
		}

		// check the start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);
		columnAliveOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);
		rowAliveOccupied16.fill(0);

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// mark no cells in this column
							colOccupied = 0;
							colAliveOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;
							rowAliveOccupied = 0;
							rowIndex = 32768;

							// clear type mask
							typeMask = 0;

							// process each row
							for (h = bottomY; h < topY; h += 1) {
								// get the grid and colour grid row
								gridRow = grid[h];
								colourGridRow = colourGrid[h];
								if (h === 0) {
									aboveRow = blankColourRow;
								} else {
									aboveRow = colourGrid[h - 1];
								}
								if (h === this.height - 1) {
									belowRow = blankColourRow;
								} else {
									belowRow = colourGrid[h + 1];
								}
								destRow = outputGrid[h];

								// get correct starting colour index
								cr = (leftX << 4);

								// get initial neighbours
								lcol = 0;
								if (cr === 0) {
									c = 0;
									ccol = 0;
								} else {
									c = colourGridRow[cr - 1];
									ccol = (1 << aboveRow[cr - 1]) | (1 << c) | (1 << belowRow[cr - 1]);
								}
								e = colourGridRow[cr];
								rcol = (1 << e) | (1 << belowRow[cr]) | (1 << aboveRow[cr]);

								// process each 16bit chunk (16 cells) along the row
								nextCell = gridRow[leftX];

								// process each cell in the chunk
								colIndex = 1 << 15;
								while (colIndex > 0) {
									// get next column
									cr += 1;

									// shift neighbourhood left
									c = e;
									e = colourGridRow[cr];
									lcol = ccol;
									ccol = rcol;
									rcol = (1 << e) | (1 << belowRow[cr]) | (1 << aboveRow[cr]);
									value = c;

									// typemask has a bit set per state in the neighbouring cells
									typeMask = lcol | ccol | rcol;

									// handle state 6
									process = true;
									if ((typeMask & (1 << 6)) !== 0) {
										process = false;
										if (c === 7 || c === 8 || c >= 13) {
											value = 0;
										} else {
											switch (c) {
												case 1:
													value = 2;
													break;

												case 3:
												case 5:
													value = 4;
													break;

												case 9:
													value = 10;
													break;

												case 11:
													value = 12;
													break;

												default:
													// not handled here so process below
													process = true;
													break;
											}
										}

										// clear cell in bit grid
										if (!process && ((c & 1) !== 0)) {
											if (nextCell & colIndex) {
												gridRow[leftX] &= ~colIndex;
												this.deaths += 1;
												this.population -= 1;
											}
										}
									}

									// check whether state still needs processing
									if (process) {
										// get cell state from bit grid
										if ((nextCell & colIndex) !== 0) {
											// cell alive
											// was cell alive in this generation
											if ((c & 1) === 0) {
												// cell was dead so has been born now
												switch (c) {
													case 4:
														value = 3;
														break;

													case 6:
														// clear cell in bit grid
														gridRow[leftX] &= ~colIndex;
														this.births -= 1;
														this.population -= 1;
														break;

													case 8:
														value = 7;
														break;

													default:
														value = 1;
														calc = typeMask & alive9to25;
														if (((typeMask & alive1or3or5or7) === 0) && (calc && (calc & (calc - 1)) === 0)) {
															// the bit index gives the cell state
															value = 31 - Math.clz32(calc);
														} else {
															calc = typeMask & alive13or15or17or19or21or23or25;
															if (((typeMask & (1 << 3)) !== 0) && ((typeMask & alive1or5or7or9or11) === 0) && (calc && (calc & (calc - 1)) === 0)) {
																value = 31 - Math.clz32(calc);
															} else {
																calc = typeMask & alive9or11;
																if (((typeMask & (1 << 7)) !== 0) && ((typeMask & alive1or3or5or13or15or17or19or21or23or25) === 0) && (calc && (calc & (calc - 1)) === 0)) {
																	value = 31 - Math.clz32(calc);
																} else {
																	calc = typeMask & alive7or13or15or17or19or21or23or25;
																	if (calc && ((typeMask & alive1or3or5or9or11) === 0)) {
																		value = 13;
																	}
																}
															}
														}
														break;
												}
											}
										} else {
											// cell dead
											// was cell alive in this generation
											if ((c & 1) !== 0) {
												// cell was alive so has died
												if (c <= 11) {
													if (c === 5) {
														value = 4;
													} else {
														value = c + 1;
													}
												} else {
													value = 0;
												}
											} else {
												// cell is still dead
												if (c >= 14) {
													switch (c) {
														case 14:
															value = 0;
															break;

														case 16:
															if ((typeMask & aliveWith14) !== 0) {
																value = 14;
															}
															break;

														case 18:
															if ((typeMask & (1 << 22)) !== 0) {
																value = 22;
															}
															break;

														case 20:
															if ((typeMask & (1 << 18)) !== 0) {
																value = 18;
															}
															break;

														case 22:
															if ((typeMask & (1 << 20)) !== 0) {
																value = 20;
															}
															break;

														case 24:
															if ((typeMask & aliveWith14or18) !== 0) {
																value = 18;
															}
															break;
													}
												}
											}
										}
									}

									// output new cell state
									destRow[cr - 1] = value;
									if (value > 0) {
										colOccupied |= colIndex;
										rowOccupied |= rowIndex;

										// update alive tracker
										if (value & 1) {
											colAliveOccupied |= colIndex;
											rowAliveOccupied |= rowIndex;
										}
									}

									// next bit cell
									colIndex >>= 1;
								}

								// next row
								rowIndex >>= 1;
							}

							columnOccupied16[leftX] |= colOccupied;
							rowOccupied16[th] |= rowOccupied;
							columnAliveOccupied16[leftX] |= colAliveOccupied;
							rowAliveOccupied16[th] |= rowAliveOccupied;
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = tiles;
				colourTileHistoryRow[tw] |= tiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		for (tw = 0; tw < width16; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}

			if (columnAliveOccupied16[tw]) {
				if (tw < newAliveLeftX) {
					newAliveLeftX = tw;
				}
				if (tw > newAliveRightX) {
					newAliveRightX = tw;
				}
			}
		}

		for (th = 0; th < rowOccupied16.length; th += 1) {
			if (rowOccupied16[th]) {
				if (th < newBottomY) {
					newBottomY = th;
				}
				if (th > newTopY) {
					newTopY = th;
				}
			}

			if (rowAliveOccupied16[th]) {
				if (th < newAliveBottomY) {
					newAliveBottomY = th;
				}
				if (th > newAliveTopY) {
					newAliveTopY = th;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// convert new height to pixels
		newBottomY = (newBottomY << 4) + this.leftBitOffset16(rowOccupied16[newBottomY]);
		newTopY = (newTopY << 4) + this.rightBitOffset16(rowOccupied16[newTopY]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// convert new width to pixels
		newAliveLeftX = (newAliveLeftX << 4) + this.leftBitOffset16(columnAliveOccupied16[newAliveLeftX]);
		newAliveRightX = (newAliveRightX << 4) + this.rightBitOffset16(columnAliveOccupied16[newAliveRightX]);

		// convert new height to pixels
		newAliveBottomY = (newAliveBottomY << 4) + this.leftBitOffset16(rowAliveOccupied16[newAliveBottomY]);
		newAliveTopY = (newAliveTopY << 4) + this.rightBitOffset16(rowAliveOccupied16[newAliveTopY]);

		// ensure the alive box is not blank
		if (newAliveTopY < 0) {
			newAliveTopY = height - 1;
		}
		if (newAliveBottomY >= height) {
			newAliveBottomY = 0;
		}
		if (newAliveLeftX >= width) {
			newAliveLeftX = 0;
		}
		if (newAliveRightX < 0) {
			newAliveRightX = width - 1;
		}

		// clip to the screen
		if (newAliveTopY > height - 1) {
			newAliveTopY = height - 1;
		}
		if (newAliveBottomY < 0) {
			newAliveBottomY = 0;
		}
		if (newAliveLeftX < 0) {
			newAliveLeftX = 0;
		}
		if (newAliveRightX > width - 1) {
			newAliveRightX = width - 1;
		}

		// save to alive box
		aliveBox.topY = newAliveTopY;
		aliveBox.bottomY = newAliveBottomY;
		aliveBox.leftX = newAliveLeftX;
		aliveBox.rightX = newAliveRightX;
	};

	// compute generations rule next generation (after state 0 and 1)
	Life.prototype.nextGenerationGenerations = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourGridRow = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {number} */ value = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,
			/** @type {number} */ lastValue = 0,
			/** @type {number} */ width = this.width,
			/** @type {number} */ width16 = width >> 4,
			/** @type {number} */ height = this.height,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newBottomY = height,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// whether the tile is alive
			/** @type {number} */ tileAlive = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows,

			// maximum generations state
			/** @type {number} */ maxGenState = this.multiNumStates + this.historyStates - 1,

			// maximum dead state number
			/** @type {number} */ deadState = this.historyStates,

			// minimum dead state number
			/** @type {number} */ minDeadState = (this.historyStates > 0 ? 1 : 0);

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// check the start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];
				nextTiles = 0;

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;
							rowIndex = 32768;

							// flag nothing alive in the tile
							tileAlive = 0;

							// process each row
							for (h = bottomY; h < topY; h += 1) {
								// get the grid and colour grid row
								gridRow = grid[h];
								colourGridRow = colourGrid[h];

								// get correct starting colour index
								cr = (leftX << 4);

								// process each 16bit chunk (16 cells) along the row
								nextCell = gridRow[leftX];

								// process each cell in the chunk
								colIndex = 32768;
								n = 1 << 15;
								while (n > 0) {
									// get next colour cell
									value = colourGridRow[cr];
									lastValue = value;

									// process the Generations rule
									if ((value <= deadState || value === maxGenState) && ((nextCell & n) !== 0)) {
										value = maxGenState;
									} else {
										nextCell &= ~n;
										if (value > minDeadState) {
											value -= 1;
										}
									}

									// write the colour back
									colourGridRow[cr] = value;
									if (value > minDeadState) {
										tileAlive = 1;
										if (value > deadState) {
											colOccupied |= colIndex;
											rowOccupied |= rowIndex;
										}
										if (value === maxGenState) {
											population += 1;
											if (lastValue !== maxGenState) {
												births += 1;
											}
										}
									}
									if (lastValue === maxGenState && value !== maxGenState) {
										deaths += 1;
									}

									colIndex >>= 1;
									cr += 1;
									n >>= 1;

									// loop unroll 1
									value = colourGridRow[cr];
									lastValue = value;
									if ((value <= deadState || value === maxGenState) && ((nextCell & n) !== 0)) {
										value = maxGenState;
									} else {
										nextCell &= ~n;
										if (value > minDeadState) {
											value -= 1;
										}
									}
									colourGridRow[cr] = value;
									if (value > minDeadState) {
										tileAlive = 1;
										if (value > deadState) {
											colOccupied |= colIndex;
											rowOccupied |= rowIndex;
										}
										if (value === maxGenState) {
											population += 1;
											if (lastValue !== maxGenState) {
												births += 1;
											}
										}
									}
									if (lastValue === maxGenState && value !== maxGenState) {
										deaths += 1;
									}
									colIndex >>= 1;
									cr += 1;
									n >>= 1;

									// loop unroll 2
									value = colourGridRow[cr];
									lastValue = value;
									if ((value <= deadState || value === maxGenState) && ((nextCell & n) !== 0)) {
										value = maxGenState;
									} else {
										nextCell &= ~n;
										if (value > minDeadState) {
											value -= 1;
										}
									}
									colourGridRow[cr] = value;
									if (value > minDeadState) {
										tileAlive = 1;
										if (value > deadState) {
											colOccupied |= colIndex;
											rowOccupied |= rowIndex;
										}
										if (value === maxGenState) {
											population += 1;
											if (lastValue !== maxGenState) {
												births += 1;
											}
										}
									}
									if (lastValue === maxGenState && value !== maxGenState) {
										deaths += 1;
									}
									colIndex >>= 1;
									cr += 1;
									n >>= 1;

									// loop unroll 3
									value = colourGridRow[cr];
									lastValue = value;
									if ((value <= deadState || value === maxGenState) && ((nextCell & n) !== 0)) {
										value = maxGenState;
									} else {
										nextCell &= ~n;
										if (value > minDeadState) {
											value -= 1;
										}
									}
									colourGridRow[cr] = value;
									if (value > minDeadState) {
										tileAlive = 1;
										if (value > deadState) {
											colOccupied |= colIndex;
											rowOccupied |= rowIndex;
										}
										if (value === maxGenState) {
											population += 1;
											if (lastValue !== maxGenState) {
												births += 1;
											}
										}
									}
									if (lastValue === maxGenState && value !== maxGenState) {
										deaths += 1;
									}
									colIndex >>= 1;
									cr += 1;
									n >>= 1;
								}

								// save the updated state 1 cells to the bitmap
								gridRow[leftX] = nextCell;
								rowIndex >>= 1;
							}
							columnOccupied16[leftX] |= colOccupied;
							rowOccupied16[th] |= rowOccupied;

							// check if the row was alive
							if (tileAlive) {
								// update tile flag
								nextTiles |= (1 << b);
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = nextTiles;
				colourTileHistoryRow[tw] |= nextTiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		for (tw = 0; tw < width16; tw += 1) {
			if (columnOccupied16[tw]) {
				if (tw < newLeftX) {
					newLeftX = tw;
				}
				if (tw > newRightX) {
					newRightX = tw;
				}
			}
		}

		for (th = 0; th < rowOccupied16.length; th += 1) {
			if (rowOccupied16[th]) {
				if (th < newBottomY) {
					newBottomY = th;
				}
				if (th > newTopY) {
					newTopY = th;
				}
			}
		}

		// convert new width to pixels
		newLeftX = (newLeftX << 4) + this.leftBitOffset16(columnOccupied16[newLeftX]);
		newRightX = (newRightX << 4) + this.rightBitOffset16(columnOccupied16[newRightX]);

		// convert new height to pixels
		newBottomY = (newBottomY << 4) + this.leftBitOffset16(rowOccupied16[newBottomY]);
		newTopY = (newTopY << 4) + this.rightBitOffset16(rowOccupied16[newTopY]);

		// ensure the box is not blank
		if (newTopY < 0) {
			newTopY = height - 1;
		}
		if (newBottomY >= height) {
			newBottomY = 0;
		}
		if (newLeftX >= width) {
			newLeftX = 0;
		}
		if (newRightX < 0) {
			newRightX = width - 1;
		}

		// clip to the screen
		if (newTopY > height - 1) {
			newTopY = height - 1;
		}
		if (newBottomY < 0) {
			newBottomY = 0;
		}
		if (newLeftX < 0) {
			newLeftX = 0;
		}
		if (newRightX > width - 1) {
			newRightX = width - 1;
		}

		// save to zoom box
		zoomBox.topY = newTopY;
		zoomBox.bottomY = newBottomY;
		zoomBox.leftX = newLeftX;
		zoomBox.rightX = newRightX;

		// update the population
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// convert life grid region to pens using tiles
	Life.prototype.convertToPensTile = function() {
		var	/** @type {number} */ timing = performance.now(),
			/** @type {number} */ shader = this.cellRenderer;

		// ignore if rule is none, PCA, RuleTable, Super, Extended or Generations
		if (!(this.isNone || this.isPCA || this.isRuleTree || this.isSuper || this.isExtended || this.multiNumStates !== -1)) {
			// if using the Cell Age shader and the current Theme has no history then use the Basic shader since it is faster
			if (shader === LifeConstants.shaderCellAge && !this.themes[this.colourTheme].hasHistory(this.isLifeHistory, this.view)) {
				shader = LifeConstants.shaderBasic;
			}

			switch (shader) {
				case LifeConstants.shaderCellAge:
					// use Cell Age converter
					if (Controller.useWASM && Controller.wasmEnableConvertToPens && this.view.wasmEnabled) {
						if ((this.counter & 1) !== 0) {
							WASM.convertToPensAge(
								this.colourGrid.whole.byteOffset | 0,
								this.colourTileHistoryGrid.whole.byteOffset | 0,
								this.colourTileGrid.whole.byteOffset | 0,
								this.tileY | 0,
								this.tileX | 0,
								this.tileRows | 0,
								this.tileCols | 0,
								this.nextGrid16.whole.byteOffset | 0,
								this.nextTileGrid.whole.byteOffset | 0,
								this.colourGrid[0].length | 0
							);
						} else {
							WASM.convertToPensAge(
								this.colourGrid.whole.byteOffset | 0,
								this.colourTileHistoryGrid.whole.byteOffset | 0,
								this.colourTileGrid.whole.byteOffset | 0,
								this.tileY | 0,
								this.tileX | 0,
								this.tileRows | 0,
								this.tileCols | 0,
								this.grid16.whole.byteOffset | 0,
								this.tileGrid.whole.byteOffset | 0,
								this.colourGrid[0].length | 0
							);
						}
					} else {
						this.convertToPensTileRegular();
					}
					break;

				case LifeConstants.shaderRainbow:
					this.convertToPensTileRainbow();
					break;

				case LifeConstants.shaderNeighbourCount:
					if (Controller.useWASM && Controller.wasmEnableConvertToPens && this.view.wasmEnabled) {
						if ((this.counter & 1) !== 0) {
							WASM.convertToPensNeighbours(
								this.colourGrid.whole.byteOffset | 0,
								this.colourTileHistoryGrid.whole.byteOffset | 0,
								this.colourTileGrid.whole.byteOffset | 0,
								this.tileY | 0,
								this.tileX | 0,
								this.tileRows | 0,
								this.tileCols | 0,
								this.nextGrid16.whole.byteOffset | 0,
								this.nextTileGrid.whole.byteOffset | 0,
								this.colourGrid[0].length | 0
							);
						} else {
							WASM.convertToPensNeighbours(
								this.colourGrid.whole.byteOffset | 0,
								this.colourTileHistoryGrid.whole.byteOffset | 0,
								this.colourTileGrid.whole.byteOffset | 0,
								this.tileY | 0,
								this.tileX | 0,
								this.tileRows | 0,
								this.tileCols | 0,
								this.grid16.whole.byteOffset | 0,
								this.tileGrid.whole.byteOffset | 0,
								this.colourGrid[0].length | 0
							);
						}
					} else {
						this.convertToPensTileNeighbourCount();
					}
					break;

				case LifeConstants.shaderBasic:
					if (Controller.useWASM && Controller.wasmEnableConvertToPens && this.view.wasmEnabled) {
						if ((this.counter & 1) !== 0) {
							WASM.convertToPens2(
								this.colourGrid.whole.byteOffset | 0,
								this.colourTileHistoryGrid.whole.byteOffset | 0,
								this.colourTileGrid.whole.byteOffset | 0,
								this.tileY | 0,
								this.tileX | 0,
								this.tileRows | 0,
								this.tileCols | 0,
								this.nextGrid16.whole.byteOffset | 0,
								this.nextTileGrid.whole.byteOffset | 0,
								this.colourGrid[0].length | 0
							);
						} else {
							WASM.convertToPens2(
								this.colourGrid.whole.byteOffset | 0,
								this.colourTileHistoryGrid.whole.byteOffset | 0,
								this.colourTileGrid.whole.byteOffset | 0,
								this.tileY | 0,
								this.tileX | 0,
								this.tileRows | 0,
								this.tileCols | 0,
								this.grid16.whole.byteOffset | 0,
								this.tileGrid.whole.byteOffset | 0,
								this.colourGrid[0].length | 0
							);
						}
					} else {
						this.convertToPensTile2();
					}
					break;
			}

			timing = performance.now() - timing;
			if (Controller.wasmTiming) {
				this.view.menuManager.updateTimingItem("convertToPens", timing, Controller.useWASM && Controller.wasmEnableConvertToPens && this.view.wasmEnabled);
			}

			// clear ecaping gliders if enabled
			if (this.clearGliders) {
				this.clearEscapingGliders();
			}
		}

		if (this.clearGliders && this.isHROT && this.HROT.xrange === 1) {
			this.clearEscapingGliders();
		}
	};

	// remove history cells from colour grid
	Life.prototype.clearHistoryCells = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ cr = 0,
			/** @type {Array<Uint16Array>} */ colourGrid16 = this.colourGrid16,
			/** @type {Uint16Array} */ colourGridRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ twoCells = 0,
			/** @type {number} */ newCells = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// alive mask for two cells
			/** @type {number} */ aliveMask16 = (64 << 8) | 64;

		// initialize for first tile
		topY = ySize;

		// scan each row of tiles
		for (th = 0; th < tileRows; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = colourTileHistoryRow[tw];

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							if (this.multiNumStates === -1) {
								// process each row
								h = bottomY;
								while (h < topY) {
									// get the grid and colour grid row
									colourGridRow = colourGrid16[h];

									// get correct starting colour index
									cr = (leftX << 3);

									// mask out all but the two alive cells
									colourGridRow[cr] &= aliveMask16;
									cr += 1;
									colourGridRow[cr] &= aliveMask16;
									cr += 1;
									colourGridRow[cr] &= aliveMask16;
									cr += 1;
									colourGridRow[cr] &= aliveMask16;
									cr += 1;
									colourGridRow[cr] &= aliveMask16;
									cr += 1;
									colourGridRow[cr] &= aliveMask16;
									cr += 1;
									colourGridRow[cr] &= aliveMask16;
									cr += 1;
									colourGridRow[cr] &= aliveMask16;
									// cr += 1   - no need for final increment it will be reset next row

									// next row
									h += 1;
								}
							} else {
								// process each row
								h = bottomY;
								while (h < topY) {
									// get the grid and colour grid row
									colourGridRow = colourGrid16[h];

									// get correct starting colour index
									cr = (leftX << 3);

									// remove history cells
									for (w = 0; w < 8; w += 1) {
										twoCells = colourGridRow[cr];
										newCells = 0;
										if ((twoCells & 255) > this.historyStates) {
											newCells = twoCells & 0xff;
										}
										if ((twoCells >> 8) > this.historyStates) {
											newCells |= twoCells & 0xff00;
										}
										colourGridRow[cr] = newCells;
										cr += 1;
									}

									// next row
									h += 1;
								}
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// update the life grid region using tiles for Extended patterns
	Life.prototype.nextGenerationExtendedTile = function() {
		var	/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ bLeftX = 0,
			/** @type {number} */ bBottomY = 0,
			/** @type {number} */ bRightX = 0,
			/** @type {number} */ bTopY = 0,
			/** @type {number} */ timing = performance.now();

		if (this.isHex) {
			if (Controller.useWASM && Controller.wasmEnableNextGenerationInvestigator && this.view.wasmEnabled) {
				WASM.nextGenerationInvestigatorHex(
					this.colourGrid.whole.byteOffset | 0,
					this.nextColourGrid.whole.byteOffset | 0,
					this.colourGrid[0].length | 0,
					this.tileGrid.whole.byteOffset | 0,
					this.nextTileGrid.whole.byteOffset | 0,
					this.colourTileHistoryGrid.whole.byteOffset | 0,
					this.tileGrid[0].length | 0,
					this.tileGrid.whole.length | 0,
					this.diedGrid.whole.byteOffset | 0,
					this.columnOccupied16.byteOffset | 0,
					this.columnOccupied16.length | 0,
					this.rowOccupied16.byteOffset | 0,
					this.rowOccupied16.length | 0,
					this.manager.ruleArray.byteOffset | 0,
					this.altSpecified ? (this.manager.ruleAltArray.byteOffset | 0) : 0 | 0,
					this.width | 0,
					this.height | 0,
					this.tileY | 0,
					this.tileRows | 0,
					this.tileCols | 0,
					this.blankTileRow.byteOffset | 0,
					this.blankTileRow.length | 0,
					this.blankColourRow.byteOffset | 0,
					this.counter | 0,
					this.altSpecified ? (1 | 0) : (0 | 0),
					this.nextStateExtendedWASM.byteOffset | 0,
					LifeConstants.bottomRightSet | 0,
					LifeConstants.bottomSet | 0,
					LifeConstants.topRightSet | 0,
					LifeConstants.topSet | 0,
					LifeConstants.bottomLeftSet | 0,
					LifeConstants.topLeftSet | 0,
					LifeConstants.leftSet | 0,
					LifeConstants.rightSet | 0,
					this.sharedBuffer.byteOffset | 0
				);
			} else {
				this.nextGenerationExtendedTileHex();
			}
		} else {
			if (this.isVonNeumann) {
				if (Controller.useWASM && Controller.wasmEnableNextGenerationInvestigator && this.view.wasmEnabled) {
					WASM.nextGenerationInvestigatorVN(
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.tileGrid.whole.length | 0,
						this.diedGrid.whole.byteOffset | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.manager.ruleArray.byteOffset | 0,
						this.altSpecified ? (this.manager.ruleAltArray.byteOffset | 0) : 0 | 0,
						this.width | 0,
						this.height | 0,
						this.tileY | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.blankTileRow.byteOffset | 0,
						this.blankTileRow.length | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						this.altSpecified ? (1 | 0) : (0 | 0),
						this.nextStateExtendedWASM.byteOffset | 0,
						LifeConstants.bottomRightSet | 0,
						LifeConstants.bottomSet | 0,
						LifeConstants.topRightSet | 0,
						LifeConstants.topSet | 0,
						LifeConstants.bottomLeftSet | 0,
						LifeConstants.topLeftSet | 0,
						LifeConstants.leftSet | 0,
						LifeConstants.rightSet | 0,
						this.sharedBuffer.byteOffset | 0
					);
				} else {
					this.nextGenerationExtendedTileVN();
				}
			} else {
				if (Controller.useWASM && Controller.wasmEnableNextGenerationInvestigator && this.view.wasmEnabled) {
					WASM.nextGenerationInvestigatorMoore(
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.tileGrid.whole.length | 0,
						this.diedGrid.whole.byteOffset | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.manager.ruleArray.byteOffset | 0,
						this.altSpecified ? (this.manager.ruleAltArray.byteOffset | 0) : 0 | 0,
						this.width | 0,
						this.height | 0,
						this.tileY | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.blankTileRow.byteOffset | 0,
						this.blankTileRow.length | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						this.altSpecified ? (1 | 0) : (0 | 0),
						this.nextStateExtendedWASM.byteOffset | 0,
						LifeConstants.bottomRightSet | 0,
						LifeConstants.bottomSet | 0,
						LifeConstants.topRightSet | 0,
						LifeConstants.topSet | 0,
						LifeConstants.bottomLeftSet | 0,
						LifeConstants.topLeftSet | 0,
						LifeConstants.leftSet | 0,
						LifeConstants.rightSet | 0,
						this.sharedBuffer.byteOffset | 0
					);
				} else {
					this.nextGenerationExtendedTileMoore();
				}
			}
		}

		if (Controller.useWASM && Controller.wasmEnableNextGenerationSuper && this.view.wasmEnabled) {
			this.population = this.sharedBuffer[0];
			this.births = this.sharedBuffer[1];
			this.deaths = this.sharedBuffer[2];
			this.zoomBox.leftX = this.sharedBuffer[3];
			this.zoomBox.bottomY = this.sharedBuffer[4];
			this.zoomBox.rightX = this.sharedBuffer[5];
			this.zoomBox.topY = this.sharedBuffer[6];
		}

		timing = performance.now() - timing;
		if (Controller.wasmTiming) {
			this.view.menuManager.updateTimingItem("nextInvestigator", timing, Controller.useWASM && Controller.wasmEnableNextGenerationInvestigator && this.view.wasmEnabled);
		}

		// clip bounding box to bounded grid
		if (this.boundedGridType !== -1) {
			// bottom left
			bLeftX = Math.round((this.width - width) / 2);
			bBottomY = Math.round((this.height - height) / 2);

			// top right
			bRightX = bLeftX + width - 1;
			bTopY = bBottomY + height - 1;

			if (zoomBox.leftX < bLeftX) {
				zoomBox.leftX = bLeftX;
			}
			if (zoomBox.rightX > bRightX) {
				zoomBox.rightX = bRightX;
			}
			if (zoomBox.bottomY < bBottomY) {
				zoomBox.bottomY = bBottomY;
			}
			if (zoomBox.topY > bTopY) {
				zoomBox.topY = bTopY;
			}
		}
	};

	// update the life grid region using tiles for Super patterns
	Life.prototype.nextGenerationSuperTile = function() {
		var	/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {number} */ bLeftX = 0,
			/** @type {number} */ bBottomY = 0,
			/** @type {number} */ bRightX = 0,
			/** @type {number} */ bTopY = 0,
			/** @type {number} */ timing = performance.now();

		if (this.isHex) {
			if (Controller.useWASM && Controller.wasmEnableNextGenerationSuper && this.view.wasmEnabled) {
				WASM.nextGenerationSuperHex(
					this.grid16.whole.byteOffset | 0,
					this.nextGrid16.whole.byteOffset | 0,
					this.grid16[0].length | 0,
					this.tileGrid.whole.byteOffset | 0,
					this.nextTileGrid.whole.byteOffset | 0,
					this.colourTileGrid.whole.byteOffset | 0,
					this.colourTileHistoryGrid.whole.byteOffset | 0,
					this.tileGrid[0].length | 0,
					this.colourGrid.whole.byteOffset | 0,
					this.nextColourGrid.whole.byteOffset | 0,
					this.colourGrid[0].length | 0,
					this.columnOccupied16.byteOffset | 0,
					this.columnAliveOccupied16.byteOffset | 0,
					this.columnOccupied16.length | 0,
					this.rowOccupied16.byteOffset | 0,
					this.rowAliveOccupied16.byteOffset | 0,
					this.rowOccupied16.length | 0,
					this.width | 0,
					this.height | 0,
					this.tileX | 0,
					this.tileY | 0,
					this.tileCols | 0,
					this.blankColourRow.byteOffset | 0,
					this.counter | 0,
					this.population | 0,
					this.births | 0,
					this.deaths | 0,
					this.sharedBuffer.byteOffset | 0
				);
			} else {
				this.nextGenerationSuperTileHex();
			}
		} else {
			if (this.isVonNeumann) {
				if (Controller.useWASM && Controller.wasmEnableNextGenerationSuper && this.view.wasmEnabled) {
					WASM.nextGenerationSuperVN(
						this.grid16.whole.byteOffset | 0,
						this.nextGrid16.whole.byteOffset | 0,
						this.grid16[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnAliveOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowAliveOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.width | 0,
						this.height | 0,
						this.tileX | 0,
						this.tileY | 0,
						this.tileCols | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						this.population | 0,
						this.births | 0,
						this.deaths | 0,
						this.sharedBuffer.byteOffset | 0
					);
				} else {
					this.nextGenerationSuperTileVN();
				}
			} else {
				if (Controller.useWASM && Controller.wasmEnableNextGenerationSuper && this.view.wasmEnabled) {
					WASM.nextGenerationSuperMoore(
						this.grid16.whole.byteOffset | 0,
						this.nextGrid16.whole.byteOffset | 0,
						this.grid16[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnAliveOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowAliveOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.width | 0,
						this.height | 0,
						this.tileX | 0,
						this.tileY | 0,
						this.tileCols | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						this.population | 0,
						this.births | 0,
						this.deaths | 0,
						this.sharedBuffer.byteOffset | 0
					);
				} else {
					this.nextGenerationSuperTileMoore();
				}
			}
		}

		if (Controller.useWASM && Controller.wasmEnableNextGenerationSuper && this.view.wasmEnabled) {
			this.population = this.sharedBuffer[0];
			this.births = this.sharedBuffer[1];
			this.deaths = this.sharedBuffer[2];
			this.zoomBox.leftX = this.sharedBuffer[3];
			this.zoomBox.bottomY = this.sharedBuffer[4];
			this.zoomBox.rightX = this.sharedBuffer[5];
			this.zoomBox.topY = this.sharedBuffer[6];
			this.aliveBox.leftX = this.sharedBuffer[7];
			this.aliveBox.bottomY = this.sharedBuffer[8];
			this.aliveBox.rightX = this.sharedBuffer[9];
			this.aliveBox.topY = this.sharedBuffer[10];
		}

		timing = performance.now() - timing;
		if (Controller.wasmTiming) {
			this.view.menuManager.updateTimingItem("nextSuper", timing, Controller.useWASM && Controller.wasmEnableNextGenerationSuper && this.view.wasmEnabled);
		}

		// clip bounding box to bounded grid
		if (this.boundedGridType !== -1) {
			// bottom left
			bLeftX = Math.round((this.width - width) / 2);
			bBottomY = Math.round((this.height - height) / 2);

			// top right
			bRightX = bLeftX + width - 1;
			bTopY = bBottomY + height - 1;

			if (zoomBox.leftX < bLeftX) {
				zoomBox.leftX = bLeftX;
			}
			if (zoomBox.rightX > bRightX) {
				zoomBox.rightX = bRightX;
			}
			if (zoomBox.bottomY < bBottomY) {
				zoomBox.bottomY = bBottomY;
			}
			if (zoomBox.topY > bTopY) {
				zoomBox.topY = bTopY;
			}
		}
	};

	// update the life grid region using tiles for RuleTable patterns
	Life.prototype.nextGenerationRuleTableTile = function() {
		var	/** @type {number} */ timing = performance.now();

		switch (this.ruleTableNeighbourhood) {
		// von Neumann
		case PatternConstants.ruleTableVN:
			if (Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled) {
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					// pick appropriate algo for state bits
					switch (this.ruleLoaderLookupBits) {
					case 1:
						WASM.nextGenerationRuleLoaderVNLookup1(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 2:
						WASM.nextGenerationRuleLoaderVNLookup2(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 3:
						WASM.nextGenerationRuleLoaderVNLookup3(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 4:
						WASM.nextGenerationRuleLoaderVNLookup4(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 5:
						WASM.nextGenerationRuleLoaderVNLookup5(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					default:
						WASM.nextGenerationRuleTableVN(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.flatRuleTableLUT.byteOffset | 0,
							this.flatRuleTableLUT.length | 0,
							this.ruleTableOutput.byteOffset | 0,
							this.ruleTableCompressedRules | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;
					}
				} else {
					WASM.nextGenerationRuleTableVN(
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.tileGrid.whole.length | 0,
						this.diedGrid.whole.byteOffset | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.flatRuleTableLUT.byteOffset | 0,
						this.flatRuleTableLUT.length | 0,
						this.ruleTableOutput.byteOffset | 0,
						this.ruleTableCompressedRules | 0,
						this.width | 0,
						this.height | 0,
						this.tileY | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.blankTileRow.byteOffset | 0,
						this.blankTileRow.length | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						LifeConstants.bottomRightSet | 0,
						LifeConstants.bottomSet | 0,
						LifeConstants.topRightSet | 0,
						LifeConstants.topSet | 0,
						LifeConstants.bottomLeftSet | 0,
						LifeConstants.topLeftSet | 0,
						LifeConstants.leftSet | 0,
						LifeConstants.rightSet | 0,
						this.sharedBuffer.byteOffset | 0
					);
				}
			} else {
				// check if a fast lookup is available
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					switch (this.ruleLoaderLookupBits) {
					case 1:
						this.nextGenerationRuleLoaderTileVNLookup1();
						break;

					case 2:
						this.nextGenerationRuleLoaderTileVNLookup2();
						break;

					case 3:
						this.nextGenerationRuleLoaderTileVNLookup3();
						break;

					case 4:
						this.nextGenerationRuleLoaderTileVNLookup4();
						break;

					case 5:
						this.nextGenerationRuleLoaderTileVNLookup5();
						break;

					default:
						this.nextGenerationRuleTreeTileVN();
						break;
					}
				} else {
					// no fast lookup so use standard routine
					this.nextGenerationRuleTableTileVN();
				}
				break;
			}
			break;

		// Moore
		case PatternConstants.ruleTableMoore:
			if (Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled) {
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					switch (this.ruleLoaderLookupBits) {
					case 1:
						WASM.nextGenerationRuleLoaderMooreLookup1(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 2:
						WASM.nextGenerationRuleLoaderMooreLookup2(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 3:
						WASM.nextGenerationRuleLoaderMooreLookup3(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					default:
						WASM.nextGenerationRuleTableMoore(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.flatRuleTableLUT.byteOffset | 0,
							this.flatRuleTableLUT.length | 0,
							this.ruleTableOutput.byteOffset | 0,
							this.ruleTableCompressedRules | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
					}
				} else {
					WASM.nextGenerationRuleTableMoore(
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.tileGrid.whole.length | 0,
						this.diedGrid.whole.byteOffset | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.flatRuleTableLUT.byteOffset | 0,
						this.flatRuleTableLUT.length | 0,
						this.ruleTableOutput.byteOffset | 0,
						this.ruleTableCompressedRules | 0,
						this.width | 0,
						this.height | 0,
						this.tileY | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.blankTileRow.byteOffset | 0,
						this.blankTileRow.length | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						LifeConstants.bottomRightSet | 0,
						LifeConstants.bottomSet | 0,
						LifeConstants.topRightSet | 0,
						LifeConstants.topSet | 0,
						LifeConstants.bottomLeftSet | 0,
						LifeConstants.topLeftSet | 0,
						LifeConstants.leftSet | 0,
						LifeConstants.rightSet | 0,
						this.sharedBuffer.byteOffset | 0
					);
				}
			} else {
				// check if a fast lookup is available
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					switch (this.ruleLoaderLookupBits) {
					case 1:
						this.nextGenerationRuleLoaderTileMooreLookup1();
						break;

					case 2:
						this.nextGenerationRuleLoaderTileMooreLookup2();
						break;

					case 3:
						this.nextGenerationRuleLoaderTileMooreLookup3();
						break;

					default:
						this.nextGenerationRuleTableTileMoore();
						break;
					}
				} else {
					// no fast lookup so use standard routine
					this.nextGenerationRuleTableTileMoore();
				}
			}
			break;

		// Hex
		case PatternConstants.ruleTableHex:
			if (Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled) {
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					switch (this.ruleLoaderLookupBits) {
					case 1:
						WASM.nextGenerationRuleLoaderHexLookup1(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 2:
						WASM.nextGenerationRuleLoaderHexLookup2(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 3:
						WASM.nextGenerationRuleLoaderHexLookup3(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					default:
						WASM.nextGenerationRuleTableHex(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.flatRuleTableLUT.byteOffset | 0,
							this.flatRuleTableLUT.length | 0,
							this.ruleTableOutput.byteOffset | 0,
							this.ruleTableCompressedRules | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
					}
				} else {
					WASM.nextGenerationRuleTableHex(
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.tileGrid.whole.length | 0,
						this.diedGrid.whole.byteOffset | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.flatRuleTableLUT.byteOffset | 0,
						this.flatRuleTableLUT.length | 0,
						this.ruleTableOutput.byteOffset | 0,
						this.ruleTableCompressedRules | 0,
						this.width | 0,
						this.height | 0,
						this.tileY | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.blankTileRow.byteOffset | 0,
						this.blankTileRow.length | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						LifeConstants.bottomRightSet | 0,
						LifeConstants.bottomSet | 0,
						LifeConstants.topRightSet | 0,
						LifeConstants.topSet | 0,
						LifeConstants.bottomLeftSet | 0,
						LifeConstants.topLeftSet | 0,
						LifeConstants.leftSet | 0,
						LifeConstants.rightSet | 0,
						this.sharedBuffer.byteOffset | 0
					);
				}
			} else {
				// check if a fast lookup is available
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					switch (this.ruleLoaderLookupBits) {
					case 1:
						this.nextGenerationRuleLoaderTileHexLookup1();
						break;

					case 2:
						this.nextGenerationRuleLoaderTileHexLookup2();
						break;

					case 3:
						this.nextGenerationRuleLoaderTileHexLookup3();
						break;

					default:
						this.nextGenerationRuleTableTileHex();
						break;
					}
				} else {
					// no fast lookup so use standard routine
					this.nextGenerationRuleTableTileHex();
				}
			}
			break;

		// 1D
		case PatternConstants.ruleTableOneD:
			this.nextGenerationRuleTableTile1D();
			break;
		}

		if (Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled) {
			this.population = this.sharedBuffer[0];
			this.births = this.sharedBuffer[1];
			this.deaths = this.sharedBuffer[2];
			this.zoomBox.leftX = this.sharedBuffer[3];
			this.zoomBox.bottomY = this.sharedBuffer[4];
			this.zoomBox.rightX = this.sharedBuffer[5];
			this.zoomBox.topY = this.sharedBuffer[6];
		}

		timing = performance.now() - timing;
		if (Controller.wasmTiming) {
			this.view.menuManager.updateTimingItem("nextRuleTable", timing, Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled);
		}
};

	// update the life grid region using tiles for 1D RuleTable patterns
	Life.prototype.nextGenerationRuleTableTile1D = function() {
		var	/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Array<Array<Uint32Array>>} */ lut = this.ruleTableLUT,
			/** @type {Array<Uint32Array>} */ lut0 = lut[0],
			/** @type {Array<Uint32Array>} */ lut1 = lut[1],
			/** @type {Array<Uint32Array>} */ lut2 = lut[2],
			/** @type {Uint32Array} */ lute = null,
			/** @type {Uint32Array} */ lutw = null,
			/** @type {Uint32Array} */ lutc = null,
			/** @type {Uint8Array} */ output = this.ruleTableOutput,
			/** @type {number} */ nCompressed = this.ruleTableCompressedRules,
			/** @type {number} */ isMatch = 0,
			/** @type {number} */ iRuleC = 0,
			/** @type {number} */ iBit = 0,
			/** @type {number} */ mask = 0,

			// cells
			/** @type {number} */ e = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,

			/** @type {number} */ state = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;
			rightX = leftX + xSize;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							// process each row of the tile
							while (y < topY) {
								// current row
								gridRow1 = grid[y];

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// get initial values for this row
								if (x === 0) {
									c = 0;
								} else {
									c = gridRow1[x - 1];
								}
								e = gridRow1[x];

								// process each cell along the tile row
								while (x < rightX - 1) {
									w = c;
									c = e;
									e = gridRow1[x + 1];
									lutc = lut0[c];
									lutw = lut1[w];
									lute = lut2[e];
									state = c;
									for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
										isMatch = lutc[iRuleC] & lutw[iRuleC] & lute[iRuleC];
										if (isMatch) {
											iBit = 0;
											mask = 1;
											while (!(isMatch & mask)) {
												iBit += 1;
												mask <<= 1;
											}
											state = output[(iRuleC << 5) + iBit];
											break;
										}
									}

									// check if state is alive
									nextRow[x] = state;
									if (state > 0) {
										population += 1;

										// update births
										if (c === 0) {
											births += 1;
										}
										rowOccupied |= rowIndex;
										colOccupied |= colIndex;
									} else {
										// check for death
										if (c > 0) {
											// update deaths
											deaths += 1;
										}
									}

									// check if any cells were alive in the source
									anyAlive |= c;

									// next column
									colIndex >>= 1;
									x += 1;
								}

								// handle right edge
								w = c;
								c = e;
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								lutc = lut0[c];
								lutw = lut1[w];
								lute = lut2[e];
								state = c;
								for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
									isMatch = lutc[iRuleC] & lutw[iRuleC] & lute[iRuleC];
									if (isMatch) {
										iBit = 0;
										mask = 1;
										while (!(isMatch & mask)) {
											iBit += 1;
											mask <<= 1;
										}
										state = output[(iRuleC << 5) + iBit];
										break;
									}
								}
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
						rightX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
					rightX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for von Neumann RuleTable patterns
	Life.prototype.nextGenerationRuleTableTileVN = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Array<Array<Uint32Array>>} */ lut = this.ruleTableLUT,
			/** @type {Array<Uint32Array>} */ lut0 = lut[0],
			/** @type {Array<Uint32Array>} */ lut1 = lut[1],
			/** @type {Array<Uint32Array>} */ lut2 = lut[2],
			/** @type {Array<Uint32Array>} */ lut3 = lut[3],
			/** @type {Array<Uint32Array>} */ lut4 = lut[4],
			/** @type {Uint32Array} */ lutn = null,
			/** @type {Uint32Array} */ lute = null,
			/** @type {Uint32Array} */ luts = null,
			/** @type {Uint32Array} */ lutw = null,
			/** @type {Uint32Array} */ lutc = null,
			/** @type {Uint8Array} */ output = this.ruleTableOutput,
			/** @type {number} */ nCompressed = this.ruleTableCompressedRules,
			/** @type {number} */ isMatch = 0,
			/** @type {number} */ iRuleC = 0,
			/** @type {number} */ iBit = 0,
			/** @type {number} */ mask = 0,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,

			/** @type {number} */ state = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;
			rightX = leftX + xSize;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// get initial values for this row
								if (x === 0) {
									c = 0;
								} else {
									c = gridRow1[x - 1];
								}
								e = gridRow1[x];

								// process each cell along the tile row
								while (x < rightX - 1) {
									w = c;
									c = e;
									n = gridRow0[x];
									e = gridRow1[x + 1];
									s = gridRow2[x];
									lutc = lut0[c];
									lutn = lut1[n];
									lute = lut2[e];
									luts = lut3[s];
									lutw = lut4[w];
									state = c;
									for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
										isMatch = lutc[iRuleC] & lutn[iRuleC];
										if (isMatch) {
											isMatch &= lute[iRuleC] & luts[iRuleC] & lutw[iRuleC];
											if (isMatch) {
												iBit = 0;
												mask = 1;
												while (!(isMatch & mask)) {
													iBit += 1;
													mask <<= 1;
												}
												state = output[(iRuleC << 5) + iBit];
												break;
											}
										}
									}

									// check if state is alive
									nextRow[x] = state;
									if (state > 0) {
										population += 1;

										// update births
										if (c === 0) {
											births += 1;
										}
										rowOccupied |= rowIndex;
										colOccupied |= colIndex;
									} else {
										// check for death
										if (c > 0) {
											// update deaths
											deaths += 1;
										}
									}

									// check if any cells were alive in the source tile
									anyAlive |= c;

									// next column
									colIndex >>= 1;
									x += 1;
								}

								// handle right edge
								w = c;
								c = e;
								n = gridRow0[x];
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];
								lutc = lut0[c];
								lutn = lut1[n];
								lute = lut2[e];
								luts = lut3[s];
								lutw = lut4[w];
								state = c;
								for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
									isMatch = lutc[iRuleC] & lutn[iRuleC];
									if (isMatch) {
										isMatch &= lute[iRuleC] & luts[iRuleC] & lutw[iRuleC];
										if (isMatch) {
											iBit = 0;
											mask = 1;
											while (!(isMatch & mask)) {
												iBit += 1;
												mask <<= 1;
											}
											state = output[(iRuleC << 5) + iBit];
											break;
										}
									}
								}
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
						rightX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
					rightX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for Moore RuleTable patterns
	Life.prototype.nextGenerationRuleTableTileMoore = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Array<Array<Uint32Array>>} */ lut = this.ruleTableLUT,
			/** @type {Array<Uint32Array>} */ lut0 = lut[0],
			/** @type {Array<Uint32Array>} */ lut1 = lut[1],
			/** @type {Array<Uint32Array>} */ lut2 = lut[2],
			/** @type {Array<Uint32Array>} */ lut3 = lut[3],
			/** @type {Array<Uint32Array>} */ lut4 = lut[4],
			/** @type {Array<Uint32Array>} */ lut5 = lut[5],
			/** @type {Array<Uint32Array>} */ lut6 = lut[6],
			/** @type {Array<Uint32Array>} */ lut7 = lut[7],
			/** @type {Array<Uint32Array>} */ lut8 = lut[8],
			/** @type {Uint32Array} */ lutnw = null,
			/** @type {Uint32Array} */ lutn = null,
			/** @type {Uint32Array} */ lutne = null,
			/** @type {Uint32Array} */ lutw = null,
			/** @type {Uint32Array} */ lutc = null,
			/** @type {Uint32Array} */ lute = null,
			/** @type {Uint32Array} */ lutsw = null,
			/** @type {Uint32Array} */ luts = null,
			/** @type {Uint32Array} */ lutse = null,
			/** @type {Uint8Array} */ output = this.ruleTableOutput,
			/** @type {number} */ nCompressed = this.ruleTableCompressedRules,
			/** @type {number} */ isMatch = 0,
			/** @type {number} */ iRuleC = 0,
			/** @type {number} */ iBit = 0,
			/** @type {number} */ mask = 0,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ sw = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;
			rightX = leftX + xSize;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// get initial values for this row
								if (x === 0) {
									n = 0;
									s = 0;
									c = 0;
								} else {
									n = gridRow0[x - 1];
									s = gridRow2[x - 1];
									c = gridRow1[x - 1];
								}
								ne = gridRow0[x];
								se = gridRow2[x];
								e = gridRow1[x];

								// process each cell along the tile row
								while (x < rightX - 1) {
									nw = n;
									n = ne;
									ne = gridRow0[x + 1];
									w = c;
									c = e;
									e = gridRow1[x + 1];
									sw = s;
									s = se;
									se = gridRow2[x + 1];
									state = c;
									lutc = lut0[c];
									lutn = lut1[n];
									lutne = lut2[ne];
									lute = lut3[e];
									lutse = lut4[se];
									luts = lut5[s];
									lutsw = lut6[sw];
									lutw = lut7[w];
									lutnw = lut8[nw];
									for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
										isMatch = lutc[iRuleC] & lutn[iRuleC];
										if (isMatch) {
											isMatch &= lutne[iRuleC] & lute[iRuleC];
											if (isMatch) {
												isMatch &= lutse[iRuleC] & luts[iRuleC];
												if (isMatch) {
													isMatch &= lutsw[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
													if (isMatch) {
														iBit = 0;
														mask = 1;
														while (!(isMatch & mask)) {
															iBit += 1;
															mask <<= 1;
														}
														state = output[(iRuleC << 5) + iBit];
														break;
													}
												}
											}
										}
									}

									// check if state is alive
									nextRow[x] = state;
									if (state > 0) {
										population += 1;

										// update births
										if (c === 0) {
											births += 1;
										}
										rowOccupied |= rowIndex;
										colOccupied |= colIndex;
									} else {
										// check for death
										if (c > 0) {
											// update deaths
											deaths += 1;
										}
									}

									// check if any cells were alive in the source tile
									anyAlive |= c;

									// next column
									colIndex >>= 1;
									x += 1;
								}

								// handle right edge
								nw = n;
								n = ne;
								w = c;
								c = e;
								sw = s;
								s = se;
								if (x === width - 1) {
									ne = 0;
									e = 0;
									se = 0;
								} else {
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								lutc = lut0[c];
								lutn = lut1[n];
								lutne = lut2[ne];
								lute = lut3[e];
								lutse = lut4[se];
								luts = lut5[s];
								lutsw = lut6[sw];
								lutw = lut7[w];
								lutnw = lut8[nw];
								state = c;
								for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
									isMatch = lutc[iRuleC] & lutn[iRuleC];
									if (isMatch) {
										isMatch &= lutne[iRuleC] & lute[iRuleC];
										if (isMatch) {
											isMatch &= lutse[iRuleC] & luts[iRuleC];
											if (isMatch) {
												isMatch &= lutsw[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
												if (isMatch) {
													iBit = 0;
													mask = 1;
													while (!(isMatch & mask)) {
														iBit += 1;
														mask <<= 1;
													}
													state = output[(iRuleC << 5) + iBit];
													break;
												}
											}
										}
									}
								}
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied flags
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
						rightX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
					rightX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for hexagonal RuleTable patterns
	Life.prototype.nextGenerationRuleTableTileHex = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Array<Array<Uint32Array>>} */ lut = this.ruleTableLUT,
			/** @type {Array<Uint32Array>} */ lut0 = lut[0],
			/** @type {Array<Uint32Array>} */ lut1 = lut[1],
			/** @type {Array<Uint32Array>} */ lut2 = lut[2],
			/** @type {Array<Uint32Array>} */ lut3 = lut[3],
			/** @type {Array<Uint32Array>} */ lut4 = lut[4],
			/** @type {Array<Uint32Array>} */ lut5 = lut[5],
			/** @type {Array<Uint32Array>} */ lut6 = lut[6],
			/** @type {Uint32Array} */ lutn = null,
			/** @type {Uint32Array} */ lute = null,
			/** @type {Uint32Array} */ luts = null,
			/** @type {Uint32Array} */ lutw = null,
			/** @type {Uint32Array} */ lutc = null,
			/** @type {Uint32Array} */ lutnw = null,
			/** @type {Uint32Array} */ lutse = null,
			/** @type {Uint8Array} */ output = this.ruleTableOutput,
			/** @type {number} */ nCompressed = this.ruleTableCompressedRules,
			/** @type {number} */ isMatch = 0,
			/** @type {number} */ iRuleC = 0,
			/** @type {number} */ iBit = 0,
			/** @type {number} */ mask = 0,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ se = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;
			rightX = leftX + xSize;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// get initial values for this row
								if (x === 0) {
									n = 0;
									s = 0;
									c = 0;
								} else {
									n = gridRow0[x - 1];
									s = gridRow2[x - 1];
									c = gridRow1[x - 1];
								}
								se = gridRow2[x];
								e = gridRow1[x];

								// process each cell along the tile row
								while (x < rightX - 1) {
									nw = n;
									n = gridRow0[x];
									w = c;
									c = e;
									e = gridRow1[x + 1];
									s = se;
									se = gridRow2[x + 1];
									lutc = lut0[c];
									lutn = lut1[n];
									lute = lut2[e];
									lutse = lut3[se];
									luts = lut4[s];
									lutw = lut5[w];
									lutnw = lut6[nw];
									state = c;
									for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
										isMatch = lutc[iRuleC] & lutn[iRuleC];
										if (isMatch) {
											isMatch &= lute[iRuleC] & lutse[iRuleC];
											if (isMatch) {
												isMatch &= luts[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
												if (isMatch) {
													iBit = 0;
													mask = 1;
													while (!(isMatch & mask)) {
														iBit += 1;
														mask <<= 1;
													}
													state = output[(iRuleC << 5) + iBit];
													break;
												}
											}
										}
									}

									// check if state is alive
									nextRow[x] = state;
									if (state > 0) {
										population += 1;

										// update births
										if (c === 0) {
											births += 1;
										}
										rowOccupied |= rowIndex;
										colOccupied |= colIndex;
									} else {
										// check for death
										if (c > 0) {
											// update deaths
											deaths += 1;
										}
									}

									// check if any cells were alive in the source tile
									anyAlive |= c;

									// next column
									colIndex >>= 1;
									x += 1;
								}

								// handle right edge
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								s = se;
								if (x === width - 1) {
									e = 0;
									se = 0;
								} else {
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								lutc = lut0[c];
								lutn = lut1[n];
								lute = lut2[e];
								lutse = lut3[se];
								luts = lut4[s];
								lutw = lut5[w];
								lutnw = lut6[nw];
								state = c;
								for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
									isMatch = lutc[iRuleC] & lutn[iRuleC];
									if (isMatch) {
										isMatch &= lute[iRuleC] & lutse[iRuleC];
										if (isMatch) {
											isMatch &= luts[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
											if (isMatch) {
												iBit = 0;
												mask = 1;
												while (!(isMatch & mask)) {
													iBit += 1;
													mask <<= 1;
												}
												state = output[(iRuleC << 5) + iBit];
												break;
											}
										}
									}
								}
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
						rightX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
					rightX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid using tiles for RuleTree patterns
	Life.prototype.nextGenerationRuleTreeTile = function() {
		var	/** @type {number} */ timing = performance.now();

		// check neighbourhood
		if (this.ruleTreeNeighbours === 4) {
			if (Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled) {
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					// pick appropriate algo for state bits
					switch (this.ruleLoaderLookupBits) {
					case 1:
						WASM.nextGenerationRuleLoaderVNLookup1(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 2:
						WASM.nextGenerationRuleLoaderVNLookup2(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 3:
						WASM.nextGenerationRuleLoaderVNLookup3(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 4:
						WASM.nextGenerationRuleLoaderVNLookup4(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 5:
						WASM.nextGenerationRuleLoaderVNLookup5(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					default:
						WASM.nextGenerationRuleTreeVN(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleTreeA.byteOffset | 0,
							this.ruleTreeB.byteOffset | 0,
							this.ruleTreeBase | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;
					}
				} else {
					WASM.nextGenerationRuleTreeVN(
						this.colourGrid.whole.byteOffset | 0,
						this.nextColourGrid.whole.byteOffset | 0,
						this.colourGrid[0].length | 0,
						this.tileGrid.whole.byteOffset | 0,
						this.nextTileGrid.whole.byteOffset | 0,
						this.colourTileHistoryGrid.whole.byteOffset | 0,
						this.tileGrid[0].length | 0,
						this.tileGrid.whole.length | 0,
						this.diedGrid.whole.byteOffset | 0,
						this.columnOccupied16.byteOffset | 0,
						this.columnOccupied16.length | 0,
						this.rowOccupied16.byteOffset | 0,
						this.rowOccupied16.length | 0,
						this.ruleTreeA.byteOffset | 0,
						this.ruleTreeB.byteOffset | 0,
						this.ruleTreeBase | 0,
						this.width | 0,
						this.height | 0,
						this.tileY | 0,
						this.tileRows | 0,
						this.tileCols | 0,
						this.blankTileRow.byteOffset | 0,
						this.blankTileRow.length | 0,
						this.blankColourRow.byteOffset | 0,
						this.counter | 0,
						LifeConstants.bottomRightSet | 0,
						LifeConstants.bottomSet | 0,
						LifeConstants.topRightSet | 0,
						LifeConstants.topSet | 0,
						LifeConstants.bottomLeftSet | 0,
						LifeConstants.topLeftSet | 0,
						LifeConstants.leftSet | 0,
						LifeConstants.rightSet | 0,
						this.sharedBuffer.byteOffset | 0
					);
				}
			} else {
				// von Neumann
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					// pick appropriate algo for state bits
					switch (this.ruleLoaderLookupBits) {
					case 1:
						this.nextGenerationRuleLoaderTileVNLookup1();
						break;

					case 2:
						this.nextGenerationRuleLoaderTileVNLookup2();
						break;

					case 3:
						this.nextGenerationRuleLoaderTileVNLookup3();
						break;

					case 4:
						this.nextGenerationRuleLoaderTileVNLookup4();
						break;

					case 5:
						this.nextGenerationRuleLoaderTileVNLookup5();
						break;

					default:
						this.nextGenerationRuleTreeTileVN();
						break;
					}
				} else {
					this.nextGenerationRuleTreeTileVN();
				}
			}
		} else {
			if (Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled) {
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					switch (this.ruleLoaderLookupBits) {
					case 1:
						WASM.nextGenerationRuleLoaderMooreLookup1(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 2:
						WASM.nextGenerationRuleLoaderMooreLookup2(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					case 3:
						WASM.nextGenerationRuleLoaderMooreLookup3(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleLoaderLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;

					default:
						WASM.nextGenerationRuleTreeMoore(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleTreeA.byteOffset | 0,
							this.ruleTreeB.byteOffset | 0,
							this.ruleTreeBase | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
						break;
					};
				} else {
					if (this.ruleTreePartialLookup !== null && this.ruleLoaderLookupEnabled) {
						WASM.nextGenerationRuleTreeMoorePartial4(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleTreeA.byteOffset | 0,
							this.ruleTreeB.byteOffset | 0,
							this.ruleTreePartialLookup.byteOffset | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
					} else {
						WASM.nextGenerationRuleTreeMoore(
							this.colourGrid.whole.byteOffset | 0,
							this.nextColourGrid.whole.byteOffset | 0,
							this.colourGrid[0].length | 0,
							this.tileGrid.whole.byteOffset | 0,
							this.nextTileGrid.whole.byteOffset | 0,
							this.colourTileHistoryGrid.whole.byteOffset | 0,
							this.tileGrid[0].length | 0,
							this.tileGrid.whole.length | 0,
							this.diedGrid.whole.byteOffset | 0,
							this.columnOccupied16.byteOffset | 0,
							this.columnOccupied16.length | 0,
							this.rowOccupied16.byteOffset | 0,
							this.rowOccupied16.length | 0,
							this.ruleTreeA.byteOffset | 0,
							this.ruleTreeB.byteOffset | 0,
							this.ruleTreeBase | 0,
							this.width | 0,
							this.height | 0,
							this.tileY | 0,
							this.tileRows | 0,
							this.tileCols | 0,
							this.blankTileRow.byteOffset | 0,
							this.blankTileRow.length | 0,
							this.blankColourRow.byteOffset | 0,
							this.counter | 0,
							LifeConstants.bottomRightSet | 0,
							LifeConstants.bottomSet | 0,
							LifeConstants.topRightSet | 0,
							LifeConstants.topSet | 0,
							LifeConstants.bottomLeftSet | 0,
							LifeConstants.topLeftSet | 0,
							LifeConstants.leftSet | 0,
							LifeConstants.rightSet | 0,
							this.sharedBuffer.byteOffset | 0
						);
					}
				}
			} else {
				if (this.ruleLoaderLookup !== null && this.ruleLoaderLookupEnabled && this.ruleLoaderStep === -1) {
					switch (this.ruleLoaderLookupBits) {
					case 1:
						this.nextGenerationRuleLoaderTileMooreLookup1();
						break;

					case 2:
						this.nextGenerationRuleLoaderTileMooreLookup2();
						break;

					case 3:
						this.nextGenerationRuleLoaderTileMooreLookup3();
						break;

					default:
						this.nextGenerationRuleTreeTileMoore();
						break;
					}
				} else {
					if (this.ruleTreePartialLookup !== null && this.ruleLoaderLookupEnabled) {
						this.nextGenerationRuleTreeTileMoorePartial4();
					} else {
						this.nextGenerationRuleTreeTileMoore();
					}
				}
			}
		}

		if (Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled) {
			this.population = this.sharedBuffer[0];
			this.births = this.sharedBuffer[1];
			this.deaths = this.sharedBuffer[2];
			this.zoomBox.leftX = this.sharedBuffer[3];
			this.zoomBox.bottomY = this.sharedBuffer[4];
			this.zoomBox.rightX = this.sharedBuffer[5];
			this.zoomBox.topY = this.sharedBuffer[6];
		}

		timing = performance.now() - timing;
		if (Controller.wasmTiming) {
			this.view.menuManager.updateTimingItem("nextRuleTree", timing, Controller.useWASM && Controller.wasmEnableNextGenerationRuleLoader && this.view.wasmEnabled);
		}
	};

	// update the life grid region using tiles for von Neumann RuleTree patterns
	Life.prototype.nextGenerationRuleTreeTileVN = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint32Array} */ a = this.ruleTreeA,
			/** @type {Uint8Array} */ b = this.ruleTreeB,
			/** @type {number} */ base = this.ruleTreeBase,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,
			/** @type {number} */ localCol = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;
								localCol = 0;

								// process each column in the row
								x = leftX;

								// get initial values for this row
								if (x === 0) {
									c = 0;
								} else {
									c = gridRow1[x - 1];
								}
								e = gridRow1[x];

								// process each cell along the tile row
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 = state;

								// check if state is alive
								if (state > 0) {
									population += 1;

									// update births
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									// check for death
									if (c > 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32  = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32  = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32  = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								w = c;
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								w = c;
								c = e;
								n = gridRow0[x];
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];
								state = b[a[a[a[a[base + n] + w] + e] + s] + c];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// if column had cells then update row flag
								if (localCol) {
									colOccupied |= localCol;
									rowOccupied |= rowIndex;
								}

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
											neighbours |= LifeConstants.bottomLeftSet;
										}
										if (colOccupied & 1) {
											neighbours |= LifeConstants.bottomRightSet;
										}
									}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// convert 2-state RuleTable to MAP
	/** @returns {string} */
	Life.prototype.ruleTableToMAP = function() {
		var	/** @type {string} */ result = "MAP",

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ sw = 0,

			// lookup
			/** @type {Array<Array<Uint32Array>>} */ lut = this.ruleTableLUT,
			/** @type {Array<Uint32Array>} */ lut0 = null,
			/** @type {Array<Uint32Array>} */ lut1 = null,
			/** @type {Array<Uint32Array>} */ lut2 = null,
			/** @type {Array<Uint32Array>} */ lut3 = null,
			/** @type {Array<Uint32Array>} */ lut4 = null,
			/** @type {Array<Uint32Array>} */ lut5 = null,
			/** @type {Array<Uint32Array>} */ lut6 = null,
			/** @type {Array<Uint32Array>} */ lut7 = null,
			/** @type {Array<Uint32Array>} */ lut8 = null,
			/** @type {Uint32Array} */ lutnw = null,
			/** @type {Uint32Array} */ lutn = null,
			/** @type {Uint32Array} */ lutne = null,
			/** @type {Uint32Array} */ lutw = null,
			/** @type {Uint32Array} */ lutc = null,
			/** @type {Uint32Array} */ lute = null,
			/** @type {Uint32Array} */ lutsw = null,
			/** @type {Uint32Array} */ luts = null,
			/** @type {Uint32Array} */ lutse = null,
			/** @type {Uint8Array} */ output = this.ruleTableOutput,
			/** @type {number} */ nCompressed = this.ruleTableCompressedRules,
			/** @type {number} */ isMatch = 0,
			/** @type {number} */ iRuleC = 0,
			/** @type {number} */ iBit = 0,
			/** @type {number} */ mask = 0,

			// iterator
			/** @type {number} */ i = 0,

			// calculated state
			/** @type {number} */ state = 0,

			// base64 characters
			/** @type {string} */ base64chars = this.manager.base64Characters,

			// character to add
			/** @type {number} */ character = 0;

		// check neighbourhood
		switch (this.ruleTableNeighbourhood) {

		// Moore
		case PatternConstants.ruleTableMoore:
			// get the lookups
			lut0 = lut[0];
			lut1 = lut[1];
			lut2 = lut[2];
			lut3 = lut[3];
			lut4 = lut[4];
			lut5 = lut[5];
			lut6 = lut[6];
			lut7 = lut[7];
			lut8 = lut[8];

			// iterate over all 512 possible combinations
			for (i = 0; i < 512; i += 1) {
				nw = (i & 256) >> 8;
				n = (i & 128) >> 7;
				ne = (i & 64) >> 6;
				w = (i & 32) >> 5;
				c = (i & 16) >> 4;
				e = (i & 8) >> 3;
				sw = (i & 4) >> 2;
				s = (i & 2) >> 1;
				se = (i & 1);

				// lookup the result
				state = c;
				lutc = lut0[c];
				lutn = lut1[n];
				lutne = lut2[ne];
				lute = lut3[e];
				lutse = lut4[se];
				luts = lut5[s];
				lutsw = lut6[sw];
				lutw = lut7[w];
				lutnw = lut8[nw];

				for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
					isMatch = lutc[iRuleC] & lutn[iRuleC];
					if (isMatch) {
						isMatch &= lutne[iRuleC] & lute[iRuleC];
						if (isMatch) {
							isMatch &= lutse[iRuleC] & luts[iRuleC];
							if (isMatch) {
								isMatch &= lutsw[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
								if (isMatch) {
									iBit = 0;
									mask = 1;
									while (!(isMatch & mask)) {
										iBit += 1;
										mask <<= 1;
									}
									state = output[(iRuleC << 5) + iBit];
									break;
								}
							}
						}
					}
				}

				// update the character
				character = (character << 1) | (state & 1);
				if ((i % 6) === 5) {
					result += base64chars[character];
					character = 0;
				}
			}

			// add final digit
			character <<= (6 - (i % 6));
			result += base64chars[character];

			break;

		// von Neumann
		case PatternConstants.ruleTableVN:
			// get the lookups
			lut0 = lut[0];
			lut1 = lut[1];
			lut2 = lut[2];
			lut3 = lut[3];
			lut4 = lut[4];

			// iterate over all 32 possible combinations
			for (i = 0; i < 32; i += 1) {
				n = (i & 16) >> 4;
				w = (i & 8) >> 3;
				c = (i & 4) >> 2;
				e = (i & 2) >> 1;
				s = (i & 1);

				// lookup the result
				state = c;
				lutc = lut0[c];
				lutn = lut1[n];
				lute = lut2[e];
				luts = lut3[s];
				lutw = lut4[w];

				for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
					isMatch = lutc[iRuleC] & lutn[iRuleC];
					if (isMatch) {
						isMatch &= lute[iRuleC] & luts[iRuleC] & lutw[iRuleC];
						if (isMatch) {
							iBit = 0;
							mask = 1;
							while (!(isMatch & mask)) {
								iBit += 1;
								mask <<= 1;
							}
							state = output[(iRuleC << 5) + iBit];
							break;
						}
					}
				}

				// update the character
				character = (character << 1) | (state & 1);
				if ((i % 6) === 5) {
					result += base64chars[character];
					character = 0;
				}
			}

			// add final digit
			character <<= (6 - (i % 6));
			result += base64chars[character];

			break;

		// Hexagonal
		case PatternConstants.ruleTableHex:
			// get the lookups
			lut0 = lut[0];
			lut1 = lut[1];
			lut2 = lut[2];
			lut3 = lut[3];
			lut4 = lut[4];
			lut5 = lut[5];
			lut6 = lut[6];

			// iterate over all 128 possible combinations
			for (i = 0; i < 128; i += 1) {
				s = (i & 64) >> 6;
				se = (i & 32) >> 5;
				w = (i & 16) >> 4;
				c = (i & 8) >> 3;
				e = (i & 4) >> 2;
				nw = (i & 2) >> 1;
				n = (i & 1);

				// lookup the result
				state = c;
				lutc = lut0[c];
				lutn = lut1[n];
				lute = lut2[e];
				lutse = lut3[se];
				luts = lut4[s];
				lutw = lut5[w];
				lutnw = lut6[nw];

				for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
					isMatch = lutc[iRuleC] & lutn[iRuleC];
					if (isMatch) {
						isMatch &= lute[iRuleC] & lutse[iRuleC];
						if (isMatch) {
							isMatch &= luts[iRuleC] & lutw[iRuleC] & lutnw[iRuleC];
							if (isMatch) {
								iBit = 0;
								mask = 1;
								while (!(isMatch & mask)) {
									iBit += 1;
									mask <<= 1;
								}
								state = output[(iRuleC << 5) + iBit];
								break;
							}
						}
					}
				}

				// update the character
				character = (character << 1) | (state & 1);
				if ((i % 6) === 5) {
					result += base64chars[character];
					character = 0;
				}
			}

			// add final digit
			character <<= (6 - (i % 6));
			result += base64chars[character];

			break;

		/* TBD not supported by MAP algo

		// One Dimensional
		case PatternConstants.ruleTableOneD:
			// get the lookups
			lut0 = lut[0];
			lut1 = lut[1];
			lut2 = lut[2];

			// iterate over all 8 possible combinations
			for (i = 0; i < 8; i += 1) {
				w = (i & 4) >> 2;
				c = (i & 2) >> 1;
				e = (i & 1);

				// lookup the result
				state = c;
				lutc = lut0[c];
				lutw = lut1[w];
				lute = lut2[e];

				for (iRuleC = 0; iRuleC < nCompressed; iRuleC += 1) {
					isMatch = lutc[iRuleC] & lutw[iRuleC] & lute[iRuleC];
					if (isMatch) {
						iBit = 0;
						mask = 1;
						while (!(isMatch & mask)) {
							iBit += 1;
							mask <<= 1;
						}
						state = output[(iRuleC << 5) + iBit];
						break;
					}
				}

				// update the character
				character = (character << 1) | (state & 1);
				if ((i % 6) === 5) {
					result += base64chars[character];
					character = 0;
				}
			}

			// add final digit
			character <<= (6 - (i % 6));
			result += base64chars[character];

			break;
		*/

		}

		return result;
	};

	// convert 2-state RuleTree to MAP
	/** @returns {string} */
	Life.prototype.ruleTreeToMAP = function() {
		var	/** @type {string} */ result = "MAP",

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ sw = 0,

			// lookup
			/** @type {Uint32Array} */ a = this.ruleTreeA,
			/** @type {Uint8Array} */ b = this.ruleTreeB,
			/** @type {number} */ base = this.ruleTreeBase,

			// iterator
			/** @type {number} */ i = 0,

			// calculated state
			/** @type {number} */ state = 0,

			// base64 characters
			/** @type {string} */ base64chars = this.manager.base64Characters,

			// character to add
			/** @type {number} */ character = 0;

		// check neighbourhood
		if (this.ruleTreeNeighbours === 8) {
			// iterate over all 512 possible combinations
			for (i = 0; i < 512; i += 1) {
				nw = (i & 256) >> 8;
				n = (i & 128) >> 7;
				ne = (i & 64) >> 6;
				w = (i & 32) >> 5;
				c = (i & 16) >> 4;
				e = (i & 8) >> 3;
				sw = (i & 4) >> 2;
				s = (i & 2) >> 1;
				se = (i & 1);

				// lookup the result
				state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];

				// update the character
				character = (character << 1) | (state & 1);
				if ((i % 6) === 5) {
					result += base64chars[character];
					character = 0;
				}
			}

			// add final digit
			character <<= (6 - (i % 6));
			result += base64chars[character];
		} else {
			// iterate over all 32 possible combinations
			for (i = 0; i < 32; i += 1) {
				n = (i & 16) >> 4;
				w = (i & 8) >> 3;
				c = (i & 4) >> 2;
				e = (i & 2) >> 1;
				s = (i & 1);

				// lookup the result
				state = b[a[a[a[a[base + n] + w] + e] + s] + c];

				// update the character
				character = (character << 1) | (state & 1);
				if ((i % 6) === 5) {
					result += base64chars[character];
					character = 0;
				}
			}

			// add final digit
			character <<= (6 - (i % 6));
			result += base64chars[character];
		}

		return result;
	};

	// convert a 2-state RuleLoader rule to MAP
	/** @returns {string} */
	Life.prototype.ruleLoaderToMAP = function() {
		var	/** @type {string} */ result = "";

		if (this.ruleTableOutput === null) {
			result = this.ruleTreeToMAP();
		} else {
			result = this.ruleTableToMAP();
		}

		return result;
	};

	// update the life grid region using partial fast lookup for Moore RuleTree rules with 4bit states
	Life.prototype.nextGenerationRuleTreeTileMoorePartial4 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Uint32Array} */ a = this.ruleTreeA,
			/** @type {Uint8Array} */ b = this.ruleTreeB,
			/** @type {Uint32Array} */ lookup = this.ruleTreePartialLookup,
			/** @type {number} */ index = 0,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ sw = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,
			/** @type {number} */ localCol = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;
								localCol = 0;

								// process each column in the row
								x = leftX;

								// get initial values for this row
								if (x === 0) {
									n = 0;
									s = 0;
									c = 0;
								} else {
									n = gridRow0[x - 1];
									s = gridRow2[x - 1];
									c = gridRow1[x - 1];
								}
								ne = gridRow0[x];
								se = gridRow2[x];
								e = gridRow1[x];

								// process each cell along the tile row
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];

								// check if state is alive
								nextRow[x] = state;
								if (state > 0) {
									population += 1;

									// update births
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									// check for death
									if (c > 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								nw = n;
								n = ne;
								w = c;
								c = e;
								sw = s;
								s = se;
								if (x === width - 1) {
									ne = 0;
									e = 0;
									se = 0;
								} else {
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								index = w | (n << 4) | (se << 8) | (sw << 12) | (ne << 16) | (nw << 20);
								state = b[a[a[lookup[index] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// if column had cells then update row flag
								if (localCol) {
									colOccupied |= localCol;
									rowOccupied |= rowIndex;
								}

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for Moore RuleTree patterns
	Life.prototype.nextGenerationRuleTreeTileMoore = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {Uint32Array} */ a = this.ruleTreeA,
			/** @type {Uint8Array} */ b = this.ruleTreeB,
			/** @type {number} */ base = this.ruleTreeBase,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ ne = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ sw = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint8Array>} */ nextGrid = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,
			/** @type {number} */ localCol = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid = this.colourGrid;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid = this.nextColourGrid;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid[y];

								// column index
								colIndex = 32768;
								localCol = 0;

								// process each column in the row
								x = leftX;

								// get initial values for this row
								if (x === 0) {
									n = 0;
									s = 0;
									c = 0;
								} else {
									n = gridRow0[x - 1];
									s = gridRow2[x - 1];
									c = gridRow1[x - 1];
								}
								ne = gridRow0[x];
								se = gridRow2[x];
								e = gridRow1[x];

								// process each cell along the tile row
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];

								// check if state is alive
								nextRow[x] = state;
								if (state > 0) {
									population += 1;

									// update births
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									// check for death
									if (c > 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								nw = n;
								n = ne;
								ne = gridRow0[x + 1];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								sw = s;
								s = se;
								se = gridRow2[x + 1];
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								nw = n;
								n = ne;
								w = c;
								c = e;
								sw = s;
								s = se;
								if (x === width - 1) {
									ne = 0;
									e = 0;
									se = 0;
								} else {
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								state = b[a[a[a[a[a[a[a[a[base + nw] + ne] + sw] + se] + n] + w] + e] + s] + c];
								nextRow[x] = state;
								if (state > 0) {
									population += 1;
									if (c === 0) {
										births += 1;
									}
									localCol |= colIndex;
								} else {
									if (c > 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// if column had cells then update row flag
								if (localCol) {
									colOccupied |= localCol;
									rowOccupied |= rowIndex;
								}

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for hexagonal RuleLoader patterns using array for 1bit states
	Life.prototype.nextGenerationRuleLoaderTileHexLookup1 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ w = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								n = gridRow0[x];
								c = gridRow1[x];
								s = gridRow2[x];
								if (x === 0) {
									// handle left edge of grid
									index = c | (s << 1) | (e << 2) | (n << 4) | (se << 5);
								} else {
									index = (gridRow0[x - 1] << 6) | (gridRow1[x - 1] << 3) | c | (s << 1) | (e << 2) | (n << 4) | (se << 5);
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 1) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								s = se;
								if (x === width - 1) {
									e = 0;
									se = 0;
								} else {
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								index = c | (s << 1) | (e << 2) | (w << 3) | (n << 4) | (se << 5) | (nw << 6);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for hexagonal RuleLoader patterns using array for 2bit states
	Life.prototype.nextGenerationRuleLoaderTileHexLookup2 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ w = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								n = gridRow0[x];
								c = gridRow1[x];
								s = gridRow2[x];
								if (x === 0) {
									// handle left edge of grid
									index = c | (s << 2) | (e << 4) | (n << 8) | (se << 10);
								} else {
									index = (gridRow0[x - 1] << 12) | (gridRow1[x - 1] << 6) | c | (s << 2) | (e << 4) | (n << 8) | (se << 10);
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 3) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								s = se;
								if (x === width - 1) {
									e = 0;
									se = 0;
								} else {
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								index = c | (s << 2) | (e << 4) | (w << 6) | (n << 8) | (se << 10) | (nw << 12);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for hexagonal RuleLoader patterns using array for 3bit states
	Life.prototype.nextGenerationRuleLoaderTileHexLookup3 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ nw = 0,
			/** @type {number} */ w = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								n = gridRow0[x];
								c = gridRow1[x];
								s = gridRow2[x];
								if (x === 0) {
									// handle left edge of grid
									index = c | (s << 3) | (e << 6) | (n << 12) | (se << 15);
								} else {
									index = (gridRow0[x - 1] << 18) | (gridRow1[x - 1] << 9) | c | (s << 3) | (e << 6) | (n << 12) | (se << 15);
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 7) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								e = gridRow1[x + 1];
								s = se;
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								nw = n;
								w = c;
								c = e;
								s = se;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								nw = n;
								n = gridRow0[x];
								w = c;
								c = e;
								s = se;
								if (x === width - 1) {
									e = 0;
									se = 0;
								} else {
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								index = c | (s << 3) | (e << 6) | (w << 9) | (n << 12) | (se << 15) | (nw << 18);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 7) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 7) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for Moore RuleLoader patterns using array for 1bit states
	Life.prototype.nextGenerationRuleLoaderTileMooreLookup1 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ ne = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								n = gridRow0[x];
								c = gridRow1[x];
								s = gridRow2[x];
								if (x === 0) {
									// handle left edge of grid
									index = (n << 3) | (c << 4) | (s << 5) | (ne << 6) | (e << 7) | (se << 8);
								} else {
									index = gridRow0[x - 1] | (gridRow1[x - 1] << 1) | (gridRow2[x - 1] << 2) | (n << 3) | (c << 4) | (s << 5) | (ne << 6) | (e << 7) | (se << 8);
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 16) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								if (x === width - 1) {
									ne = 0;
									e = 0;
									se = 0;
								} else {
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								index = (index >> 3) | (ne << 6) | (e << 7) | (se << 8);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 16) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 16) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for Moore RuleLoader patterns using array for 2bit states
	Life.prototype.nextGenerationRuleLoaderTileMooreLookup2 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ ne = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								n = gridRow0[x];
								c = gridRow1[x];
								s = gridRow2[x];
								if (x === 0) {
									// handle left edge of grid
									index = (n << 6) | (c << 8) | (s << 10) | (ne << 12) | (e << 14) | (se << 16);
								} else {
									index = gridRow0[x - 1] | (gridRow1[x - 1] << 2) | (gridRow2[x - 1] << 4) | (n << 6) | (c << 8) | (s << 10) | (ne << 12) | (e << 14) | (se << 16);
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 768) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								if (x === width - 1) {
									ne = 0;
									e = 0;
									se = 0;
								} else {
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								index = (index >> 6) | (ne << 12) | (e << 14) | (se << 16);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 768) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 768) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for Moore RuleLoader patterns using array for 3bit states
	Life.prototype.nextGenerationRuleLoaderTileMooreLookup3 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ ne = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ se = 0,
			/** @type {number} */ n = 0,
			/** @type {number} */ c = 0,
			/** @type {number} */ s = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								n = gridRow0[x];
								c = gridRow1[x];
								s = gridRow2[x];
								if (x === 0) {
									// handle left edge of grid
									index = (n << 9) | (c << 12) | (s << 15) | (ne << 18) | (e << 21) | (se << 24);
								} else {
									index = gridRow0[x - 1] | (gridRow1[x - 1] << 3) | (gridRow2[x - 1] << 6) | (n << 9) | (c << 12) | (s << 15) | (ne << 18) | (e << 21) | (se << 24);
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 45056) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								ne = gridRow0[x + 1];
								e = gridRow1[x + 1];
								se = gridRow2[x + 1];
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								if (x === width - 1) {
									ne = 0;
									e = 0;
									se = 0;
								} else {
									ne = gridRow0[x + 1];
									e = gridRow1[x + 1];
									se = gridRow2[x + 1];
								}
								index = (index >> 9) | (ne << 18) | (e << 21) | (se << 24);
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 45056) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 45056) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for von Neumann RuleLoader patterns using array for 1bit states
	Life.prototype.nextGenerationRuleLoaderTileVNLookup1 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ c = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								c = gridRow1[x];
								if (x === 0) {
									// handle left edge of grid
									index = (c << 3) | (e << 2) | (n << 1) | s;
								} else {
									index = (gridRow1[x - 1] << 4) | (c << 3) | (e << 2) | (n << 1) | s;
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 8) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								n = gridRow0[x];
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];
								index = ((index << 1) & 24) | (e << 2) | (n << 1) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 8) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 8) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for von Neumann RuleLoader patterns using array for 2bit states
	Life.prototype.nextGenerationRuleLoaderTileVNLookup2 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ c = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								c = gridRow1[x];
								if (x === 0) {
									// handle left edge of grid
									index = (c << 6) | (e << 4) | (n << 2) | s;
								} else {
									index = (gridRow1[x - 1] << 8) | (c << 6) | (e << 4) | (n << 2) | s;
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 192) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								n = gridRow0[x];
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];
								index = ((index << 2) & 960) | (e << 4) | (n << 2) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 192) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 192) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for von Neumann RuleLoader patterns using array for 3bit states
	Life.prototype.nextGenerationRuleLoaderTileVNLookup3 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ c = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								c = gridRow1[x];
								if (x === 0) {
									// handle left edge of grid
									index = (c << 9) | (e << 6) | (n << 3) | s;
								} else {
									index = (gridRow1[x - 1] << 12) | (c << 9) | (e << 6) | (n << 3) | s;
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 3584) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells are alive in source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								n = gridRow0[x];
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];
								index = ((index << 3) & 32256) | (e << 6) | (n << 3) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 3584) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 3584) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for von Neumann RuleLoader patterns using array for 4bit states
	Life.prototype.nextGenerationRuleLoaderTileVNLookup4 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ c = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								c = gridRow1[x];
								if (x === 0) {
									// handle left edge of grid
									index = (c << 12) | (e << 8) | (n << 4) | s;
								} else {
									index = (gridRow1[x - 1] << 16) | (c << 12) | (e << 8) | (n << 4) | s;
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 61440) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								n = gridRow0[x];
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];
								index = ((index << 4) & 1044480) | (e << 8) | (n << 4) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 61440) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 61440) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// update the life grid region using tiles for von Neumann RuleLoader patterns using array for 5bit states
	Life.prototype.nextGenerationRuleLoaderTileVNLookup5 = function() {
		var	/** @type {Uint8Array} */ gridRow0 = null,
			/** @type {Uint8Array} */ gridRow1 = null,
			/** @type {Uint8Array} */ gridRow2 = null,
			/** @type {Uint32Array} */ nextRow = null,
			/** @type {Uint8Array} */ lookup = this.ruleLoaderLookup,

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ c = 0,

			// states and counters
			/** @type {number} */ state = 0,
			/** @type {number} */ state32 = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ index = 0,

			/** @type {Array<Uint8Array>} */ grid = null,
			/** @type {Array<Uint32Array>} */ nextGrid32 = null,
			/** @type {Array<Uint32Array>} */ grid32 = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Array<Uint16Array>} */ nextTileGrid = null,
			/** @type {Array<Uint16Array>} */ diedGrid = this.diedGrid,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint16Array} */ nextTileRow = null,
			/** @type {Uint16Array} */ diedRow = null,
			/** @type {Uint16Array} */ belowNextTileRow = null,
			/** @type {Uint16Array} */ aboveNextTileRow = null,

			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @type {number} */ diedTiles = 0,
			/** @type {number} */ belowNextTiles = 0,
			/** @type {number} */ aboveNextTiles = 0,
			/** @type {number} */ anyAlive = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,

			// column occupied
			/** @type {Uint16Array} */ columnOccupied16 = this.columnOccupied16,
			/** @type {number} */ colOccupied = 0,
			/** @type {number} */ colIndex = 0,

			// row occupied
			/** @type {Uint16Array} */ rowOccupied16 = this.rowOccupied16,
			/** @type {number} */ rowOccupied = 0,
			/** @type {number} */ rowIndex = 0,

			// population statistics
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,

			// height of grid
			/** @type {number} */ height = this.height,

			// width of grid
			/** @type {number} */ width = this.width,

			// get the bounding box
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,

			// new box extent
			/** @type {number} */ newBottomY = height,
			/** @type {number} */ newTopY = -1,
			/** @type {number} */ newLeftX = width,
			/** @type {number} */ newRightX = -1,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (use height since we need bytes)
			/** @type {number} */ xSize = this.tileY,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// blank tile row for top and bottom
			/** @type {Uint16Array} */ blankTileRow = this.blankTileRow,

			// flags for edges of tile occupied
			/** @type {number} */ neighbours = 0;

		// switch buffers each generation
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			grid32 = this.nextColourGrid32;
			nextGrid32 = this.colourGrid32;
			tileGrid = this.nextTileGrid;
			nextTileGrid = this.tileGrid;
		} else {
			grid = this.colourGrid;
			grid32 = this.colourGrid32;
			nextGrid32 = this.nextColourGrid32;
			tileGrid = this.tileGrid;
			nextTileGrid = this.nextTileGrid;
		}

		// clear column occupied flags
		columnOccupied16.fill(0);

		// clear row occupied flags
		rowOccupied16.fill(0);

		// set the initial tile row
		bottomY = 0;
		topY = bottomY + ySize;

		// clear the next tile grid
		nextTileGrid.whole.fill(0);

		// scan each row of tiles
		for (th = 0; th < tileGrid.length; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row
			tileRow = tileGrid[th];
			nextTileRow = nextTileGrid[th];
			diedRow = diedGrid[th];

			// get the tile row below
			if (th > 0) {
				belowNextTileRow = nextTileGrid[th - 1];
			} else {
				belowNextTileRow = blankTileRow;
			}

			// get the tile row above
			if (th < tileRows - 1) {
				aboveNextTileRow = nextTileGrid[th + 1];
			} else {
				aboveNextTileRow = blankTileRow;
			}

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileRow[tw];
				diedTiles = 0;

				// check if any are occupied
				if (tiles) {
					// get the destination (with any set because of edges)
					nextTiles = nextTileRow[tw];
					belowNextTiles = belowNextTileRow[tw];
					aboveNextTiles = aboveNextTileRow[tw];

					// compute next generation for each set tile
					for (bit = 15; bit >= 0; bit -= 1) {
						// check if this tile needs computing
						if ((tiles & (1 << bit)) !== 0) {
							// mark no cells alive in the source tile
							anyAlive = 0;

							// mark no cells in this column
							colOccupied = 0;

							// mark no cells in the tile rows
							rowOccupied = 0;

							// clear the edge flags
							neighbours = 0;

							// process the bottom row of the tile
							y = bottomY;
							rowIndex = 32768;

							if (y === 0) {
								gridRow0 = this.blankColourRow;
							} else {
								gridRow0 = grid[y - 1];
							}
							gridRow1 = grid[y];

							// process each row of the tile
							while (y < topY) {
								// deal with bottom row of the grid
								if (y === 0) {
									gridRow0 = this.blankColourRow;
								} else {
									gridRow0 = grid[y - 1];
								}

								// current row
								gridRow1 = grid[y];

								// deal with top row of the grid
								if (y === this.height - 1) {
									gridRow2 = this.blankColourRow;
								} else {
									gridRow2 = grid[y + 1];
								}

								// get output row
								nextRow = nextGrid32[y];

								// column index
								colIndex = 32768;

								// process each column in the row
								x = leftX;

								// process each cell along the tile row
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								c = gridRow1[x];
								if (x === 0) {
									// handle left edge of grid
									index = (c << 15) | (e << 10) | (n << 5) | s;
								} else {
									index = (gridRow1[x - 1] << 20) | (c << 15) | (e << 10) | (n << 5) | s;
								}
								state32 = lookup[index];

								// check if state is alive
								if (state32 > 0) {
									population += 1;

									// update births
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									// check for death
									if ((index & 1015808) !== 0) {
										// update deaths
										deaths += 1;
									}
								}

								// check if any cells were alive in the source tile
								anyAlive |= c;

								// next column
								colIndex >>= 1;
								x += 1;

								// unroll 1
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 2
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 3
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 4
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 5
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 6
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 7
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 8
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 9
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 10
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 11
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 12
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state32 = lookup[index];
								if (state32 > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 13
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 8);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 14
								c = e;
								n = gridRow0[x];
								e = gridRow1[x + 1];
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 16);
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;
								colIndex >>= 1;
								x += 1;

								// unroll 15 (and handle right edge)
								c = e;
								n = gridRow0[x];
								if (x === width - 1) {
									e = 0;
								} else {
									e = gridRow1[x + 1];
								}
								s = gridRow2[x];
								index = ((index << 5) & 33521664) | (e << 10) | (n << 5) | s;
								state = lookup[index];
								state32 |= (state << 24);
								nextRow[x >> 2] = state32;
								if (state > 0) {
									population += 1;
									if ((index & 1015808) === 0) {
										births += 1;
									}
									rowOccupied |= rowIndex;
									colOccupied |= colIndex;
								} else {
									if ((index & 1015808) !== 0) {
										deaths += 1;
									}
								}
								anyAlive |= c;

								// no need for next column
								//colIndex >>= 1;
								//x += 1;

								// next row
								y += 1;
								rowIndex >>= 1;
							}

							// save the column occupied cells
							columnOccupied16[leftX >> 4] |= colOccupied;

							// update tile grid if any cells are set
							if (colOccupied) {
								// set this tile
								nextTiles |= (1 << bit);

								// check for neighbours
								if (rowOccupied & 1) {
									neighbours |= LifeConstants.topSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.topLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.topRightSet;
									}
								}

								if (rowOccupied & 32768) {
									neighbours |= LifeConstants.bottomSet;
									if (colOccupied & 32768) {
										neighbours |= LifeConstants.bottomLeftSet;
									}
									if (colOccupied & 1) {
										neighbours |= LifeConstants.bottomRightSet;
									}
								}

								if (colOccupied & 32768) {
									neighbours |= LifeConstants.leftSet;
								}

								if (colOccupied & 1) {
									neighbours |= LifeConstants.rightSet;
								}

								// update any neighbouring tiles
								if (neighbours) {
									// check whether left edge occupied
									if ((neighbours & LifeConstants.leftSet) !== 0) {
										if (bit < 15) {
											nextTiles |= (1 << (bit + 1));
										} else {
											// set in previous set if not at left edge
											if ((tw > 0) && (leftX > 0)) {
												nextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether right edge occupied
									if ((neighbours & LifeConstants.rightSet) !== 0) {
										if (bit > 0) {
											nextTiles |= (1 << (bit - 1));
										} else {
											// set carry over to go into next set if not at right edge
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												nextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether bottom edge occupied
									if ((neighbours & LifeConstants.bottomSet) !== 0) {
										// set in lower tile set
										belowNextTiles |= (1 << bit);
									}

									// check whether top edge occupied
									if ((neighbours & LifeConstants.topSet) !== 0) {
										// set in upper tile set
										aboveNextTiles |= (1 << bit);
									}

									// check whether bottom left occupied
									if ((neighbours & LifeConstants.bottomLeftSet) !== 0) {
										if (bit < 15) {
											belowNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												belowNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether bottom right occupied
									if ((neighbours & LifeConstants.bottomRightSet) !== 0) {
										if (bit > 0) {
											belowNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												belowNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}

									// check whether top left occupied
									if ((neighbours & LifeConstants.topLeftSet) !== 0) {
										if (bit < 15) {
											aboveNextTiles |= (1 << (bit + 1));
										} else {
											if ((tw > 0) && (leftX > 0)) {
												aboveNextTileRow[tw - 1] |= 1;
											}
										}
									}

									// check whether top right occupied
									if ((neighbours & LifeConstants.topRightSet) !== 0) {
										if (bit > 0) {
											aboveNextTiles |= (1 << (bit - 1));
										} else {
											if ((tw < tileCols16 - 1) && (leftX < width - 1)) {
												aboveNextTileRow[tw + 1] |= (1 << 15);
											}
										}
									}
								}
							} else {
								// all the cells in the tile died so check if any cells were alive in the source
								if (anyAlive) {
									diedTiles |= 1 << bit;
								}
							}

							// save the row occupied falgs
							rowOccupied16[th] |= rowOccupied;
						}

						// next tile columns
						leftX += xSize;
					}

					// save the tile groups
					nextTileRow[tw] |= nextTiles;
					if (th > 0) {
						belowNextTileRow[tw] |= belowNextTiles;
					}
					if (th < tileRows - 1) {
						aboveNextTileRow[tw] |= aboveNextTiles;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// update tiles where all cells died
				diedRow[tw] = diedTiles;
			}

			// next tile rows
			bottomY += ySize;
			topY += ySize;
		}

		// update bounding box
		this.updateBoundingBox(columnOccupied16, rowOccupied16, newLeftX, newBottomY, newRightX, newTopY, zoomBox);

		// clear the blank tile row since it may have been written to at top and bottom
		blankTileRow.fill(0);

		// clear tiles in source that died
		this.clearTilesThatDied(grid32);

		// set the history tile grid to the colour tile grid
		for (y = 0; y < this.colourTileHistoryGrid.whole.length; y += 1) {
			this.colourTileHistoryGrid.whole[y] |= tileGrid.whole[y] | nextTileGrid.whole[y];
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;
	};

	// next generation for PCA rules
	Life.prototype.nextGenerationPCATile = function() {
		var	/** @type {Uint16Array} */ indexLookup = this.margolusLookup1,
			/** @type {number} */ y = 0,
			/** @type {number} */ x = 0,
			/** @type {Array<Uint8Array>} */ grid = this.colourGrid,
			/** @type {Array<Uint8Array>} */ nextGrid = this.nextColourGrid,
			/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox,
			/** @type {number} */ leftX = historyBox.leftX,
			/** @type {number} */ bottomY = historyBox.bottomY,
			/** @type {number} */ rightX = historyBox.rightX,
			/** @type {number} */ topY = historyBox.topY,
			/** @type {number} */ state = 0,
			/** @type {number} */ index = 0,
			/** @type {number} */ population = 0,
			/** @type {number} */ births = 0,
			/** @type {number} */ deaths = 0,
			/** @type {number} */ nLeftX = this.width,
			/** @type {number} */ nBottomY = this.height,
			/** @type {number} */ nRightX = 0,
			/** @type {number} */ nTopY = 0,
			/** @type {number} */ zLeftX = this.width,
			/** @type {number} */ zBottomY = this.height,
			/** @type {number} */ zRightX = 0,
			/** @type {number} */ zTopY = 0,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileHistoryGrid,
			/** @type {Uint8Array} */ aboveRow = null,
			/** @type {Uint8Array} */ gridRow = null,
			/** @type {Uint8Array} */ belowRow = null,
			/** @type {Uint16Array} */ tileRow = null,
			/** @type {Uint8Array} */ nextRow = null,
			/** @type {boolean} */ rowAlive = false,
			/** @type {boolean} */ zAlive = false,
			/** @type {Uint8Array} */ bitCounts = this.bitCounts16,

			// maximum dead state number
			/** @type {number} */ deadState = this.historyStates,

			// minimum dead state number
			/** @type {number} */ minDeadState = (this.historyStates > 0 ? 1 : 0),

			// cells
			/** @type {number} */ n = 0,
			/** @type {number} */ e = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ w = 0,

			// bounded grid width and height
			/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// bottom left
			/** @type {number} */ bLeftX = Math.round((this.width - width) / 2),
			/** @type {number} */ bBottomY = Math.round((this.height - height) / 2),

			// top right
			/** @type {number} */ bRightX = bLeftX + width - 1,
			/** @type {number} */ bTopY = bBottomY + height - 1;

		// check for bounded grid
		if (this.boundedGridType !== -1) {
			if (this.boundedGridWidth === 0) {
				bLeftX = 0;
				bRightX = this.width - 1;
			}

			if (this.boundedGridHeight === 0) {
				bBottomY = 0;
				bTopY = this.height - 1;
			}

			leftX = bLeftX + 1;
			bottomY = bBottomY + 1;
			rightX = bRightX - 1;
			topY = bTopY - 1;
		}

		// check for change in playback direction
		if (this.reversePending) {
			this.reverseMargolus = !this.reverseMargolus;
			this.reversePending = false;
		}

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextColourGrid;
			nextGrid = this.colourGrid;

			// check for reverse playback
			if (this.reverseMargolus) {
				if (this.altSpecified) {
					// use reverse 2
					indexLookup = this.margolusReverseLookup2;
				} else {
					// always use reverse 1 if alt not specified
					indexLookup = this.margolusReverseLookup1;
				}
			}
		} else {
			grid = this.colourGrid;
			nextGrid = this.nextColourGrid;
			if (this.altSpecified) {
				indexLookup = this.margolusLookup2;
			}

			// check for reverse playback
			if (this.reverseMargolus) {
				if (this.altSpecified) {
					// use reverse 1
					indexLookup = this.margolusReverseLookup1;
				} else {
					// always use reverse 1 if alt not specified
					indexLookup = this.margolusReverseLookup1;
				}
			}
		}

		// ensure on display
		if (bottomY < 2) {
			bottomY = 2;
		}
		if (topY > this.height - 3) {
			topY = this.height - 3;
		}
		if (leftX < 1) {
			leftX = 1;
		}
		if (rightX > this.width - 3) {
			rightX = this.width - 3;
		}

		// process each cell in the bounding box
		for (y = bottomY - 1; y <= topY + 1; y += 1) {
			aboveRow = grid[y - 1];
			gridRow = grid[y];
			belowRow = grid[y + 1];
			tileRow = colourTileGrid[y >> 4];
			nextRow = nextGrid[y];

			// process each row in the bounding box
			rowAlive = false;
			zAlive = false;
			w = gridRow[leftX - 2];
			for (x = leftX - 1; x <= rightX + 1; x += 1) {
				n = belowRow[x];
				e = gridRow[x + 1];
				s = aboveRow[x];

				if (deadState > 0) {
					n = (n <= deadState ? 0 : n - deadState);
					e = (e <= deadState ? 0 : e - deadState);
					s = (s <= deadState ? 0 : s - deadState);
					w = (w <= deadState ? 0 : w - deadState);
				}

				index = w | (n << 4) | (e << 8) | (s << 12);
				state = indexLookup[index];
				w = gridRow[x];

				// check if state is alive
				if (state > 0) {
					population += bitCounts[state];
					nextRow[x] = state + deadState;
					rowAlive = true;
					tileRow[x >> 8] = 65535;

					// update bounding box
					if (x < nLeftX) {
						nLeftX = x;
					}
					if (x > nRightX) {
						nRightX = x;
					}
				} else {
					// check for death
					state = w;
					if (state > deadState) {
						state = deadState;
					} else {
						// check for dying
						if (state > minDeadState) {
							state -= 1;
						}
					}
					nextRow[x] = state;
					if (state > 0) {
						rowAlive = true;
						if (x < nLeftX) {
							nLeftX = x;
						}
						if (x > nRightX) {
							nRightX = x;
						}
					}
					state = 0;
				}

				// updates births and deaths from the 4 cell bitmap
				if (w > deadState) {
					w -= deadState;
					births += bitCounts[state & ~w];
					deaths += bitCounts[w & ~state];
					w += deadState;
				} else {
					births += bitCounts[state];
				}

				// check for alive states
				if (state > 0) {
					if (x < zLeftX) {
						zLeftX = x;
					}
					if (x > zRightX) {
						zRightX = x;
					}
					zAlive = true;
				}
			}

			// if any cells alive in the row then update bounding box
			if (rowAlive) {
				if (y < nBottomY) {
					nBottomY = y;
				}
				if (y > nTopY) {
					nTopY = y;
				}
			}
			if (zAlive) {
				if (y < zBottomY) {
					zBottomY = y;
				}
				if (y > zTopY) {
					zTopY = y;
				}
			}
		}

		// save statistics
		this.population = population;
		this.births = births;
		this.deaths = deaths;

		// update bounding boxes
		historyBox.leftX = nLeftX;
		historyBox.bottomY = nBottomY;
		historyBox.rightX = nRightX;
		historyBox.topY = nTopY;
		zoomBox.leftX = zLeftX;
		zoomBox.bottomY = zBottomY;
		zoomBox.rightX = zRightX;
		zoomBox.topY = zTopY;
	};

	// convert life grid region to pens using cell age
	Life.prototype.convertToPensTileRegular = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {Array<Uint16Array>} */ colourGrid16 = this.colourGrid16,
			/** @type {number} */ value16 = 0,
			/** @type {Uint16Array} */ colourGridRow16 = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Uint16Array} */ colourLookup = this.colourLookup17,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,

			// whether the tile is alive
			/** @type {number} */ tileAlive = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows;

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// check start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];
				nextTiles = 0;

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// flag nothing alive in the tile
							tileAlive = 0;

							// process each row
							h = bottomY;
							while (h < topY) {
								// get the grid and colour grid row
								gridRow = grid[h];
								colourGridRow16 = colourGrid16[h];

								// get correct starting colour index
								cr = (leftX << 3);

								// process each 16bit chunk (16 cells) along the row
								nextCell = gridRow[leftX];

								// lookup next colour
								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 49152) << 1)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								cr += 1;

								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 12288) << 3)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								cr += 1;

								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 3072) << 5)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								cr += 1;

								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 768) << 7)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								cr += 1;

								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 192) << 9)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								cr += 1;

								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 48) << 11)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								cr += 1;

								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 12) << 13)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								cr += 1;

								value16 = colourLookup[colourGridRow16[cr] | ((nextCell & 3) << 15)];
								tileAlive |= value16;
								colourGridRow16[cr] = value16;
								// cr += 1 - no need for final increments they will be reset next row

								// next row
								h += 1;
							}

							// check if the tile was alive (has any cells not completely faded)
							if ((tileAlive & 0xfefe) !== 0) {
								// update tile flag
								nextTiles |= (1 << b);
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = nextTiles;
				colourTileHistoryRow[tw] |= nextTiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// convert life grid region to pens using alive/dead
	Life.prototype.convertToPensTile2 = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {Uint16Array} */ colourLookup = this.colourLookup16,
			/** @type {Array<Uint16Array>} */ colourGrid16 = this.colourGrid16,
			/** @type {number} */ value16 = 0,
			/** @type {Uint16Array} */ colourGridRow16 = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,

			// whether the tile is alive
			/** @type {number} */ tileAlive = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows;

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// check start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];
				nextTiles = 0;

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// flag nothing alive in the tile
							tileAlive = 0;

							// process each row
							h = bottomY;
							while (h < topY) {
								// get the grid and colour grid row
								gridRow = grid[h];
								colourGridRow16 = colourGrid16[h];

								// get correct starting colour index
								cr = (leftX << 3);

								// process each 16bit chunk (16 cells) along the row
								nextCell = gridRow[leftX];
								if (nextCell) {
									// lookup next colour
									value16 = colourLookup[(nextCell >> 14) & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[(nextCell >> 12) & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[(nextCell >> 10) & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[(nextCell >> 8) & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[(nextCell >> 6) & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[(nextCell >> 4) & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[(nextCell >> 2) & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[nextCell & 3];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									// cr += 1 - no need for final increments they will be reset next row
								} else {
									colourGridRow16[cr] = 0;
									colourGridRow16[cr + 1] = 0;
									colourGridRow16[cr + 2] = 0;
									colourGridRow16[cr + 3] = 0;
									colourGridRow16[cr + 4] = 0;
									colourGridRow16[cr + 5] = 0;
									colourGridRow16[cr + 6] = 0;
									colourGridRow16[cr + 7] = 0;
								}

								// next row
								h += 1;
							}

							// check if the tile was alive
							if (tileAlive !== 0) {
								// update tile flag
								nextTiles |= (1 << b);
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = nextTiles;
				colourTileHistoryRow[tw] |= nextTiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// convert life grid region to pens using neighbour count
	Life.prototype.convertToPensTileNeighbourCount = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ s = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {number} */ aboveCell = 0,
			/** @type {number} */ belowCell = 0,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourGridRow = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Uint16Array} */ aboveRow = null,
			/** @type {Uint16Array} */ belowRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {Uint16Array} */ blankGridRow = this.blankRow16,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bit = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,
			/** @const {number} */ aliveStart = LifeConstants.aliveStart,
			/** @type {number} */ count = 0,

			// whether the tile is alive
			/** @type {number} */ tileAlive = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows;

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];
				nextTiles = 0;

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// flag nothing alive in the tile
							tileAlive = 0;

							// process each row
							h = bottomY;
							while (h < topY) {
								// get the grid and colour grid row
								gridRow = grid[h];
								if (h === 0) {
									aboveRow = blankGridRow;
								} else {
									aboveRow = grid[h - 1];
								}

								if (h >= this.height - 1) {
									belowRow = blankGridRow;
								} else {
									belowRow = grid[h + 1];
								}

								colourGridRow = colourGrid[h];

								// get correct starting colour index
								cr = (leftX << 4);

								// get next 16 cells
								nextCell = gridRow[leftX];
								if (nextCell) {
									aboveCell = aboveRow[leftX];
									belowCell = belowRow[leftX];

									count = 0;
									bit = 15;

									// process left cell
									if ((nextCell & (1 << bit)) !== 0) {
										// handle left edge
										var leftTop = aboveRow[leftX - 1] & 1;
										var leftMid = gridRow[leftX - 1] & 1;
										var leftBottom = belowRow[leftX - 1] & 1;

										count = 5 * ((nextCell & (1 << (bit - 1))) ? 1 : 0) +
											5 * leftMid +
											((aboveCell & (1 << (bit - 1))) ? 1 : 0) +
											5 * ((aboveCell & (1 << bit)) ? 1 : 0) +
											leftTop +
											((belowCell & (1 << (bit - 1))) ? 1 : 0) +
											5 * ((belowCell & (1 << bit)) ? 1 : 0) +
											leftBottom;

										s = aliveStart + count;
										tileAlive |= s;
									} else {
										// cell is dead
										s = 0;
									}

									colourGridRow[cr] = s;
									cr += 1;

									// process middle bits
									for (bit = 14; bit >= 1; bit -= 1) {
										// check if the cell is alive
										if ((nextCell & (1 << bit)) !== 0) {
											// calculate the number of neighbours
											count = 5 * ((nextCell & (1 << (bit - 1))) ? 1 : 0) +
												5 * ((nextCell & (1 << (bit + 1))) ? 1 : 0) +
												((aboveCell & (1 << (bit - 1))) ? 1 : 0) +
												5 * ((aboveCell & (1 << bit)) ? 1 : 0) +
												((aboveCell & (1 << (bit + 1))) ? 1 : 0) +
												((belowCell & (1 << (bit - 1))) ? 1 : 0) +
												5 * ((belowCell & (1 << bit)) ? 1 : 0) +
												((belowCell & (1 << (bit + 1))) ? 1 : 0);

											s = aliveStart + count;
											tileAlive |= s;
										} else {
											// cell is dead
											s = 0;
										}

										colourGridRow[cr] = s;
										cr += 1;
									}

									// process right cell
									if ((nextCell & (1 << bit)) !== 0) {
										// handle right edge
										var rightTop = aboveRow[leftX + 1] >> 15;
										var rightMid = gridRow[leftX + 1] >> 15;
										var rightBottom = belowRow[leftX + 1] >> 15;

										count = 5 * rightMid +
											5 * ((nextCell & (1 << (bit + 1))) ? 1 : 0) +
											rightTop +
											5 * ((aboveCell & (1 << bit)) ? 1 : 0) +
											((aboveCell & (1 << (bit + 1))) ? 1 : 0) +
											rightBottom +
											5 * ((belowCell & (1 << bit)) ? 1 : 0) +
											((belowCell & (1 << (bit + 1))) ? 1 : 0);

										s = aliveStart + count;
										tileAlive |= s;
									} else {
										// cell is dead
										s = 0;
									}

									colourGridRow[cr] = s;
									cr += 1;
								} else {
									for (bit = 0; bit < 4; bit += 1) {
										colourGridRow[cr] = 0;
										colourGridRow[cr + 1] = 0;
										colourGridRow[cr + 2] = 0;
										colourGridRow[cr + 3] = 0;
										cr += 4;
									}
								}

								// next row
								h += 1;
							}

							// check if the tile was alive (has any cells not completely faded)
							if (tileAlive) {
								// update tile flag
								nextTiles |= (1 << b);
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = nextTiles;
				colourTileHistoryRow[tw] |= nextTiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// convert life grid region to pens using cell position
	Life.prototype.convertToPensTileRainbow = function() {
		var	/** @type {number} */ h = 0,
			/** @type {number} */ cr = 0,
			/** @type {number} */ nextCell = 0,
			/** @type {Array<Uint16Array>} */ colourGrid16 = this.colourGrid16,
			/** @type {number} */ value16 = 0,
			/** @type {Uint16Array} */ colourGridRow16 = null,
			/** @type {Uint16Array} */ colourTileRow = null,
			/** @type {Uint16Array} */ colourTileHistoryRow = null,
			/** @type {Array<Uint16Array>} */ colourTileHistoryGrid = this.colourTileHistoryGrid,
			/** @type {Array<Uint16Array>} */ colourTileGrid = this.colourTileGrid,
			/** @type {Uint16Array} */ colourLookup = this.colourLookup16,
			/** @type {Array<Uint16Array>} */ grid = null,
			/** @type {Uint16Array} */ gridRow = null,
			/** @type {Array<Uint16Array>} */ tileGrid = null,
			/** @type {Uint16Array} */ tileGridRow = null,
			/** @type {number} */ th = 0,
			/** @type {number} */ tw = 0,
			/** @type {number} */ b = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ tiles = 0,
			/** @type {number} */ nextTiles = 0,

			// whether the tile is alive
			/** @type {number} */ tileAlive = 0,

			// set tile height
			/** @type {number} */ ySize = this.tileY,

			// tile width (in 16bit chunks)
			/** @type {number} */ xSize = this.tileX >> 1,

			// tile rows
			/** @type {number} */ tileRows = this.tileRows,

			// tile columns in 16 bit values
			/** @type {number} */ tileCols16 = this.tileCols >> 4,

			// starting and ending tile row
			/** @type {number} */ tileStartRow = 0,
			/** @type {number} */ tileEndRow = tileRows;

		// select the correct grid
		if ((this.counter & 1) !== 0) {
			grid = this.nextGrid16;
			tileGrid = this.nextTileGrid;
		} else {
			grid = this.grid16;
			tileGrid = this.tileGrid;
		}

		// check start and end row are in range
		if (tileStartRow < 0) {
			tileStartRow = 0;
		}
		if (tileEndRow > tileRows) {
			tileEndRow = tileRows;
		}

		// set the initial tile row
		bottomY = tileStartRow << this.tilePower;
		topY = bottomY + ySize;

		// scan each row of tiles
		for (th = tileStartRow; th < tileEndRow; th += 1) {
			// set initial tile column
			leftX = 0;

			// get the tile row and colour tile rows
			tileGridRow = tileGrid[th];
			colourTileRow = colourTileGrid[th];
			colourTileHistoryRow = colourTileHistoryGrid[th];

			// scan each set of tiles
			for (tw = 0; tw < tileCols16; tw += 1) {
				// get the next tile group (16 tiles)
				tiles = tileGridRow[tw] | colourTileRow[tw];
				nextTiles = 0;

				// check if any are occupied
				if (tiles) {
					// compute next colour for each tile in the set
					for (b = 15; b >= 0; b -= 1) {
						// check if this tile is occupied
						if ((tiles & (1 << b)) !== 0) {
							// flag nothing alive in the tile
							tileAlive = 0;

							// process each row
							h = bottomY;
							while (h < topY) {
								// get the grid and colour grid row
								gridRow = grid[h];
								colourGridRow16 = colourGrid16[h];

								// get correct starting colour index
								cr = (leftX << 3);

								// process each 16bit chunk (16 cells) along the row
								nextCell = gridRow[leftX];
								if (nextCell) {
									// lookup next colour
									value16 = colourLookup[((nextCell & 49152) >> 7) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[((nextCell & 12288) >> 5) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[((nextCell & 3072) >> 3) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[((nextCell & 768) >> 1) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[((nextCell & 192) << 1) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[((nextCell & 48) << 3) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[((nextCell & 12) << 5) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									cr += 1;

									value16 = colourLookup[((nextCell & 3) << 7) | ((h + cr + cr) & 127)];
									tileAlive |= value16;
									colourGridRow16[cr] = value16;
									// cr += 1 - no need for final increments they will be reset next row
								} else {
									colourGridRow16[cr] = 0;
									colourGridRow16[cr + 1] = 0;
									colourGridRow16[cr + 2] = 0;
									colourGridRow16[cr + 3] = 0;
									colourGridRow16[cr + 4] = 0;
									colourGridRow16[cr + 5] = 0;
									colourGridRow16[cr + 6] = 0;
									colourGridRow16[cr + 7] = 0;
								}

								// next row
								h += 1;
							}

							// check if the tile was alive (has any cells not completely faded)
							if (((tileAlive & 255) > 1) || ((tileAlive >> 8) > 1)) {
								// update tile flag
								nextTiles |= (1 << b);
							}
						}

						// next tile columns
						leftX += xSize;
					}
				} else {
					// skip tile set
					leftX += xSize << 4;
				}

				// save the tile group
				colourTileRow[tw] = nextTiles;
				colourTileHistoryRow[tw] |= nextTiles;
			}

			// next tile row
			bottomY += ySize;
			topY += ySize;
		}
	};

	// get zoom at a particular generation
	/** @returns {number} */
	Life.prototype.zoomAt = function(/** @type {number} */ generation, /** @type {number} */ trackN, /** @type {number} */ trackE, /** @type {number} */ trackS, /** @type {number} */ trackW, /** @type {number} */ displayWidth, /** @type {number} */ displayHeight, /** @type {number} */ minZoom, /** @type {number} */ maxZoom, /** @type {number} */ scaleFactor) {
		var	/** @type {number} */ result = 0,
			/** @type {BoundingBox} */ initialBox = this.initialBox,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ width = 0,
			/** @type {number} */ height = 0,
			/** @type {number} */ zoomX = 0,
			/** @type {number} */ zoomY = 0;

		// compute the track box
		topY = trackS * generation + initialBox.topY;
		rightX = trackE * generation + initialBox.rightX;
		bottomY = trackN * generation + initialBox.bottomY;
		leftX = trackW * generation + initialBox.leftX;

		// compute the width and height
		width = rightX - leftX + 1;
		height = topY - bottomY + 1;

		// check for Hex neighbourhood
		if (this.isHex) {
			leftX -= (topY / 2);
			rightX -= (bottomY / 2);
			width = rightX - leftX + 1;
		}

		// ensure width and height are non zero
		if (width === 0) {
			width = 1;
		}
		if (height === 0) {
			height = 1;
		}

		// compute the zoom in each direction
		zoomX = displayWidth / width;
		zoomY = displayHeight / height;

		// select the zoom from the smallest ratio
		if (zoomX > zoomY) {
			result = zoomY;
		} else {
			result = zoomX;
		}

		// apply scale factor
		result = Math.round(result * 20000 / scaleFactor) / 20000;

		// ensure in range
		if (result < minZoom) {
			result = minZoom;
		} else {
			if (result > maxZoom) {
				result = maxZoom;
			}
		}

		// return the zoom
		return result;
	};

	// make zoom an exact value if close enough to the exact value
	/** @returns {number} */
	Life.prototype.makeIntegerZoom = function(/** @type {number} */ zoom) {
		var	/** @type {number} */ testZoom = zoom,
			/** @type {number} */ percent = 0,
			/** @type {number} */ intZoom;

		// check for negative zooms and convert to positive
		if (zoom < 1) {
			testZoom = 1 / zoom;
		}

		// get integer zoom
		intZoom = Math.round(testZoom);

		// compute what percentage the integer zoom is of the actual zoom
		if (intZoom < testZoom) {
			percent = intZoom / testZoom;
		} else {
			percent = testZoom / intZoom;
		}

		// check if the percentage is within a threshold
		if (percent >= ViewConstants.integerZoomThreshold) {
			if (zoom < 1) {
				zoom = 1 / intZoom;
			} else {
				zoom = intZoom;
			}
		}

		return zoom;
	};

	// fit zoom to display
	/** @returns {Array} */
	Life.prototype.fitZoomDisplay = function(/** @type {number} */ fitType, /** @type {BoundingBox} */ selBox, /** @type {number} */ displayWidth, /** @type {number} */ displayHeight, /** @type {number} */ minZoom, /** @type {number} */ maxZoom, /** @type {number} */ scaleFactor, /** @type {number} */ patternWidth, /** @type {number} */ patternHeight,
			/** @type {boolean} */ usePattern, /** @type {boolean} */ historyFit, /** @type {boolean} */ state1Fit, /** @type {boolean} */ autoFit) {
		var	/** @type {BoundingBox} */ zoomBox = this.zoomBox,
			/** @type {BoundingBox} */ initialBox = this.initialBox,
			/** @type {BoundingBox} */ historyBox = this.historyBox,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Uint8Array} */ colourRow = null,
			/** @type {Array<Uint8Array>} */ overlayGrid = this.overlayGrid,
			/** @type {Uint8Array} */ overlayRow = null,
			/** @type {number} */ zoom = 1,
			/** @type {number} */ newX = 0,
			/** @type {number} */ newY = 0,
			/** @type {number} */ width = 0,
			/** @type {number} */ height = 0,
			/** @type {number} */ zoomX = 0,
			/** @type {number} */ zoomY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = 0,
			/** @type {number} */ topY = 0,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ state = 0,
			/** @type {number} */ swap = 0,
			/** @type {number} */ hexX = 0,
			/** @type {number} */ minX = 0,
			/** @type {number} */ maxX = 0,
			/** @type {number} */ count = 0,
			/** @type {number} */ targetState = 0,
			/** @type {number} */ aliveState = LifeConstants.aliveStart;

		// check for PCA, RuleTree Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
			}
		}

		// set the alive state based on the rule
		if (this.multiNumStates <= 2 && !this.isRuleTree) {
			aliveState = LifeConstants.aliveStart;
		} else {
			aliveState = this.historyStates + 1;
		}

		// check for history mode
		if (historyFit) {
			// use history box
			zoomBox = historyBox;
		} else {
			// check for HROT
			if (this.isHROT) {
				// use HROT alive state box
				zoomBox = this.HROTBox;
			}
		}

		// check for fit selection
		if (fitType === ViewConstants.fitZoomSelection) {
			leftX = selBox.leftX;
			rightX = selBox.rightX;
			topY = selBox.topY;
			bottomY = selBox.bottomY;
		} else {
			leftX = zoomBox.leftX;
			rightX = zoomBox.rightX;
			topY = zoomBox.topY;
			bottomY = zoomBox.bottomY;

			// ensure box no smaller than initial box due to multi-state cells in LifeHistory
			if (this.isLifeHistory && !state1Fit) {
				if (initialBox.leftX < leftX) {
					leftX = initialBox.leftX;
				}
				if (initialBox.rightX > rightX) {
					rightX = initialBox.rightX;
				}
				if (initialBox.topY > topY) {
					topY = initialBox.topY;
				}
				if (initialBox.bottomY < bottomY) {
					bottomY = initialBox.bottomY;
				}
			}

			// check if bounded box defined
			if (this.boundedGridType !== -1) {
				width = this.boundedGridWidth + 2;
				height = this.boundedGridHeight + 2;
				if (width > 2) {
					leftX = this.width / 2 - width / 2 + (width & 1) / 2;
					rightX = this.width / 2 + width / 2;
				} else {
					width = rightX - leftX + 1;
				}
				if (height > 2) {
					bottomY = this.height / 2 - height / 2 + (height & 1) / 2;
					topY = this.height / 2 + height / 2;
				} else {
					height = topY - bottomY + 1;
				}
			}
		}

		// check whether to use pattern dimensions
		if (!usePattern) {
			if (this.boundedGridType === -1) {
				// compute the width and height
				width = rightX - leftX + 1;
				height = topY - bottomY + 1;
			}
		} else {
			// use pattern width and height
			width = patternWidth;
			height = patternHeight;
			leftX = this.width / 2 - width / 2;
			rightX = this.width / 2 + width / 2;
			topY = this.height / 2 + height / 2;
			bottomY = this.height / 2 - height / 2;
		}

		// check for hex
		if (this.isHex) {
			if (fitType === ViewConstants.fitZoomSelection) {
				leftX = leftX - topY / 2;
				rightX = rightX - bottomY / 2;
				width = rightX - leftX + 1;
			} else {
				if (this.population === 0) {
					leftX = leftX - topY / 2;
					rightX = rightX - bottomY / 2;
					width = rightX - leftX + 1;
				} else {
					minX = (this.width + this.height) * 2;
					maxX = -minX;
					bottomY |= 0;
					topY |= 0;
					leftX |= 0;
					rightX |= 0;

					// draw the bounded grid border
					if (this.boundedGridType !== -1) {
						this.drawBoundedGridBorder(colourGrid, this.boundedBorderColour);
					}

					// find the pattern on the grid
					targetState = aliveState;
					if (historyFit) {
						targetState = 1;
					}

					// check for [R]History
					if (this.isLifeHistory) {
						for (y = bottomY; y <= topY; y += 1) {
							colourRow = colourGrid[y];
							overlayRow = overlayGrid[y];
							for (x = leftX; x <= rightX; x += 1) {
								state = colourRow[x];
								if (state >= targetState || overlayRow[x] > 0) {
									count += 1;
									hexX = x - y / 2;
									if (hexX < minX) {
										minX = hexX;
									}
									if (hexX > maxX) {
										maxX = hexX;
									}
								}
							}
						}
					} else {
						for (y = bottomY; y <= topY; y += 1) {
							colourRow = colourGrid[y];
							for (x = leftX; x <= rightX; x += 1) {
								state = colourRow[x];
								if (state >= targetState) {
									count += 1;
									hexX = x - y / 2;
									if (hexX < minX) {
										minX = hexX;
									}
									if (hexX > maxX) {
										maxX = hexX;
									}
								}
							}
						}
					}

					leftX = minX;
					rightX = maxX;
					if (leftX > rightX) {
						swap = leftX;
						leftX = rightX;
						rightX = swap;
					}
				}

				width = rightX - leftX + 1;
			}
		}

		// ensure width and height are non zero
		if (width === 0) {
			width = 1;
		}
		if (height === 0) {
			height = 1;
		}

		// compute the zoom in each direction
		zoomX = displayWidth / width;
		zoomY = displayHeight / height;

		// adjust for triangular grid if required
		if (this.isTriangular) {
			zoomY /= ViewConstants.triangularYFactor;
		}

		// adjust for hexagonal grid if required
		if (this.isHex) {
			zoomY /= ViewConstants.hexagonalYFactor;
		}

		// select the zoom from the smallest ratio
		if (zoomX > zoomY) {
			zoom = zoomY;
		} else {
			zoom = zoomX;
		}

		// apply scale factor
		zoom = Math.round(zoom * 20000 / scaleFactor) / 20000;

		// add offset
		zoom /= this.originZ;

		// ensure in range
		if (zoom < minZoom) {
			zoom = minZoom;
		} else {
			if (zoom > maxZoom) {
				zoom = maxZoom;
			}
		}

		// set the x and y offset
		newY = bottomY - this.originY + (height / 2);
		newX = leftX - this.originX + (width / 2);

		// make zoom an exact value if close to the exact value
		if (!autoFit) {
			zoom = this.makeIntegerZoom(zoom);
		}

		// check if fit to middle required
		if (fitType === ViewConstants.fitZoomMiddle) {
			zoom = this.zoom;
		}

		// adjust for Margolus in Bounded Grid
		if (this.isMargolus && this.boundedGridType !== -1) {
			newX -= 1;
			newY -= 1;
		}

		// return zoom
		return [zoom, newX, newY];
	};

	// draw horizontal line
	Life.prototype.drawHLine = function(/** @type {number} */ startX, /** @type {number} */ endX, /** @type {number} */ y, /** @type {number} */ colour) {
		var	/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ w = this.displayWidth,
			/** @type {number} */ h = this.displayHeight,

			// pixel offset in bitmap
			/** @type {number} */ offset = 0,
			/** @type {number} */ end = 0,
			/** @type {number} */ temp = 0;

		// order the x coordinates
		if (startX > endX) {
			temp = endX;
			endX = startX;
			startX = temp;
		}

		// clip to the display
		if (startX < 0) {
			startX = 0;
		} else {
			if (startX >= w) {
				startX = w - 1;
			}
		}
		if (endX < 0) {
			endX = 0;
		} else {
			if (endX >= w) {
				endX = w - 1;
			}
		}

		// pixel offsets in bitmap
		offset = y * w + startX;
		end = y * w + endX;

		// see if the line is on the display
		if (y >= 0 && y < h) {
			data32.fill(colour, offset, end + 1);
		}
	};

	// draw vertical line
	Life.prototype.drawVLine = function(/** @type {number} */ x, /** @type {number} */ startY, /** @type {number} */ endY, /** @type {number} */ colour) {
		var	/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ w = this.displayWidth,
			/** @type {number} */ h = this.displayHeight,

			// pixel offsets in bitmap
			/** @type {number} */ offset = 0,
			/** @type {number} */ end = 0,
			/** @type {number} */ endTarget = 0,
			/** @type {number} */ temp = 0;

		// order the y coordinates
		if (startY > endY) {
			temp = endY;
			endY = startY;
			startY = temp;
		}

		// clip to the display
		if (startY < 0) {
			startY = 0;
		} else {
			if (startY >= h) {
				startY = h - 1;
			}
		}
		if (endY < 0) {
			endY = 0;
		} else {
			if (endY >= h) {
				endY = h - 1;
			}
		}

		// pixel offsets in bitmap
		offset = startY * w + x;
		end = endY * w + x;
		endTarget = end - w * 15;

		// see if the line is on the display
		if (x >= 0 && x < w) {
			// draw the vertical line
			while (offset <= endTarget) {
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
				data32[offset] = colour;
				offset += w;
			}

			// draw the last part
			while (offset <= end) {
				data32[offset] = colour;
				offset += w;
			}
		}
	};

	// bresenham line draw
	Life.prototype.drawBresenhamLine = function(/** @type {number} */ startX, /** @type {number} */ startY, /** @type {number} */ endX, /** @type {number} */ endY, /** @type {number} */ colour) {
		var	/** @type {number} */ dx = Math.abs(endX - startX),
			/** @type {number} */ dy = Math.abs(endY - startY),
			/** @type {number} */ sx = (startX < endX) ? 1 : -1,
			/** @type {number} */ sy = (startY < endY) ? 1 : -1,
			/** @type {number} */ err = dx - dy,
			/** @type {number} */ e2 = 0,
			/** @type {number} */ w = this.displayWidth,
			/** @type {number} */ h = this.displayHeight,
			/** @type {Uint32Array} */ data32 = this.data32;

		// see if the line is on the display
		if (!((startX < 0 && endX < 0) || (startX >= w && endX >= w) || (startY < 0 && endY < 0) || (startY >= h && endY >= h))) {
			// see if bounds checking is required
			if (startX >= 0 && startX < w && startY >=0 && startY < h && endX >= 0 && endX < w && endY >= 0 && endY < h) {
				// line all on display so no bounds checking
				// set the first point
				data32[startY * w + startX] = colour;

				// loop for each pixel on the line
				while (!((startX === endX) && (startY === endY))) {
					// move to next pixel
					e2 = err + err;
					if (e2 > -dy) {
						err -= dy;
						startX += sx;
					}
					if (e2 < dx) {
						err += dx;
						startY += sy;
					}

					// draw the point
					data32[startY * w + startX] = colour;
				}
			} else {
				// some or all of the line is off display so use bounds checking
				// set the first point
				if (startX >= 0 && startX < w && startY >=0 && startY < h) {
					data32[startY * w + startX] = colour;
				}

				// loop for each pixel on the line
				while (!((startX === endX) && (startY === endY))) {
					// move to next pixel
					e2 = err + err;
					if (e2 > -dy) {
						err -= dy;
						startX += sx;
					}
					if (e2 < dx) {
						err += dx;
						startY += sy;
					}

					// draw the point
					if (startX >= 0 && startX < w && startY >=0 && startY < h) {
						data32[startY * w + startX] = colour;
					}
				}
			}
		}
	};

	// draw a line
	Life.prototype.drawLine = function(/** @type {number} */ startX, /** @type {number} */ startY, /** @type {number} */ endX, /** @type {number} */ endY, /** @type {number} */ colour) {
		var	/** @type {number} */ radius = 0,
			/** @type {number} */ theta = 0,
			/** @type {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @type {number} */ halfDisplayHeight = this.displayHeight / 2;

		// check for rotation
		if (this.camAngle !== 0) {
			// rotate start point around center
			startX -= halfDisplayWidth;
			startY -= halfDisplayHeight;
			radius = Math.sqrt((startX * startX) + (startY * startY));
			theta = Math.atan2(startY, startX) * (180 / Math.PI);
			theta += this.camAngle;
			// grow radius
			startX = Math.round(radius * Math.cos(theta * (Math.PI / 180)) + halfDisplayWidth);
			startY = Math.round(radius * Math.sin(theta * (Math.PI / 180)) + halfDisplayHeight);

			// rotate end point
			endX -= halfDisplayWidth;
			endY -= halfDisplayHeight;
			radius = Math.sqrt((endX * endX) + (endY * endY));
			theta = Math.atan2(endY, endX) * (180 / Math.PI);
			theta += this.camAngle;
			endX = Math.round(radius * Math.cos(theta * (Math.PI / 180)) + halfDisplayWidth);
			endY = Math.round(radius * Math.sin(theta * (Math.PI / 180)) + halfDisplayHeight);
		}

		// check for vertical line
		if (startX === endX) {
			this.drawVLine(startX, startY, endY, colour);
		} else {
			if (startY === endY) {
				this.drawHLine(startX, endX, startY, colour);
			} else {
				this.drawBresenhamLine(startX, startY, endX, endY, colour);
			}
		}
	};

	// draw paste box
	Life.prototype.drawPasteWithCells = function(/** @type {View} */ view, /** @type {number} */ mouseCellX, /** @type {number} */ mouseCellY, /** @type {number} */ position, /** @type {string} */ colour) {
		var	/** @type {number} */ width = view.pasteWidth,
			/** @type {number} */ height = view.pasteHeight,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ i = 0,
			/** @type {number} */ x1 = 0,
			/** @type {number} */ y1 = 0,
			/** @type {number} */ x1d1 = 0,
			/** @type {number} */ y1d1 = 0,
			/** @type {number} */ x1d1d2 = 0,
			/** @type {number} */ y1d1d2 = 0,
			/** @type {number} */ x1d2 = 0,
			/** @type {number} */ y1d2 = 0,
			/** @type {number} */ x2 = 0,
			/** @type {number} */ y2 = 0,
			/** @type {number} */ xy1 = 0,
			/** @type {number} */ xy2 = 0,
			/** @type {number} */ state = 0,
			/** @type {CanvasRenderingContext2D} */ ctx = this.context,
			/** @type {number} */ xZoom = this.zoom,
			/** @type {number} */ yZoom = this.getYZoom(this.zoom),
			/** @type {number} */ xOff = view.panX - view.xOffset,
			/** @type {number} */ yOff = view.panY - view.yOffset,
			/** @type {number} */ engineY = view.panY - this.yOff,
			/** @type {number} */ engineX = view.panX - this.xOff - (this.isHex ? this.yOff / 2 : 0),
			/** @type {number} */ angleOff = (this.angle !== 0 ? 1 : 0),
			/** @type {Array<number>} */ coords = [0, 0],
			/** @type {number} */ dx1 = 0,
			/** @type {number} */ dx2 = 0,
			/** @type {number} */ dy1 = 0,
			/** @type {number} */ dy2 = 0;

		// draw paste rectangle
		switch (position) {
		case ViewConstants.pastePositionNW:
			// nothing to do
			break;
		case ViewConstants.pastePositionN:
			mouseCellX -= width >> 1;
			break;
		case ViewConstants.pastePositionNE:
			mouseCellX -= width - 1;
			break;
		case ViewConstants.pastePositionW:
			mouseCellY -= height >> 1;
			break;
		case ViewConstants.pastePositionMiddle:
			mouseCellX -= width >> 1;
			mouseCellY -= height >> 1;
			break;
		case ViewConstants.pastePositionE:
			mouseCellX -= width - 1;
			mouseCellY -= height >> 1;
			break;
		case ViewConstants.pastePositionSW:
			mouseCellY -= height - 1;
			break;
		case ViewConstants.pastePositionS:
			mouseCellX -= width >> 1;
			mouseCellY -= height - 1;
			break;
		case ViewConstants.pastePositionSE:
			mouseCellX -= width - 1;
			mouseCellY -= height - 1;
			break;
		}
		x1 = mouseCellX;
		y1 = mouseCellY;
		x2 = mouseCellX + width;
		y2 = mouseCellY + height;

		// adjust if CXRLE Pos defined
		if (view.posDefined) {
			xOff += view.posXOffset * 2;
			yOff += view.posYOffset * 2;
		}

		// convert cell coordinates to screen coordinates
		xy1 = xZoom * (y1 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		y1 = yZoom * (y1 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		x1 = xZoom * (x1 - xOff + engineX - this.originX + view.panX) + view.displayWidth / 2 + (this.isHex ? (view.displayHeight / 2 - xy1) / 2 : 0);
		xy2 = xZoom * (y2 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		y2 = yZoom * (y2 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		x2 = xZoom * (x2 - xOff + engineX - this.originX + view.panX) + view.displayWidth / 2 + (this.isHex ? (view.displayHeight / 2 - xy2) / 2 : 0);

		// draw a translucent box
		ctx.fillStyle = colour;
		ctx.globalAlpha = 0.5;
		if (!this.isHex) {
			if (!this.forceRectangles && this.isTriangular && this.zoom >= 4) {
				if (view.posDefined) {
					xOff -= view.posXOffset * 2;
					yOff -= view.posYOffset * 2;
				}
				this.drawTriangleSelection(mouseCellX, mouseCellY, mouseCellX + width - 1, mouseCellY + height - 1, xOff, yOff);
			} else {
				if (this.angle !== 0) {
					ctx.beginPath();
					this.rotateCoords(x1, y1, coords);
					ctx.moveTo(coords[0], coords[1]);
					this.rotateCoords(x2 + angleOff, y1, coords);
					ctx.lineTo(coords[0], coords[1]);
					this.rotateCoords(x2 + angleOff, y2 + angleOff, coords);
					ctx.lineTo(coords[0], coords[1]);
					this.rotateCoords(x1, y2 + angleOff, coords);
					ctx.lineTo(coords[0], coords[1]);
					ctx.fill();
				} else {
					ctx.beginPath();
					ctx.moveTo(this.ceilAbove(x1), this.ceilAbove(y1));
					ctx.lineTo(this.ceilAbove(x2), this.ceilAbove(y1));
					ctx.lineTo(this.ceilAbove(x2), this.ceilAbove(y2));
					ctx.lineTo(this.ceilAbove(x1), this.ceilAbove(y2));
					ctx.fill();
				}
			}
		} else {
			if (!this.forceRectangles && yZoom >= 4) {
				if (view.posDefined) {
					xOff -= view.posXOffset * 2;
					yOff -= view.posYOffset * 2;
				}
				this.drawHexSelection(mouseCellX, mouseCellY, mouseCellX + width - 1, mouseCellY + height - 1, xOff, yOff);
			} else {
				ctx.beginPath();
				for (i = 0; i < height; i += 1) {
					ctx.moveTo(x1, y1);
					ctx.lineTo(x1 + width * xZoom + 1, y1);
					ctx.lineTo(x1 + width * xZoom + 1, y1 + yZoom + 1);
					ctx.lineTo(x1, y1 + yZoom + 1);
					x1 -= xZoom / 2;
					y1 += yZoom;
				}
				ctx.fill();
			}
		}

		// now draw each set cell if zoom is high enough
		if (this.zoom >= 1) {
			// compute deltas in horizontal and vertical direction based on rotation
			if (this.camAngle > 0) {
				dx1 = Math.cos(this.camAngle / 180 * Math.PI) * this.camZoom;
				dy1 = Math.sin(this.camAngle / 180 * Math.PI) * this.camZoom;
				dx2 = Math.cos((this.camAngle + 90) / 180 * Math.PI) * this.camZoom;
				dy2 = Math.sin((this.camAngle + 90) / 180 * Math.PI) * this.camZoom;
			} else {
				dx1 = xZoom;
				dy1 = 0;
				dx2 = dy1;
				dy2 = yZoom;
			}

			// compute starting coordinates
			xy1 = xZoom * (mouseCellY + y - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
			y1 = yZoom * (mouseCellY + y - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
			x1 = xZoom * (mouseCellX + x - xOff + engineX - this.originX + view.panX) + view.displayWidth / 2 + (this.isHex ? (view.displayHeight / 2 - xy1) / 2 : 0);
			if (this.camAngle !== 0) {
				this.rotateCoords(x1, y1, coords);
				x1 = coords[0];
				y1 = coords[1];
			}
			x2 = x1;
			y2 = y1;

			// draw cells
			ctx.fillStyle = this.selectedCellsColour;
			if (this.isHex && !this.forceRectangles && yZoom >= 4) {
				this.drawHexCellsInSelection(mouseCellX, mouseCellY, mouseCellX + width - 1, mouseCellY + height - 1, xOff, yOff, view.pasteBuffer);
			} else {
				if (!this.forceRectangles && this.isTriangular && this.zoom >= 4) {
					this.drawTriangleCellsInSelection(mouseCellX, mouseCellY, mouseCellX + width - 1, mouseCellY + height - 1, xOff, yOff, view.pasteBuffer);
				} else {
					ctx.beginPath();
					i = 0;
					for (y = 0; y < height; y += 1) {
						x1 = x2;
						for (x = 0; x < width; x += 1) {
							state = view.pasteBuffer[i];
							i += 1;
							if (state) {
								// compute cell coordinates
								x1d1 = x1 + dx1;
								y1d1 = y1 + dy1;
								x1d1d2 = x1d1 + dx2;
								y1d1d2 = y1d1 + dy2;
								x1d2 = x1d1d2 - dx1;
								y1d2 = y1d1d2 - dy1;

								// don't draw cell if off window
								if (!((x1 < 0 && x1d1 < 0 && x1d1d2 < 0 && x1d2 < 0) ||
									(x1 >= view.displayWidth && x1d1 >= view.displayWidth && x1d1d2 >= view.displayWidth && x1d2 >= view.displayWidth) ||
									(y1 < 0 && y1d1 < 0 && y1d1d2 < 0 && y1d2 < 0) ||
									(y1 >= view.displayHeight && y1d1 >= view.displayHeight && y1d1d2 >= view.displayHeight && y1d2 >= view.displayHeight))) {
									// draw cell
									ctx.moveTo(x1, y1);
									ctx.lineTo(x1d1, y1d1);
									ctx.lineTo(x1d1d2, y1d1d2);
									ctx.lineTo(x1d2, y1d2);
								}
							}
							// next column
							x1 += dx1;
							y1 += dy1;
						}
						// next row
						x2 += dx2;
						y2 += dy2;
						y1 = y2;
						if (this.isHex) {
							x2 -= dx1 / 2;
						}
					}
					ctx.fill();
				}
			}
		}
	};

	// get rotated coordinates
	Life.prototype.rotateCoords = function(/** @type {number} */ x, /** @type {number} */ y, /** @type {Array<number>} */ result) {
		var	/** @type {number} */ radius = 0,
			/** @type {number} */ theta = 0,
			/** @type {number} */ halfDisplayWidth = this.displayWidth / 2,
			/** @type {number} */ halfDisplayHeight = this.displayHeight / 2;

		// check for rotation
		if (this.camAngle !== 0) {
			x -= halfDisplayWidth;
			y -= halfDisplayHeight;
			radius = Math.sqrt((x * x) + (y * y));
			theta = Math.atan2(y, x) * (180 / Math.PI);
			theta += this.camAngle;
			// grow radius
			x = Math.round(radius * Math.cos(theta * (Math.PI / 180)) + halfDisplayWidth);
			y = Math.round(radius * Math.sin(theta * (Math.PI / 180)) + halfDisplayHeight);
		}

		// save result
		result[0] = x;
		result[1] = y;
	};

	// draw selection
	Life.prototype.drawSelections = function(/** @type {View} */ view) {
		var	/** @type {number} */ position = (view.pastePosition + 0.5) | 0,
			/** @type {number} */ mouseX = view.menuManager.mouseLastX,
			/** @type {number} */ mouseY = view.menuManager.mouseLastY,
			/** @type {number} */ xOff = view.panX - view.xOffset,
			/** @type {number} */ yOff = view.panY - view.yOffset;

		if (view.isSelection || view.drawingSelection) {
			this.drawBox(view, view.selectionBox, this.selectColour);
		}

		if (view.isPasting) {
			mouseX = view.menuManager.mouseLastX;
			mouseY = view.menuManager.mouseLastY;
			if (mouseX !== -1) {
				view.updateCellLocation(mouseX, mouseY);
				this.drawPasteWithCells(view, view.cellX - xOff, view.cellY - yOff, position, this.pasteColour);
			}
		}
	};

	// ceiling if number not an integer
	//* @returns {number} */
	Life.prototype.ceilAbove = function(/** @type {number} */ v) {
		var	/** @type {number} */ result = v,
			/** @type {number} */ intV = v | 0;

		if (v !== intV) {
			result = intV + 1;
		}

		// add 0.5 for canvas line draw
		return result + 0.5;
	};

	// draw box
	Life.prototype.drawBox = function(/** @type {View} */ view, /** @type {BoundingBox} */ box, /** @type {string} */ colour) {
		var	/** @type {CanvasRenderingContext2D} */ ctx = this.context,
			/** @type {number} */ xZoom = this.zoom,
			/** @type {number} */ yZoom = this.getYZoom(this.zoom),
			/** @type {number} */ x1 = box.leftX,
			/** @type {number} */ y1 = box.bottomY,
			/** @type {number} */ x2 = box.rightX,
			/** @type {number} */ y2 = box.topY,
			/** @type {number} */ xy1 = 0,
			/** @type {number} */ xy2 = 0,
			/** @type {number} */ width = 0,
			/** @type {number} */ height = 0,
			/** @type {number} */ yOff = view.panY - view.yOffset,
			/** @type {number} */ xOff = view.panX - view.xOffset,
			/** @type {number} */ swap = 0,
			/** @type {number} */ engineY = view.panY - this.yOff,
			/** @type {number} */ engineX = view.panX - this.xOff - (this.isHex ? this.yOff / 2 : 0),
			/** @type {Array<number>} */ coords = [0, 0],
			/** @type {number} */ i = 0,
			/** @type {number} */ angleOff = (this.angle !== 0 ? 1 : 0),
			/** @type {BoundingBox} */ selBox = view.selectionBox;

		// order selection box coordinates
		if (x1 > x2) {
			swap = x1;
			x1 = x2;
			x2 = swap;
		}
		if (y1 > y2) {
			swap = y1;
			y1 = y2;
			y2 = swap;
		}
		width = x2 - x1 + 1;
		height = y2 - y1 + 1;

		// adjust if CXRLE Pos defined
		if (view.posDefined) {
			if (view.engine.boundedGridType !== -1) {
				xOff += Math.floor(view.specifiedWidth / 2);
				yOff += Math.floor(view.specifiedHeight / 2);
			}
		}

		// convert cell coordinates to screen coordinates
		xy1 = xZoom * (y1 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		y1 = yZoom * (y1 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		x1 = xZoom * (x1 - xOff + engineX - this.originX + view.panX) + view.displayWidth / 2 + (this.isHex ? (view.displayHeight / 2 - xy1) / 2 : 0);
		xy2 = xZoom * (y2 + 1 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		y2 = yZoom * (y2 + 1 - yOff + engineY - this.originY + view.panY) + view.displayHeight / 2;
		x2 = xZoom * (x2 + 1 - xOff + engineX - this.originX + view.panX) + view.displayWidth / 2 + (this.isHex ? (view.displayHeight / 2 - xy2) / 2 : 0);

		// draw a translucent box
		ctx.fillStyle = colour;
		ctx.globalAlpha = 0.5;
		if (!this.isHex) {
			if (this.isTriangular && !this.forceRectangles && this.zoom >= 4) {
				if (view.posDefined) {
					xOff += view.posXOffset * 2;
					yOff += view.posYOffset * 2;
				}
				this.drawTriangleSelection(selBox.leftX, selBox.bottomY, selBox.rightX, selBox.topY, xOff, yOff);
			} else {
				if (this.angle !== 0) {
					ctx.beginPath();
					this.rotateCoords(x1, y1, coords);
					ctx.moveTo(coords[0], coords[1]);
					this.rotateCoords(x2 + angleOff, y1, coords);
					ctx.lineTo(coords[0], coords[1]);
					this.rotateCoords(x2 + angleOff, y2 + angleOff, coords);
					ctx.lineTo(coords[0], coords[1]);
					this.rotateCoords(x1, y2 + angleOff, coords);
					ctx.lineTo(coords[0], coords[1]);
					ctx.fill();
				} else {
					ctx.fillRect(this.ceilAbove(x1), this.ceilAbove(y1), x2 - x1 + 1, y2 - y1 + 1);
				}
			}
		} else {
			// check for hexagons (rather than offset squares)
			if (!this.forceRectangles && yZoom >= 4) {
				if (view.posDefined) {
					xOff += view.posXOffset * 2;
					yOff += view.posYOffset * 2;
				}
				this.drawHexSelection(selBox.leftX, selBox.bottomY, selBox.rightX, selBox.topY, xOff, yOff);
			} else {
				ctx.beginPath();
				width *= xZoom;
				for (i = 0; i < height; i += 1) {
					ctx.moveTo(x1, y1);
					ctx.lineTo(x1 + width + 1, y1);
					ctx.lineTo(x1 + width + 1, y1 + yZoom + 1);
					ctx.lineTo(x1, y1 + yZoom + 1);
					x1 -= xZoom / 2;
					y1 += yZoom;
				}
				ctx.fill();
			}
		}
		ctx.globalAlpha = 1;
	};

	/** @returns {string} */
	Life.prototype.getColourString = function(/** @type {number} */ colour) {
		var	/** @type {string} */ result = "";

		if (this.littleEndian) {
			result = "rgb(" + (colour & 255) + "," + ((colour >> 8) & 255) + "," + ((colour >> 16) & 255) + ")";
		} else {
			result = "rgb(" + (colour >> 24) + "," + ((colour >> 16) & 255) + "," + ((colour >> 8) & 255) + ")";
		}

		return result;
	};

	// draw grid lines
	Life.prototype.drawGridLines = function() {
		var	/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ loop = 1,
			/** @type {number} */ w = this.displayWidth,
			/** @type {number} */ h = this.displayHeight,
			/** @type {number} */ xCol = 0,
			/** @type {number} */ gridCol = this.gridLineColour,
			/** @type {number} */ gridBoldCol = this.gridLineBoldColour,
			/** @type {number} */ xZoomStep = this.camZoom,
			/** @type {number} */ yZoomStep = this.getYZoom(this.camZoom),
			/** @type {number} */ gridLineNum = 0,
			/** @type {number} */ vLineNum = 0,
			/** @type {number} */ drawCol = gridCol,
			/** @type {number} */ targetCol = gridCol,
			/** @type {number} */ startX = 0,
			/** @type {number} */ startY = 0,
			/** @type {number} */ endX = 0,
			/** @type {number} */ endY = 0,
			/** @type {number} */ leftX = 0,
			/** @type {number} */ rightX = w,
			/** @type {number} */ bottomY = 0,
			/** @type {number} */ topY = h,
			/** @type {boolean} */ drawMajor = (this.gridLineMajor > 0 && this.gridLineMajorEnabled),
			/** @type {number} */ odd = (this.counter & 1),
			/** @type {number} */ extend = Math.abs(w - h),

			// compute single cell offset
			/** @type {number} */ yOff = (((this.height / 2 - (this.yOff + this.originY)) * yZoomStep) + (h / 2)) % yZoomStep,
			/** @type {number} */ xOff = (((this.width / 2 - (this.xOff + this.originX)) * xZoomStep) + (w / 2)) % xZoomStep,

			// compute top left of original pattern offset
			/** @type {number} */ xPat = (this.view.patternWidth / 2) | 0,
			/** @type {number} */ yPat = (this.view.patternHeight / 2) | 0;

		// draw twice if major grid lines enabled
		if (this.displayGrid) {
			if (drawMajor) {
				loop = 2;
			} else {
				loop = 1;
			}
		} else {
			// draw cell borders
			loop = 1;
			drawMajor = false;
			gridCol = this.pixelColours[0];
			drawCol = gridCol;
		}

		// start drawing the grid line colour
		targetCol = gridCol;

		// for Margolus alternate major/minor grid lines for odd/even generations
		if (!this.isMargolus || this.gridLineMajor !== 2 || !this.altGrid) {
			odd = 0;
		}

		while (loop) {
			// compute major grid line vertical offset
			gridLineNum = -(w / 2 / xZoomStep) - (this.width / 2 - this.xOff - this.originX) | 0;
			gridLineNum += xPat - 2;

			// extend the number of lines to cope with rotation
			startX = -xZoomStep * extend;
			endX = w + xZoomStep + xZoomStep * extend;
			startY = yOff - yZoomStep * extend;
			endY = h + yZoomStep * extend;
			leftX = -w / 1.5;
			rightX = w + w / 1.5;
			bottomY = -h / 1.5;
			topY = h + h / 1.5;

			// draw vertical lines
			for (x = startX; x <= endX; x += xZoomStep) {
				// check if major gridlines are enabled
				if (drawMajor) {
					// choose whether to use major or minor colour
					if ((gridLineNum + odd) % this.gridLineMajor === 0) {
						drawCol = gridBoldCol;
					} else {
						drawCol = gridCol;
					}
				}
				gridLineNum += 1;

				// check whether to draw the line
				if (drawCol === targetCol) {
					// check for hex mode
					if (this.isHex) {
						// compute major grid line horizontal offset
						vLineNum = -(h / 2 / yZoomStep) - (this.height / 2 - this.yOff - this.originY) | 0;

						// draw staggered vertical line
						for (y = startY; y < endY; y += yZoomStep) {
							if (y >= 0 && y < h) {
								if ((vLineNum & 1) === (extend & 1)) {
									xCol = Math.round(x + xOff);
								} else {
									xCol = Math.round(x + xOff + xZoomStep / 2);
								}

								// can use VLine since hexagonal grids don't support camera rotation
								if (xCol >= 0 && xCol < w) {
									this.drawVLine(xCol, Math.round(y), Math.round(y + yZoomStep - 1), drawCol);
								}
							}
							vLineNum += 1;
						}
					} else {
						// draw vertical line
						this.drawLine(Math.round(x + xOff), bottomY, Math.round(x + xOff), topY - 1, drawCol);
					}
				}
			}

			// compute major grid line horizontal offset
			gridLineNum = -(h / 2 / yZoomStep) - (this.height / 2 - this.yOff - this.originY) | 0;
			gridLineNum += yPat - 2;

			// draw horizontal lines
			for (y = startY; y < endY; y += yZoomStep) {
				// check if major gridlines are enabled
				if (drawMajor) {
					// choose whether to use major or minor colour
					if ((gridLineNum + odd) % this.gridLineMajor === 0) {
						drawCol = gridBoldCol;
					} else {
						drawCol = gridCol;
					}
				}
				gridLineNum += 1;

				// draw the line
				if (drawCol === targetCol) {
					this.drawLine(leftX, Math.round(y), rightX - 1, Math.round(y), drawCol);
				}
			}

			// next iteration
			loop -= 1;

			// switch to major grid line colour
			targetCol = gridBoldCol;
		}
	};

	// check whether the grid can be displayed
	/** @returns {boolean} */
	Life.prototype.canDisplayGrid = function() {
		// grid can be displayed if zoom >= 4
		return (this.getYZoom(this.camZoom) >= 4);
	};

	// draw the bounded grid border using mask for sub-sampled grids
	Life.prototype.drawBoundedGridBorderMask = function(/** @type {Array<Uint8Array>} */ colourGrid, /** @type {number} */ border, /** @type {number} */ mask) {
		// get width and height
		var	/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// coordinates of box
			/** @type {number} */ leftX = (Math.round((this.width - width) / 2 - 1) + boxOffset),
			/** @type {number} */ rightX = leftX + width + 1,
			/** @type {number} */ bottomY = (Math.round((this.height - height) / 2 - 1) + boxOffset),
			/** @type {number} */ topY = bottomY + height + 1,

			// alive cell value
			/** @type {number} */ aliveStart = this.multiNumStates <= 2 ? this.aliveStart : this.historyStates + 1,

			// counter
			/** @type {number} */ i = 0,

			/** @type {Uint8Array} */ row = null,
			/** @type {Uint8Array} */ col = null;

		// apply mask
		bottomY &= ~mask;
		topY &= ~mask;
		leftX &= ~mask;
		rightX &= ~mask;

		// check for infinite width
		if (width === 0) {
			// draw top and bottom only
			row = colourGrid[topY];
			for (i = 0; i < this.width; i += 1) {
				if (row[i] < aliveStart) {
					row[i] = border;
				}
			}

			row = colourGrid[bottomY];
			for (i = 0; i < this.width; i += 1) {
				if (row[i] < aliveStart) {
					row[i] = border;
				}
			}
		} else {
			// check for infinite height
			if (height === 0) {
				// draw left and right only
				for (i = 0; i < this.height; i += 1) {
					col = colourGrid[i];
					if (col[leftX] < aliveStart) {
						col[leftX] = border;
					}

					if (col[rightX] < aliveStart) {
						col[rightX] = border;
					}
				}
			} else {
				// draw top and bottom
				row = colourGrid[topY];
				for (i = leftX; i <= rightX; i += 1) {
					if (row[i] < aliveStart) {
						row[i] = border;
					}
				}

				row = colourGrid[bottomY];
				for (i = leftX; i <= rightX ; i += 1) {
					if (row[i] < aliveStart) {
						row[i] = border;
					}
				}

				// draw left and right
				for (i = bottomY + 1; i <= topY - 1; i += 1) {
					col = colourGrid[i];
					if (col[leftX] < aliveStart) {
						col[leftX] = border;
					}

					if (col[rightX] < aliveStart) {
						col[rightX] = border;
					}
				}
			}
		}
	};

	// draw the bounded grid border
	Life.prototype.drawBoundedGridBorder = function(/** @type {Array<Uint8Array>} */ colourGrid, /** @type {number} */ border) {
		// get width and height
		var	/** @type {number} */ width = this.boundedGridWidth,
			/** @type {number} */ height = this.boundedGridHeight,

			// box offset
			/** @type {number} */ boxOffset = (this.isMargolus ? -1 : 0),

			// coordinates of box
			/** @type {number} */ leftX = Math.round((this.width - width) / 2 - 1) + boxOffset,
			/** @type {number} */ rightX = leftX + width + 1,
			/** @type {number} */ bottomY = Math.round((this.height - height) / 2 - 1) + boxOffset,
			/** @type {number} */ topY = bottomY + height + 1,

			// top and bottom row
			/** @type {Uint8Array} */ bottomRow = colourGrid[bottomY],
			/** @type {Uint8Array} */ topRow = colourGrid[topY],

			// counter
			/** @type {number} */ i = 0;

		// check for infinite width
		if (width === 0) {
			// draw top and bottom only
			bottomRow.fill(border, 0, this.width);
			topRow.fill(border, 0, this.width);
		} else {
			// check for infinite height
			if (height === 0) {
				// draw left and right only
				for (i = 0; i < this.height; i += 1) {
					colourGrid[i][leftX] = border;
					colourGrid[i][rightX] = border;
					if (this.isTriangular) {
						if (leftX > 0) {
							colourGrid[i][leftX - 1] = border;
						}
						if (rightX < this.width - 1) {
							colourGrid[i][rightX + 1] = border;
						}
					}
				}
			} else {
				// draw top and bottom
				if (this.isHex) {
					bottomRow.fill(border, leftX, rightX);
					topRow.fill(border, leftX + 1, rightX + 1);
				} else {
					bottomRow.fill(border, leftX, rightX + 1);
					topRow.fill(border, leftX, rightX + 1);
				}

				if (this.isTriangular && leftX > 0) {
					if ((width & 3) !== (height & 3)) {
						bottomRow[rightX + 1] = border;
						topRow[rightX + 1] = border;
					} else{
						bottomRow[leftX - 1] = border;
						topRow[leftX - 1] = border;
					}
				}

				// draw left and right
				for (i = bottomY + 1; i <= topY - 1; i += 1) {
					colourGrid[i][leftX] = border;
					colourGrid[i][rightX] = border;
					if (this.isTriangular) {
						if (leftX > 0) {
							colourGrid[i][leftX - 1] = border;
						}
						if (rightX < this.width - 1) {
							colourGrid[i][rightX + 1] = border;
						}
					}
				}
			}
		}
	};

	// compute the transparent target for layers
	/** @returns {number} */
	Life.prototype.getTransparentTarget = function(/** @type {number} */ layerTarget, /** @type {number} */ i) {
		var	/** @type {number} */ result = 1;

		// compute the transparent target
		if (this.multiNumStates > 2) {
			if (this.isSuper || this.isExtended || this.isRuleTree) {
				result = 1;
			} else {
				// use number of generations states as maximum
				if (this.historyStates === 0) {
					result = (i * (this.multiNumStates / this.layers)) | 0;
				} else {
					if (i < layerTarget / 2) {
						result = (i * 2 * (this.historyStates / this.layers)) | 0;
					} else {
						result = this.historyStates + (i * (this.multiNumStates / this.layers)) | 0;
					}
				}
			}
		} else {
			if (this.cellRenderer === LifeConstants.shaderRainbow || this.isRuleTree) {  // TBD rainbow
				result = 1;
			} else {
				result = (i * ((this.aliveMax + 1) / this.layers)) | 0;
			}
		}

		if (result < 1) {
			result = 1;
		}
		return result;
	};

	// get render mask from zoom
	/** @returns {number} */
	Life.prototype.getMaskFromZoom = function(/** @type {number} */ zoom) {
		var	/** @type {number} */ result = 0;

		if (zoom < 0.0625) {
			result = 31;
		} else {
			if (zoom < 0.125) {
				result = 15;
			} else {
				if (zoom < 0.25) {
					result = 7;
				} else {
					if (zoom < 0.5) {
						result = 3;
					} else {
						if (zoom < 1) {
							result = 1;
						}
					}
				}
			}
		}

		return result;
	};

	// update colours
	Life.prototype.updateColours = function() {
		// check if colour is changing
		if (this.colourChange) {
			this.createColours();

			this.colourChange -= 1;
			if (!this.colourChange) {
				// make target current
				this.aliveColCurrent.set(this.aliveColTarget);
				this.deadColCurrent.set(this.deadColTarget);
				this.unoccupiedCurrent.set(this.unoccupiedTarget);
				this.aliveGenColCurrent.set(this.aliveGenColTarget);
				this.dyingGenColCurrent.set(this.dyingGenColTarget);
				this.unoccupiedGenCurrent.set(this.unoccupiedGenTarget);
			}
		}
	};

	// project the life grid onto the canvas
	Life.prototype.renderGrid = function(/** @type {boolean} */ drawingSnow, /** @type {boolean} */ drawingStars) {
		var	/** @type {number} */ colour0 = this.pixelColours[0],
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {Array<Uint8Array>} */ colourGrid = this.colourGrid,
			/** @type {Array<Uint16Array>} */ colourGrid16 = this.colourGrid16,
			/** @type {Array<Uint32Array>} */ colourGrid32 = this.colourGrid32,
			/** @type {number} */ camZoom = 1;

		// mark that grid should be drawn
		this.doDrawGrid = true;

		// check for PCA, RuleTree, Super or Extended rules
		if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
			// swap grids every generation
			if ((this.counter & 1) !== 0) {
				colourGrid = this.nextColourGrid;
				colourGrid16 = this.nextColourGrid16;
				colourGrid32 = this.nextColourGrid32;
			}
		}

		// update colours
		this.updateColours();

		// update background colour in case it changed
		colour0 = this.pixelColours[0];

		// read the camera position
		this.camZoom = this.zoom * this.originZ;

		if (this.camZoom < ViewConstants.minZoom) {
			this.camZoom = ViewConstants.minZoom;
		} else {
			if (this.camZoom > this.view.maxZoom) {
				this.camZoom = this.view.maxZoom;
			}
		}
		this.camXOff = this.xOff + this.originX;
		this.camYOff = this.yOff + this.originY;
		this.camLayerDepth = (this.layerDepth / 2) + 1;

		// get a copy of the camera zoom
		camZoom = this.getYZoom(this.camZoom);

		// check for hex
		if (this.isHex || this.isTriangular || this.view.useIcons) {
			// zero angle
			this.camAngle = 0;
		} else {
			this.camAngle = this.angle;
		}

		// create bounded grid border if specified
		if (this.boundedGridType !== -1 && this.camZoom >= 1) {
			this.drawBoundedGridBorder(colourGrid, this.boundedBorderColour);
		}

		// check if drawing grid with polygons
		if (camZoom >= 4 && ((!this.forceRectangles && this.isHex) || (!this.forceRectangles && this.isTriangular))) {
			// clear grid
			data32.fill(colour0);

			// create pixel colours
			this.createPixelColours(1);
		} else {
			// create small colour grids if zoomed out
			if (camZoom < 1) {
				if  (this.boundedGridType !== -1) {
					this.drawBoundedGridBorder(colourGrid, 0);
				}
				this.createSmallColourGrids(colourGrid16, colourGrid32, camZoom);
				if (this.boundedGridType !== -1) {
					this.drawBoundedGridBorderMask(this.smallColourGrid, this.boundedBorderColour, this.getMaskFromZoom(camZoom));
				}
			}

			// check if zoom < 0.0625x
			if (camZoom < 0.0625) {
				// check for LifeHistory overlay
				if (this.drawOverlay) {
					// render the grid with the overlay on top
					this.renderGridOverlayProjection(this.smallOverlayGrid, this.smallColourGrid, 31, drawingSnow, drawingStars);
				} else {
					// render using small colour grid 32x32
					this.renderGridProjection(this.smallColourGrid, this.smallColourGrid, 31, drawingSnow, drawingStars);
				}
			} else {
				// check if zoom < 0.125x
				if (camZoom < 0.125) {
					// check for LifeHistory overlay
					if (this.drawOverlay) {
						// render the grid with the overlay on top
						this.renderGridOverlayProjection(this.smallOverlayGrid, this.smallColourGrid, 15, drawingSnow, drawingStars);
					} else {
						// render using small colour grid 16x16
						this.renderGridProjection(this.smallColourGrid, this.smallColourGrid, 15, drawingSnow, drawingStars);
					}
				} else {
					// check if zoom < 0.25x
					if (camZoom < 0.25) {
						// check for LifeHistory overlay
						if (this.drawOverlay) {
							// render the grid with the overlay on top
							this.renderGridOverlayProjection(this.smallOverlayGrid, this.smallColourGrid, 7, drawingSnow, drawingStars);
						} else {
							// render using small colour grid 8x8
							this.renderGridProjection(this.smallColourGrid, this.smallColourGrid, 7, drawingSnow, drawingStars);
						}
					} else {
						// check if zoom < 0.5x
						if (camZoom < 0.5) {
							// check for LifeHistory overlay
							if (this.drawOverlay) {
								// render the grid with the overlay on top
								this.renderGridOverlayProjection(this.smallOverlayGrid, this.smallColourGrid, 3, drawingSnow, drawingStars);
							} else {
								// render using small colour grid 4x4
								this.renderGridProjection(this.smallColourGrid, this.smallColourGrid, 3, drawingSnow, drawingStars);
							}
						} else {
							// check for zoom < 1x
							if (camZoom < 1) {
								// check for LifeHistory overlay
								if (this.drawOverlay) {
									// render the grid with the overlay on top
									this.renderGridOverlayProjection(this.smallOverlayGrid, this.smallColourGrid, 1, drawingSnow, drawingStars);
								} else {
									// render using small colour grid 2x2
									this.renderGridProjection(this.smallColourGrid, this.smallColourGrid, 1, drawingSnow, drawingStars);
								}
							} else {
								// check for LifeHistory overlay
								if (this.drawOverlay) {
									// render the grid with the overlay on top
									this.renderGridOverlayProjection(this.overlayGrid, colourGrid, 0, drawingSnow, drawingStars);
								} else {
									// render the grid
									this.renderGridProjection(colourGrid, colourGrid, 0, drawingSnow, drawingStars);
								}
							}
						}
					}
				}
			}
		}
	};

	// draw grid lines and snow
	Life.prototype.drawGridLinesAndSnow = function(/** @type {boolean} */ drawingSnow) {
		// check if snow is required
		if (drawingSnow) {
			// if cell borders are being drawn then defer snow until this is done
			if (!(this.canDisplayGrid() && this.cellBorders && !this.displayGrid)) {
				this.drawSnow();
			}
		}

		// draw grid lines or cell borders if enabled and zoomed in enough
		if ((this.displayGrid || this.cellBorders) && this.canDisplayGrid()) {
			this.drawGridLines();
		}

		// check if snow was deferred
		if (drawingSnow) {
			// if cell borders are being drawn then defer snow until this is done
			if (this.canDisplayGrid() && this.cellBorders && !this.displayGrid) {
				this.drawSnow();
			}
		}
	};

	// prepare next layer for rendering
	/** @returns {Array} */
	Life.prototype.prepareNextLayer = function(/** @type {number} */ zoom, /** @type {boolean} */ firstLayer, /** @type {number} */ lastMask, /** @type {Array<Uint8Array>} */ colourGrid) {
		var	/** @type {number} */ mask = this.getMaskFromZoom(zoom),
			/** @type {Array<Uint16Array>} */ colourGrid16 = null,
			/** @type {Array<Uint32Array>} */ colourGrid32 = null;

		// remove bounded grid if present
		if (this.boundedGridType !== -1 && firstLayer) {
			// get the full resolution colour grid
			colourGrid = this.colourGrid;
			colourGrid16 = this.colourGrid16;
			colourGrid32 = this.colourGrid32;
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				if ((this.counter & 1) !== 0) {
					colourGrid = this.nextColourGrid;
					colourGrid16 = this.nextColourGrid16;
					colourGrid32 = this.nextColourGrid32;
				}
			}

			// clear the bounded grid border
			this.drawBoundedGridBorder(colourGrid, 0);

			// create the small grid if zoom < 1
			if (mask !== 0) {
				this.lastZoom16 = true;
				this.createSmallColourGrids(colourGrid16, colourGrid32, zoom);
				colourGrid = this.smallColourGrid;
			}

			// mark that conversion is complete
			mask = lastMask;

			firstLayer = false;
		}

		// check if mask changed
		if (mask !== lastMask) {
			// get the full resolution colour grid
			colourGrid = this.colourGrid;
			colourGrid16 = this.colourGrid16;
			colourGrid32 = this.colourGrid32;
			if (this.isPCA || this.isRuleTree || this.isSuper || this.isExtended) {
				if ((this.counter & 1) !== 0) {
					colourGrid = this.nextColourGrid;
					colourGrid16 = this.nextColourGrid16;
					colourGrid32 = this.nextColourGrid32;
				}
			}

			// create small colour grid if Zoom < 1
			if (mask !== 0) {
				// create the new small grid
				this.createSmallColourGrids(colourGrid16, colourGrid32, zoom);
				colourGrid = this.smallColourGrid;
			}
		}

		return [mask, colourGrid];
	};

	// project the life grid onto the canvas with transformation and clipping
	Life.prototype.renderGridProjectionClip = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dxy = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ transparentTarget = 0,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			// layer parameters
			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// max grid size
			/** @type {number} */ maxGridSize = this.maxGridSize,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// create the comparison masks for clipping
			/** @type {number} */ wt = ~mask,
			/** @type {number} */ ht = ~mask,

			// pixel when off max grid
			/** @type {number} */ offMaxGrid = this.boundaryColour,

			// pixel when off grid
			/** @type {number} */ offGrid = pixelColours[0],

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = bottomGrid,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction based on rotation
		dxy = Math.sin(this.camAngle / 180 * Math.PI) / this.camZoom;
		dyy = Math.cos(this.camAngle / 180 * Math.PI) / this.camZoom;

		// compute starting position
		sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);
		offGrid = this.pixelColours[0];

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}
		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			x = sx;
			for (w = 0; w < w8; w += 1) {
				// clip to the grid
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					// lookup the colour
					col = colourGrid[y & hm][x & wm] | 0;

					// set the pixel in the buffer
					data32[idx] = pixelColours[col];
				} else {
					// use the off grid colour
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;

				// update row position
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm] | 0;
					data32[idx] = pixelColours[col];
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm] | 0;
					data32[idx] = pixelColours[col];
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm] | 0;
					data32[idx] = pixelColours[col];
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm] | 0;
					data32[idx] = pixelColours[col];
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm] | 0;
					data32[idx] = pixelColours[col];
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm] | 0;
					data32[idx] = pixelColours[col];
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm] | 0;
					data32[idx] = pixelColours[col];
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;
			}

			// update column position
			sx += dxy;
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// get the transparent target
			transparentTarget = this.getTransparentTarget(layerTarget, i);

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dxy = dxy / this.camLayerDepth;
			dyy = dyy / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// create the comparison masks for clipping
			wt = ~mask;
			ht = ~mask;

			// compute starting position
			sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				x = sx;
				for (w = 0; w < w8; w += 1) {
					// clip to the grid
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						// lookup the colour
						col = colourGrid[y & hm][x & wm] | 0;

						// draw pixel if above the transparent target
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					// next pixel
					idx += 1;

					// update row position
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y > 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

				}
				// update column position
				sx += dxy;
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the life grid onto the canvas with transformation and clipping
	Life.prototype.renderGridProjectionClipNoRotate = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ dyx = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ transparentTarget = 0,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			// layer parameters
			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// max grid size
			/** @type {number} */ maxGridSize = this.maxGridSize,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// create the comparison masks for clipping
			/** @type {number} */ wt = ~mask,
			/** @type {number} */ ht = ~mask,

			// pixel when off max grid
			/** @type {number} */ offMaxGrid = this.boundaryColour,

			// pixel when off grid
			/** @type {number} */ offGrid = pixelColours[0],

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = bottomGrid,

			// colour grid row
			/** @type {Uint8Array} */ colourGridRow = null,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction based on rotation
		dyx = 1 / this.camZoom;
		dyy = 1 / this.camZoom;

		if (this.isTriangular) {
			dyy /= ViewConstants.triangularYFactor;
		}

		if (this.isHex) {
			dyy /= ViewConstants.hexagonalYFactor;
			if (dyy > 1 / ViewConstants.minZoom) {
				dyy = 1 / ViewConstants.minZoom;
				dyx = dyy * ViewConstants.hexagonalYFactor
			}
		}

		// compute starting position
		sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);
		offGrid = this.pixelColours[0];

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}
		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			// clip rows to the grid
			if (y >= 0 && ((y & ht) === (y & hm))) {
				// get the colour grid row
				colourGridRow = colourGrid[y & hm];
				x = sx;

				// offset if hex rule
				if (this.isHex) {
					x += 0.5 * (y | 0);
				}

				// process the row
				for (w = 0; w < w8; w += 1) {
					// clip to the grid
					if (x >= 0 && ((x & wt) === (x & wm))) {
						// lookup the colour
						col = colourGridRow[x & wm] | 0;

						// set the pixel in the buffer
						data32[idx] = pixelColours[col];
					} else {
						// use the off grid colour
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;

					// update row position
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm] | 0;
						data32[idx] = pixelColours[col];
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm] | 0;
						data32[idx] = pixelColours[col];
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm] | 0;
						data32[idx] = pixelColours[col];
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm] | 0;
						data32[idx] = pixelColours[col];
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm] | 0;
						data32[idx] = pixelColours[col];
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm] | 0;
						data32[idx] = pixelColours[col];
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm] | 0;
						data32[idx] = pixelColours[col];
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
					}
					idx += 1;
					x += dyx;
				}
			} else {
				// draw off grid row
				if (y + yadj >= 0 && y + yadj < maxGridSize) {
					x = sx;

					// offset if hex rule
					if (this.isHex) {
						x += 0.5 * (y | 0);
					}

					// process the row
					for (w = 0; w < this.displayWidth; w += 1) {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
						idx += 1;
						x += dyx;
					}
				} else {
					data32.fill(offMaxGrid, idx, idx + this.displayWidth + 1);
					idx += this.displayWidth;
				}
			}

			// update column position
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// get the transparent target
			transparentTarget = this.getTransparentTarget(layerTarget, i);

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dyy = dyy / this.camLayerDepth;
			dyx = dyx / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// create the comparison masks for clipping
			wt = ~mask;
			ht = ~mask;

			// compute starting position
			sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				// clip rows to the grid
				if (y >= 0 && ((y & ht) === (y & hm))) {
					// get the colour grid row
					colourGridRow = colourGrid[y & hm];
					x = sx;

					// offset if hex rule
					if (this.isHex) {
						x += 0.5 * (y | 0);
					}

					// process the row
					for (w = 0; w < w8; w += 1) {
						// clip to the grid
						if (x >= 0 && ((x & wt) === (x & wm))) {
							// lookup the colour
							col = colourGridRow[x & wm] | 0;

							// draw pixel if above the transparent target
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						// next pixel
						idx += 1;

						// update row position
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGrid[y & hm][x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGrid[y & hm][x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGrid[y & hm][x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGrid[y & hm][x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGrid[y & hm][x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGrid[y & hm][x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGrid[y & hm][x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;
					}
				} else {
					// skip blank row
					idx += (w8 << 3);
				}

				// update column position
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the life grid onto the canvas with transformation with no clipping
	Life.prototype.renderGridProjectionNoClip = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dxy = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ transparentTarget = 0,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = bottomGrid,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction based on rotation
		dxy = Math.sin(this.camAngle / 180 * Math.PI) / this.camZoom;
		dyy = Math.cos(this.camAngle / 180 * Math.PI) / this.camZoom;

		// compute starting position
		sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			x = sx;

			for (w = 0; w < w8; w += 1) {
				// lookup the colour
				col = colourGrid[y & hm][x & wm] | 0;

				// set the pixel in the buffer
				data32[idx] = pixelColours[col];
				idx += 1;

				// update row position
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm] | 0;
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm] | 0;
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm] | 0;
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm] | 0;
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm] | 0;
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm] | 0;
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm] | 0;
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyy;
				y -= dxy;
			}

			// update column position
			sx += dxy;
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// get the transparent target
			transparentTarget = this.getTransparentTarget(layerTarget, i);

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dxy = dxy / this.camLayerDepth;
			dyy = dyy / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// compute starting position
			sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				x = sx;
				for (w = 0; w < w8; w += 1) {
					// lookup the colour
					col = colourGrid[y & hm][x & wm] | 0;

					// draw pixel if above the transparent target
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}

					// next pixel
					idx += 1;

					// update row position
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

				}

				// update column position
				sx += dxy;
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the life grid onto the canvas with transformation with no clipping and no rotation
	Life.prototype.renderGridProjectionNoClipNoRotate = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ dyx = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ transparentTarget = 0,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = bottomGrid,

			// colour grid row
			/** @type {Uint8Array} */ colourGridRow = null,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction
		dyx = 1 / this.camZoom;
		dyy = 1 / this.camZoom;

		if (this.isTriangular) {
			dyy /= ViewConstants.triangularYFactor;
		}

		if (this.isHex) {
			dyy /= ViewConstants.hexagonalYFactor;
			if (dyy > 1 / ViewConstants.minZoom) {
				dyy = 1 / ViewConstants.minZoom;
				dyx = dyy * ViewConstants.hexagonalYFactor
			}
		}


		// compute starting position
		sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			// get the colour grid row
			colourGridRow = colourGrid[y & hm];
			x = sx;

			// offset if hex rule
			if (this.isHex) {
				x += 0.5 * (y | 0);
			}

			// process the row
			for (w = 0; w < w8; w += 1) {
				// lookup the colour
				col = colourGridRow[x & wm];

				// set the pixel in the buffer
				data32[idx] = pixelColours[col];
				idx += 1;

				// update row position
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				data32[idx] = pixelColours[col];
				idx += 1;
				x += dyx;
			}

			// update column position
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// get the transparent target
			transparentTarget = this.getTransparentTarget(layerTarget, i);

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dyy = dyy / this.camLayerDepth;
			dyx = dyx / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// compute starting position
			sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				// get the colour grid row
				colourGridRow = colourGrid[y & hm];
				x = sx;

				// offset if hex rule
				if (this.isHex) {
					x += 0.5 * (y | 0);
				}

				// process the row
				for (w = 0; w < w8; w += 1) {
					// lookup the colour
					col = colourGridRow[x & wm] | 0;

					// draw pixel if above the transparent target
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}

					// next pixel
					idx += 1;

					// update row position
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;
				}

				// update column position
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the life grid onto the canvas with transformation
	Life.prototype.renderGridProjection = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow, /** @type {boolean} */ drawingStars) {
		// compute deltas in horizontal and vertical direction based on rotation
		var	/** @type {number} */ dxy = Math.sin(this.camAngle / 180 * Math.PI) / this.camZoom,
			/** @type {number} */ dyy = Math.cos(this.camAngle / 180 * Math.PI) / this.camZoom,
			/** @type {number} */ yFactor = this.getYFactor(),

			// display width and height
			/** @type {number} */ width = this.displayWidth,
			/** @type {number} */ height = this.displayHeight,

			// compute bottom left
			/** @type {number} */ bottomLeftY = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy / yFactor) + this.camYOff,
			/** @type {number} */ bottomLeftX = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff,

			// compute bottom right
			/** @type {number} */ bottomRightY = bottomLeftY + width * (-dxy),
			/** @type {number} */ bottomRightX = bottomLeftX + width * dyy,

			// compute top left
			/** @type {number} */ topLeftY = bottomLeftY + height * dyy / yFactor,
			/** @type {number} */ topLeftX = bottomLeftX + height * dxy,

			// compute top right
			/** @type {number} */ topRightY = topLeftY + width * (-dxy),
			/** @type {number} */ topRightX = topLeftX + width * dyy,

			// initialise the bounding box
			/** @type {number} */ boundTop = topLeftY,
			/** @type {number} */ boundLeft = topLeftX,
			/** @type {number} */ boundBottom = topLeftY,
			/** @type {number} */ boundRight = topLeftX,

			// timing
			/** @type {number} */ timing = performance.now();

		// set the left X extent
		if (topRightX < boundLeft) {
			boundLeft = topRightX;
		}
		if (bottomLeftX < boundLeft) {
			boundLeft = bottomLeftX;
		}
		if (bottomRightX < boundLeft) {
			boundLeft = bottomRightX;
		}

		// set the right X extent
		if (topRightX > boundRight) {
			boundRight = topRightX;
		}
		if (bottomLeftX > boundRight) {
			boundRight = bottomLeftX;
		}
		if (bottomRightX > boundRight) {
			boundRight = bottomRightX;
		}

		// set the top Y extent
		if (topRightY > boundTop) {
			boundTop = topRightY;
		}
		if (bottomLeftY > boundTop) {
			boundTop = bottomLeftY;
		}
		if (bottomRightY > boundTop) {
			boundTop = bottomRightY;
		}

		// set the bottom Y extent
		if (topRightY < boundBottom) {
			 boundBottom = topRightY;
		}
		if (bottomLeftY < boundBottom) {
			boundBottom = bottomLeftY;
		}
		if (bottomRightY < boundBottom) {
			boundBottom = bottomRightY;
		}

		// adjust for hex grid
		if (this.isHex) {
			boundLeft += boundBottom / 2;
			boundRight += boundTop / 2;
		}

		// check whether clipping is required
		if ((boundLeft | 0) < 0 || (boundRight | 0) >= this.width || (boundBottom | 0) < 0 || (boundTop | 0) >= this.height) {
			// check angle
			if (this.camAngle === 0) {
				// check for icons
				if (this.camZoom >= 8 && this.iconsAvailable && this.view.useIcons) {
					this.renderGridProjectionIcons(bottomGrid, boundLeft, boundBottom, boundRight, boundTop, drawingSnow, drawingStars);
				} else {
					// render with clipping and no rotation
					if (this.pretty && !this.isHex && this.camZoom >= 1 && this.layers === 1 && this.tilt === 0) {
						this.createPixelColours(1);
						this.renderGridProjectionPretty(bottomGrid, boundLeft, boundBottom, boundRight, boundTop, drawingSnow, drawingStars);
					} else {
						// check whether to draw layers
						if ((this.layersOn && this.layers > 1 && this.camLayerDepth > 1) || this.isHex || !(Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled)) {
							this.renderGridProjectionClipNoRotate(bottomGrid, layersGrid, mask, drawingSnow);
						} else {
							this.createPixelColours(1);
							WASM.renderGridClipNoRotate(
								bottomGrid.whole.byteOffset | 0,
								mask | 0,
								this.pixelColours.byteOffset | 0,
								this.data32.byteOffset | 0,
								this.displayWidth | 0,
								this.displayHeight | 0,
								this.camXOff,
								this.camYOff,
								this.widthMask | 0,
								this.heightMask | 0,
								bottomGrid[0].length | 0,
								this.camZoom,
								this.isTriangular ? ViewConstants.triangularYFactor : this.isHex ? ViewConstants.hexagonalYFactor : 1,
								this.maxGridSize | 0,
								this.width | 0,
								this.height | 0,
								this.boundaryColour | 0,
								this.xOffsets.byteOffset | 0,
								this.xMaxOffsets.byteOffset | 0
							);

							// draw grid lines and snow
							this.drawGridLinesAndSnow(drawingSnow);
						}

						timing = performance.now() - timing;
						if (Controller.wasmTiming) {
							this.view.menuManager.updateTimingItem("renderGridClip", timing, Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled && !this.isHex);
						}
					}
				}
			} else {
				// render with clipping and rotation
				this.renderGridProjectionClip(bottomGrid, layersGrid, mask, drawingSnow);
			}
		} else {
			// check angle
			if (this.camAngle === 0) {
				// check for icons
				if (this.camZoom >= 8 && this.iconsAvailable && this.view.useIcons) {
					this.renderGridProjectionIcons(bottomGrid, boundLeft, boundBottom, boundRight, boundTop, drawingSnow, drawingStars);
				} else {
					// render with no clipping and rotation
					if (this.pretty && !this.isHex && this.camZoom >= 1 && this.layers === 1 && this.tilt === 0) {
						this.createPixelColours(1);
						this.renderGridProjectionPretty(bottomGrid, boundLeft, boundBottom, boundRight, boundTop, drawingSnow, drawingStars);
					} else {
						// check whether to draw layers
						if ((this.layersOn && this.layers > 1 && this.camLayerDepth > 1) || this.isHex || !(Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled)) {
							this.renderGridProjectionNoClipNoRotate(bottomGrid, layersGrid, mask, drawingSnow);
						} else {
							this.createPixelColours(1);
							WASM.renderGridNoClipNoRotate(
								bottomGrid.whole.byteOffset | 0,
								mask | 0,
								this.pixelColours.byteOffset | 0,
								this.data32.byteOffset | 0,
								this.displayWidth | 0,
								this.displayHeight | 0,
								this.camXOff,
								this.camYOff,
								this.widthMask | 0,
								this.heightMask | 0,
								bottomGrid[0].length | 0,
								this.camZoom,
								this.isTriangular ? ViewConstants.triangularYFactor : this.isHex ? ViewConstants.hexagonalYFactor : 1,
								this.xOffsets.byteOffset | 0
							);

							// draw grid lines and snow
							this.drawGridLinesAndSnow(drawingSnow);
						}

						timing = performance.now() - timing;
						if (Controller.wasmTiming) {
							this.view.menuManager.updateTimingItem("renderGridNoClip", timing, Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled && !this.isHex);
						}
					}
				}
			} else {
				this.renderGridProjectionNoClip(bottomGrid, layersGrid, mask, drawingSnow);
			}
		}
	};

	// render the grid using icons
	Life.prototype.renderGridProjectionIcons = function(/** @type {Array<Uint8Array>} */ grid, /** @type {number} */ leftX, /** @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY, /** @type {boolean} */ drawingSnow, /** @type {boolean} */ drawingStars) {
		var	/** @const {HTMLCanvasElement} */ iconCanvas = this.iconCanvas,
			/** @const {CanvasRenderingContext2D} */ iconContext = this.iconContext,
			/** @const {ImageData} */ iconImageData = this.iconImageData,
			/** @const {Uint32Array} */ iconImageData32 = new Uint32Array(iconImageData.data.buffer),
			/** @const {CanvasRenderingContext2D} */ ctx = this.context,
			/** @type {HTMLCanvasElement} */ cellIconCanvas = null,
			/** @type {ImageData} */ cellIconImageData = null,
			/** @type {Uint32Array} */ cellIconImageData32 = null,
			/** @type {Uint8Array} */ gridRow = null,
			/** @type {number} */ width = rightX - leftX,
			/** @type {number} */ height = topY - bottomY,
			/** @const {number} */ yZoom = this.getYZoom(this.camZoom),
			/** @type {number} */ dx = leftX < 0 ? -(leftX - Math.ceil(leftX)) * this.camZoom : -(leftX - Math.floor(leftX)) * this.camZoom,
			/** @type {number} */ dy = bottomY < 0 ? -(bottomY - Math.ceil(bottomY)) * yZoom : -(bottomY - Math.floor(bottomY)) * yZoom,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ iScale = 0,
			/** @type {number} */ jScale = 0,
			/** @const {number} */ scale = this.camZoom,
			/** @type {number} */ state = 0,
			/** @type {number} */ maxIcon = 0,
			/** @const {number} */ maxGridSize = this.maxGridSize,
			/** @const {number} */ widthMask = this.width - 1,
			/** @const {number} */ heightMask = this.height - 1,
			/** @const {number} */ boundaryColour = this.boundaryColour,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @const {boolean} */ drawGridLines = (this.displayGrid || this.cellBorders) && this.canDisplayGrid(),
			/** @type {Array<number>} */ rowPos = [],
			/** @type {Array<number>} */ colPos = [],
			/** @type {boolean} */ drawMajor = (this.gridLineMajor > 0 && this.gridLineMajorEnabled),
			/** @const {number} */ gridLineMajor = this.gridLineMajor,
			/** @const {number} */ gridCol = this.gridLineColour,
			/** @const {number} */ gridBoldCol = this.gridLineBoldColour,
			/** @type {number} */ iconSize = 0,
			/** @type {number} */ cellSize = 0,
			/** @type {number} */ mx = 0,
			/** @type {number} */ my = 0,
			/** @const {number} */ diedColour = this.pixelColours[this.historyStates],
			/** @const {number} */ destWidth = iconCanvas.width,
			/** @type {number} */ srcX = 0,
			/** @type {number} */ destX = 0,
			/** @type {number} */ cellColour = 0;

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}

		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// align coordinates size to integers
		bottomY |= 0;
		leftX |= 0;
		width = (width | 0) + 2;
		height = (height | 0) + 2;
		rightX = leftX + width;
		topY = bottomY + height;

		// update the pixel colours for PCA rule
		if (this.isPCA) {
			this.createPixelColours(1);
			this.createPCAIcons(null);
		} else {
			if (this.boundedGridType !== -1) {
				this.createPixelColours(1);
			}
		}

		// select the correct set of icons for the scale
		if (scale < 16) {
			cellIconCanvas = this.cellIconCanvas7;
			cellIconImageData = this.cellIconImageData7;
			iconSize = 7;
		} else {
			if (scale < 32) {
				cellIconCanvas = this.cellIconCanvas15;
				cellIconImageData = this.cellIconImageData15;
				iconSize = 15;
			} else {
				cellIconCanvas = this.cellIconCanvas31;
				cellIconImageData = this.cellIconImageData31;
				iconSize = 31;
			}
		}
		cellIconImageData32 = new Uint32Array(cellIconImageData.data.buffer);

		// cell size is one row and column bigger than the icons
		cellSize = iconSize + 1;

		// compute number of available icons
		maxIcon = cellIconCanvas.height / cellIconCanvas.width;

		// draw each row of cells
		j = 0;

		// adjust for zoom and offset
		dy -= yZoom;
		bottomY -= 1;
		height += 1;

		dx -= this.camZoom;
		leftX -= 1;
		width += 1;

		for (y = bottomY; y < topY; y += 1) {
			if ((y & heightMask) === y) {
				gridRow = grid[y];
			}

			// draw each cell on the row
			jScale = (j * scale) | 0;
			rowPos[rowPos.length] = jScale;

			i = 0;
			for (x = leftX; x < rightX; x += 1) {
				iScale = (i * scale) | 0;
				if (y === bottomY) {
					colPos[colPos.length] = iScale;
				}

				// get the offset on the icon grid to write the icon to
				destX = j * cellSize * destWidth + i * cellSize;

				// check if the cell is on the maximum grid
				if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
					// draw filled square in boundary colour
					for (my = 0; my < cellSize; my += 1) {
						for (mx = 0; mx < cellSize; mx += 4) {
							iconImageData32[destX] = boundaryColour;
							destX += 1;
							iconImageData32[destX] = boundaryColour;
							destX += 1;
							iconImageData32[destX] = boundaryColour;
							destX += 1;
							iconImageData32[destX] = boundaryColour;
							destX += 1;
						}
						destX += (destWidth - cellSize);
					}
				} else {
					// check if the cell is on the current allocated grid
					if (!(((x & widthMask) !== x) || ((y & heightMask) !== y))) {
						// get the cell state
						state = gridRow[x];

						// check for bounded grid border
						if (this.boundedGridType !== -1 && state === this.boundedBorderColour) {
							// draw filled square in bounded grid colour
							cellColour = this.pixelColours[this.boundedBorderColour];

							for (my = 0; my < cellSize; my += 1) {
								for (mx = 0; mx < cellSize; mx += 4) {
									iconImageData32[destX] = cellColour;
									destX += 1;
									iconImageData32[destX] = cellColour;
									destX += 1;
									iconImageData32[destX] = cellColour;
									destX += 1;
									iconImageData32[destX] = cellColour;
									destX += 1;
								}
								destX += (destWidth - cellSize);
							}
						} else {
							// check for PCA rule
							if (this.isPCA) {
								if (state <= this.historyStates) {
									// draw filled square in state colour
									cellColour = this.pixelColours[state];

									for (my = 0; my < cellSize; my += 1) {
										for (mx = 0; mx < cellSize; mx += 4) {
											iconImageData32[destX] = cellColour;
											destX += 1;
											iconImageData32[destX] = cellColour;
											destX += 1;
											iconImageData32[destX] = cellColour;
											destX += 1;
											iconImageData32[destX] = cellColour;
											destX += 1;
										}
										destX += (destWidth - cellSize);
									}
								} else {
									// draw icon
									state -= this.historyStates;
									srcX = (state - 1) * iconSize * iconSize;

									// copy the icon
									for (my = 0; my < iconSize; my += 1) {
										// copy the row in 4 pixel chunks for speed
										for (mx = 0; mx < iconSize - 3; mx += 4) {
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
										}

										// copy remaining 3 pixels
										iconImageData32[destX] = cellIconImageData32[srcX];
										destX += 1;
										srcX += 1;
										iconImageData32[destX] = cellIconImageData32[srcX];
										destX += 1;
										srcX += 1;
										iconImageData32[destX] = cellIconImageData32[srcX];
										destX += 1;
										srcX += 1;

										// add the right hand border
										iconImageData32[destX] = diedColour;
										destX += (destWidth - iconSize);
									}

									// add the bottom row in 4 pixel chunks
									for (mx = 0; mx < cellSize; mx += 4) {
										iconImageData32[destX] = diedColour;
										destX += 1;
										iconImageData32[destX] = diedColour;
										destX += 1;
										iconImageData32[destX] = diedColour;
										destX += 1;
										iconImageData32[destX] = diedColour;
										destX += 1;
									}
								}
							} else {
								// RuleLoader rule
								// check if there is a state for the icon
								if (state === 0 || state > maxIcon) {
									// draw filled square in state colour
									cellColour = this.pixelColours[state];

									for (my = 0; my < cellSize; my += 1) {
										for (mx = 0; mx < cellSize; mx += 4) {
											iconImageData32[destX] = cellColour;
											destX += 1;
											iconImageData32[destX] = cellColour;
											destX += 1;
											iconImageData32[destX] = cellColour;
											destX += 1;
											iconImageData32[destX] = cellColour;
											destX += 1;
										}
										destX += (destWidth - cellSize);
									}
								} else {
									// draw icon
									state -= this.historyStates;

									// use background colour for cell icon borders
									cellColour = this.pixelColours[0];
									srcX = (state - 1) * iconSize * iconSize;

									// copy the icon
									for (my = 0; my < iconSize; my += 1) {
										// copy the row in 4 pixel chunks for speed
										for (mx = 0; mx < iconSize - 3; mx += 4) {
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
											iconImageData32[destX] = cellIconImageData32[srcX];
											destX += 1;
											srcX += 1;
										}

										// copy remaining 3 pixels
										iconImageData32[destX] = cellIconImageData32[srcX];
										destX += 1;
										srcX += 1;
										iconImageData32[destX] = cellIconImageData32[srcX];
										destX += 1;
										srcX += 1;
										iconImageData32[destX] = cellIconImageData32[srcX];
										destX += 1;
										srcX += 1;

										// add the right hand border
										iconImageData32[destX] = cellColour;
										destX += (destWidth - iconSize);
									}

									// add the bottom border in 4 pixel chunks
									for (mx = 0; mx < cellSize; mx += 4) {
										iconImageData32[destX] = cellColour;
										destX += 1;
										iconImageData32[destX] = cellColour;
										destX += 1;
										iconImageData32[destX] = cellColour;
										destX += 1;
										iconImageData32[destX] = cellColour;
										destX += 1;
									}
								}
							}
						}
					} else {
						// draw filled square in background colour
						cellColour = this.pixelColours[0];

						for (my = 0; my < cellSize; my += 1) {
							for (mx = 0; mx < cellSize; mx += 4) {
								iconImageData32[destX] = cellColour;
								destX += 1;
								iconImageData32[destX] = cellColour;
								destX += 1;
								iconImageData32[destX] = cellColour;
								destX += 1;
								iconImageData32[destX] = cellColour;
								destX += 1;
							}
							destX += (destWidth - cellSize);
						}
					}
				}
				i += 1;
			}
			j += 1;
		}

		// draw to the icon grid
		iconContext.putImageData(iconImageData, 0, 0);

		// scale the icon grid onto the display
		iconContext.imageSmoothingEnabled = true;
		iconContext.imageSmoothingQuality = "high";
		ctx.drawImage(iconCanvas, 0, 0, width * cellSize, height * cellSize, dx, dy, width * this.camZoom, height * yZoom);
		iconContext.imageSmoothingEnabled = false;

		// draw the grid lines if required
		if (drawGridLines) {
			// draw the standard grid lines
			if (this.cellBorders && !this.displayGrid) {
				ctx.strokeStyle = this.getColourString(this.pixelColours[0]);
				drawMajor = false;
			} else {
				ctx.strokeStyle = this.getColourString(gridCol);
			}
			ctx.lineWidth = 1;
			ctx.beginPath();

			// draw grid line rows
			for (y = 0; y < rowPos.length; y += 1) {
				j = ((rowPos[y] + dy) | 0) + 0.5;
				ctx.moveTo(0, j);
				ctx.lineTo(this.displayWidth, j);
			}

			// draw grid line columns
			for (x = 0; x < colPos.length; x += 1) {
				i = ((colPos[x] + dx) | 0) + 0.5;
				ctx.moveTo(i, 0);
				ctx.lineTo(i, this.displayHeight);
			}

			// render lines
			ctx.stroke();

			// draw grid major line rows if needed
			if (drawMajor) {
				// compute major grid line horizontal offset
				my = -(this.displayHeight / 2 / this.getYZoom(this.camZoom)) - (this.height / 2 - this.yOff - this.originY) | 0;
				my += ((this.view.patternHeight / 2) | 0) - 7;

				mx = -(this.displayWidth / 2 / this.camZoom) - (this.width / 2 - this.xOff - this.originX) | 0;
				mx += ((this.view.patternWidth / 2) | 0) - 7;

				ctx.strokeStyle = this.getColourString(gridBoldCol);
				ctx.beginPath();

				// draw grid line rows
				for (y = 0; y < rowPos.length; y += 1) {
					if ((my % gridLineMajor) === 0) {
						j = ((rowPos[y] + dy) | 0) + 0.5;
						ctx.moveTo(0, j);
						ctx.lineTo(this.displayWidth, j);
					}
					my += 1;
				}

				// draw grid line columns
				for (x = 0; x < colPos.length; x += 1) {
					if ((mx % gridLineMajor) === 0) {
						i = ((colPos[x] + dx) | 0) + 0.5;
						ctx.moveTo(i, 0);
						ctx.lineTo(i, this.displayHeight);
					}
					mx += 1;
				}

				// render lines
				ctx.stroke();
			}
		}

		// update the image data if further rendering is required
		if (drawingSnow || drawingStars) {
			// update the image data array from the rendered image
			this.imageData = this.context.getImageData(0, 0, this.context.canvas.width, this.context.canvas.height);
			this.data32 = new Uint32Array(this.imageData.data.buffer);

			// draw snow if enabled - TBD snow will stop on grid lines since it is drawn after when the icon renderer is used
			if (drawingSnow) {
				this.drawSnow();
			}
		} else {
			// no need to draw the grid since it's already been rendered and there are no overlays (snow or stars)
			this.doDrawGrid = false;
		}
	};

	// render the grid using anti-aliasing
	Life.prototype.renderGridProjectionPretty = function(/** @type {Array<Uint8Array>} */ grid, /** @type {number} */ leftX, /** @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY, /** @type {boolean} */ drawingSnow, /** @type {boolean} */ drawingStars) {
		var	/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ sData32 = this.sData32,
			/** @type {HTMLCanvasElement} */ sCanvas = this.sCanvas,
			/** @type {CanvasRenderingContext2D} */ sContext = this.sContext,
			/** @type {ImageData} */ sImageData = this.sImageData,
			/** @const {number} */ intZoom2 = (this.camZoom | 0) << 1,
			/** @const {number} */ sWidth = sCanvas.width,
			/** @const {number} */ sZWidth = sWidth * intZoom2,
			/** @type {Uint8Array} */ gridRow = null,
			/** @type {number} */ width = rightX - leftX,
			/** @type {number} */ height = topY - bottomY,
			/** @type {number} */ yZoom = this.getYZoom(this.camZoom),
			/** @type {number} */ dx = leftX < 0 ? -(leftX - Math.ceil(leftX)) * this.camZoom : -(leftX - Math.floor(leftX)) * this.camZoom,
			/** @type {number} */ dy = bottomY < 0 ? -(bottomY - Math.ceil(bottomY)) * yZoom : -(bottomY - Math.floor(bottomY)) * yZoom,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ l = 0,
			/** @type {number} */ xz = 0,
			/** @type {number} */ yz = 0,
			/** @type {number} */ col = 0,
			/** @type {number} */ maxGridSize = this.maxGridSize,
			/** @const {number} */ widthMask = this.width - 1,
			/** @const {number} */ heightMask = this.height - 1,
			/** @type {number} */ gridCol = this.gridLineColour,
			/** @type {number} */ gridBoldCol = this.gridLineBoldColour,
			/** @type {number} */ backgroundCol = this.pixelColours[0],
			/** @type {number} */ boundaryCol = this.boundaryColour,
			/** @type {boolean} */ drawMajor = (this.gridLineMajor > 0 && this.gridLineMajorEnabled && !this.cellBorders),
			/** @type {number} */ majorX = 0,
			/** @type {number} */ majorY = 0,
			/** @type {number} */ majorXStart = 0,
			/** @type {number} */ odd = this.counter & 1,
			/** @const {number} */ intZoom = intZoom2 >> 1,
			/** @type {number} */ intWidth2 = 0,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @type {boolean} */ drawGridLines = (this.displayGrid || this.cellBorders) && this.canDisplayGrid(),
			/** @type {Array<number>} */ gridCols = [],
			/** @type {Array<number>} */ gridRows = [],
			/** @type {number} */ xOff = 0,
			/** @type {number} */ yOff = 0;

		// check for cell borders
		if (this.cellBorders && !this.displayGrid && this.canDisplayGrid()) {
			gridCol = backgroundCol;
			drawMajor = false;
		}

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}

		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// align coordinates size to integers
		bottomY |= 0;
		leftX |= 0;
		width = (width | 0) + 2;
		height = (height | 0) + 2;
		rightX = leftX + width;
		topY = bottomY + height;

		// use optimized scaling based on zoom level
		switch (intZoom) {
		case 1:
			// draw 2x2 cells
			j = sZWidth;
			intWidth2 = width << 1;

			// draw each row of cells
			for (y = bottomY; y < topY; y += 1) {
				if ((y & heightMask) === y) {
					gridRow = grid[y];
				}
				l = i;

				// draw each cell on the row
				for (x = leftX; x < rightX; x += 1) {
					// check if the cell is on the maximum grid
					if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
						col = boundaryCol;
					} else {
						// check if the cell is on the current allocated grid
						if (((x & widthMask) !== x) || ((y & heightMask) !== y)) {
							col = backgroundCol;
						} else {
							col = pixelColours[gridRow[x]];
						}
					}

					sData32[i] = col;
					sData32[i + 1] = col;
					i += 2;
				}

				i = l + sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i = j;
				j += sZWidth;
			}
			break;

		case 2:
			// draw 4x4 cells
			j = sZWidth;
			intWidth2 = width << 2;

			// draw each row of cells
			for (y = bottomY; y < topY; y += 1) {
				if ((y & heightMask) === y) {
					gridRow = grid[y];
				}
				l = i;

				// draw each cell on the row
				for (x = leftX; x < rightX; x += 1) {
					// check if the cell is on the maximum grid
					if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
						col = boundaryCol;
					} else {
						// check if the cell is on the current allocated grid
						if (((x & widthMask) !== x) || ((y & heightMask) !== y)) {
							col = backgroundCol;
						} else {
							col = pixelColours[gridRow[x]];
						}
					}

					sData32[i] = col;
					sData32[i + 1] = col;
					sData32[i + 2]= col;
					sData32[i + 3] = col;
					i += 4;
				}

				i = l + sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i += sWidth;
				l += sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i += sWidth;
				l += sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i = j;
				j += sZWidth;
			}
			break;

		default:
			// draw NxN cells
			j = sZWidth;
			intWidth2 = width * intZoom2;

			// compute major gridlines row and column start
			if (drawMajor) {
				// for Margolus alternate major/minor grid lines for odd/even generations
				if (!this.isMargolus || this.gridLineMajor !== 2 || !this.altGrid) {
					odd = 0;
				}

				majorXStart = (-(this.displayWidth / 2 / this.camZoom) - (this.width / 2 - this.xOff - this.originX) + (this.view.patternWidth / 2) + 0.05) | 0;
				majorXStart += odd;

				majorY = (-(this.displayHeight / 2 / this.camZoom) - (this.height / 2 - this.yOff - this.originY) + (this.view.patternHeight / 2) + 0.05) | 0;
				majorY += odd;
			}

			// draw each row of cells
			for (y = bottomY; y < topY; y += 1) {
				if ((y & heightMask) === y) {
					gridRow = grid[y];
				}
				l = i;

				majorX = majorXStart;

				// draw each cell on the row
				for (x = leftX; x < rightX; x += 1) {

					// check if the cell is on the maximum grid
					if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
						col = boundaryCol;
					} else {
						// check if the cell is on the current allocated grid
						if (((x & widthMask) !== x) || ((y & heightMask) !== y)) {
							col = backgroundCol;
						} else {
							col = pixelColours[gridRow[x]];
						}
					}

					for (xz = 0; xz < intZoom; xz += 1) {
						sData32[i] = col;
						sData32[i + 1] = col;
						i += 2;
					}

					if (drawGridLines && y === bottomY) {
						if (drawMajor && ((majorX % this.gridLineMajor) === 0)) {
							gridCols[gridCols.length] = gridBoldCol;
						} else {
							gridCols[gridCols.length] = gridCol;
						}
					}

					majorX += 1;
				}

				i = l + sWidth;
				for (yz = 1; yz < intZoom2; yz += 1) {
					sData32.copyWithin(i, l, l + intWidth2);
					l += sWidth;
					i += sWidth;
				}

				if (drawGridLines) {
					if (drawMajor && ((majorY % this.gridLineMajor) === 0)) {
						gridRows[gridRows.length] = gridBoldCol;
					} else {
						gridRows[gridRows.length] = gridCol;
					}
				}

				i = j;
				j += sZWidth;

				majorY += 1;
			}
			break;
		}

		// put the integer zoom cells from the image data onto the canvas
		sContext.putImageData(sImageData, 0, 0, 0, 0, width * intZoom2, height * intZoom2);

		// pretty scale to the display
		this.context.imageSmoothingEnabled = true;
		this.context.drawImage(this.sCanvas, 0, 0, width * intZoom2, height * intZoom2, dx, dy, width * this.camZoom, height * yZoom);
		this.context.imageSmoothingEnabled = false;

		// update the image data if further rendering is required
		if (drawingSnow || drawingStars || drawGridLines) {
			// update the image data array from the rendered image
			this.imageData = this.context.getImageData(0, 0, this.context.canvas.width, this.context.canvas.height);
			this.data32 = new Uint32Array(this.imageData.data.buffer);

			// draw vertical grid lines
			if (drawGridLines) {
				i = 0;
				xOff = (((this.width / 2 - (this.xOff + this.originX)) * this.camZoom) + (this.displayWidth / 2)) % this.camZoom;
				x = -this.camZoom + xOff;
				for (j = leftX; j <= rightX; j += 1) {
					this.drawVLine(x | 0, 0, this.displayHeight - 1, gridCols[i]);
					x += this.camZoom;
					i += 1;
				}

				// draw horizontal grid lines
				i = 0;
				yOff = (((this.height / 2 - (this.yOff + this.originY)) * this.camZoom) + (this.displayHeight / 2)) % this.camZoom;
				y = -this.camZoom + yOff;
				for (j = bottomY; j <= topY; j += 1) {
					this.drawHLine(0, this.displayWidth - 1, y | 0, gridRows[i]);
					y += this.camZoom;
					i += 1;
				}
			}

			// draw snow if enabled
			if (drawingSnow) {
				this.drawSnow();
			}
		} else {
			// no need to draw the grid since it's already been rendered and there are no overlays (snow, stars, gridlines)
			this.doDrawGrid = false;
		}
	};

	// render the grid with overlay using anti-aliasing
	Life.prototype.renderGridOverlayProjectionPretty = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ leftX, /** @type {number} */ bottomY, /** @type {number} */ rightX, /** @type {number} */ topY, /** @type {boolean} */ drawingSnow, /** @type {boolean} */ drawingStars) {
		var	/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ sData32 = this.sData32,
			/** @type {HTMLCanvasElement} */ sCanvas = this.sCanvas,
			/** @type {CanvasRenderingContext2D} */ sContext = this.sContext,
			/** @type {ImageData} */ sImageData = this.sImageData,
			/** @const {number} */ intZoom2 = (this.camZoom | 0) << 1,
			/** @const {number} */ sWidth = sCanvas.width,
			/** @const {number} */ sZWidth = sWidth * intZoom2,
			/** @type {Uint8Array} */ gridRow = null,
			/** @type {Array<Uint8Array>} */ grid = layersGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = bottomGrid,
			/** @type {Uint8Array} */ overlayRow = null,
			/** @type {number} */ width = rightX - leftX,
			/** @type {number} */ height = topY - bottomY,
			/** @type {number} */ yZoom = this.getYZoom(this.camZoom),
			/** @type {number} */ dx = -(leftX - Math.floor(leftX)) * this.camZoom,
			/** @type {number} */ dy = -(bottomY - Math.floor(bottomY)) * yZoom,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ i = 0,
			/** @type {number} */ j = 0,
			/** @type {number} */ l = 0,
			/** @type {number} */ xz = 0,
			/** @type {number} */ yz = 0,
			/** @type {number} */ col = 0,
			/** @type {number} */ over = 0,
			/** @type {number} */ maxGridSize = this.maxGridSize,
			/** @const {number} */ widthMask = this.width - 1,
			/** @const {number} */ heightMask = this.height - 1,
			/** @type {number} */ gridCol = this.gridLineColour,
			/** @type {number} */ gridBoldCol = this.gridLineBoldColour,
			/** @type {number} */ backgroundCol = this.pixelColours[0],
			/** @type {number} */ boundaryCol = this.boundaryColour,
			/** @type {boolean} */ drawMajor = (this.gridLineMajor > 0 && this.gridLineMajorEnabled && !this.cellBorders),
			/** @type {number} */ majorX = 0,
			/** @type {number} */ majorY = 0,
			/** @type {number} */ majorXStart = 0,
			/** @type {number} */ odd = this.counter & 1,
			/** @const {number} */ intZoom = intZoom2 >> 1,
			/** @type {number} */ intWidth2 = 0,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @const {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @const {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @const {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @const {number} */ state6 = ViewConstants.stateMap[6] + 128,
			/** @const {number} */ aliveStart = this.aliveStart,
			/** @type {boolean} */ drawGridLines = (this.displayGrid || this.cellBorders) && this.canDisplayGrid(),
			/** @type {Array<number>} */ gridCols = [],
			/** @type {Array<number>} */ gridRows = [],
			/** @type {number} */ xOff = 0,
			/** @type {number} */ yOff = 0;

		// check for cell borders
		if (this.cellBorders && !this.displayGrid && this.canDisplayGrid()) {
			gridCol = backgroundCol;
			drawMajor = false;
		}

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}

		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// align coordinates size to integers
		bottomY |= 0;
		leftX |= 0;
		width = (width | 0) + 2;
		height = (height | 0) + 2;
		rightX = leftX + width;
		topY = bottomY + height;

		// use optimized scaling based on zoom level
		switch (intZoom) {
		case 1:
			// draw 2x2 cells
			j = sZWidth;
			intWidth2 = width << 1;

			// draw each row of cells
			for (y = bottomY; y < topY; y += 1) {
				if ((y & heightMask) === y) {
					gridRow = grid[y];
					overlayRow = overlayGrid[y];
				}
				l = i;

				// draw each cell on the row
				for (x = leftX; x < rightX; x += 1) {
					// check if the cell is on the maximum grid
					if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
						col = boundaryCol;
					} else {
						// check if the cell is on the current allocated grid
						if (((x & widthMask) !== x) || ((y & heightMask) !== y)) {
							col = backgroundCol;
						} else {
							col = gridRow[x];
							over = overlayRow[x];
							if (over === state4 || over === state6) {
								if (col >= aliveStart) {
									over = state3;
								}
								col = pixelColours[over];
							} else {
								if (over === state3 || over === state5) {
									if (col < aliveStart) {
										over = state4;
									}
									col = pixelColours[over];
								} else {
									col = pixelColours[col];
								}
							}
						}
					}

					sData32[i] = col;
					sData32[i + 1] = col;
					i += 2;
				}

				i = l + sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i = j;
				j += sZWidth;
			}
			break;

		case 2:
			// draw 4x4 cells
			j = sZWidth;
			intWidth2 = width << 2;

			// draw each row of cells
			for (y = bottomY; y < topY; y += 1) {
				if ((y & heightMask) === y) {
					gridRow = grid[y];
					overlayRow = overlayGrid[y];
				}
				l = i;

				// draw each cell on the row
				for (x = leftX; x < rightX; x += 1) {
					// check if the cell is on the maximum grid
					if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
						col = boundaryCol;
					} else {
						// check if the cell is on the current allocated grid
						if (((x & widthMask) !== x) || ((y & heightMask) !== y)) {
							col = backgroundCol;
						} else {
							col = gridRow[x];
							over = overlayRow[x];
							if (over === state4 || over === state6) {
								if (col >= aliveStart) {
									over = state3;
								}
								col = pixelColours[over];
							} else {
								if (over === state3 || over === state5) {
									if (col < aliveStart) {
										over = state4;
									}
									col = pixelColours[over];
								} else {
									col = pixelColours[col];
								}
							}
						}
					}

					sData32[i] = col;
					sData32[i + 1] = col;
					sData32[i + 2]= col;
					sData32[i + 3] = col;
					i += 4;
				}

				i = l + sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i += sWidth;
				l += sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i += sWidth;
				l += sWidth;
				sData32.copyWithin(i, l, l + intWidth2);
				i = j;
				j += sZWidth;
			}
			break;

		default:
			// draw NxN cells
			j = sZWidth;
			intWidth2 = width * intZoom2;

			// compute major gridlines row and column start
			if (drawMajor) {
				// for Margolus alternate major/minor grid lines for odd/even generations
				if (!this.isMargolus || this.gridLineMajor !== 2 || !this.altGrid) {
					odd = 0;
				}

				majorXStart = (-(this.displayWidth / 2 / this.camZoom) - (this.width / 2 - this.xOff - this.originX) + (this.view.patternWidth / 2) + 0.05) | 0;
				majorXStart += odd;

				majorY = (-(this.displayHeight / 2 / this.camZoom) - (this.height / 2 - this.yOff - this.originY) + (this.view.patternHeight / 2) + 0.05) | 0;
				majorY += odd;
			}

			// draw each row of cells
			for (y = bottomY; y < topY; y += 1) {
				if ((y & heightMask) === y) {
					gridRow = grid[y];
					overlayRow = overlayGrid[y];
				}
				l = i;

				majorX = majorXStart;

				// draw each cell on the row
				for (x = leftX; x < rightX; x += 1) {
					// check if the cell is on the maximum grid
					if (x + xadj < 0 || x + xadj >= maxGridSize || y + yadj < 0 || y + yadj >= maxGridSize) {
						col = boundaryCol;
					} else {
						// check if the cell is on the current allocated grid
						if (((x & widthMask) !== x) || ((y & heightMask) !== y)) {
							col = backgroundCol;
						} else {
							col = gridRow[x];
							over = overlayRow[x];
							if (over === state4 || over === state6) {
								if (col >= aliveStart) {
									over = state3;
								}
								col = pixelColours[over];
							} else {
								if (over === state3 || over === state5) {
									if (col < aliveStart) {
										over = state4;
									}
									col = pixelColours[over];
								} else {
									col = pixelColours[col];
								}
							}
						}
					}

					for (xz = 0; xz < intZoom; xz += 1) {
						sData32[i] = col;
						sData32[i + 1] = col;
						i += 2;
					}

					if (drawGridLines && y === bottomY) {
						if (drawMajor && ((majorX % this.gridLineMajor) === 0)) {
							gridCols[gridCols.length] = gridBoldCol;
						} else {
							gridCols[gridCols.length] = gridCol;
						}
					}

					majorX += 1;
				}

				i = l + sWidth;
				for (yz = 1; yz < intZoom2; yz += 1) {
					sData32.copyWithin(i, l, l + intWidth2);
					l += sWidth;
					i += sWidth;
				}

				if (drawGridLines) {
					if (drawMajor && ((majorY % this.gridLineMajor) === 0)) {
						gridRows[gridRows.length] = gridBoldCol;
					} else {
						gridRows[gridRows.length] = gridCol;
					}
				}

				i = j;
				j += sZWidth;

				majorY += 1;
			}
			break;
		}

		// put the integer zoom cells from the image data onto the canvas
		sContext.putImageData(sImageData, 0, 0, 0, 0, width * intZoom2, height * intZoom2);

		// pretty scale to the display
		this.context.imageSmoothingEnabled = true;
		this.context.drawImage(this.sCanvas, 0, 0, width * intZoom2, height * intZoom2, dx, dy, width * this.camZoom, height * yZoom);
		this.context.imageSmoothingEnabled = false;

		// update the image data if further rendering is required
		if (drawingSnow || drawingStars || drawGridLines) {
			// update the image data array from the rendered image
			this.imageData = this.context.getImageData(0, 0, this.context.canvas.width, this.context.canvas.height);
			this.data32 = new Uint32Array(this.imageData.data.buffer);

			// draw vertical grid lines
			if (drawGridLines) {
				i = 0;
				xOff = (((this.width / 2 - (this.xOff + this.originX)) * this.camZoom) + (this.displayWidth / 2)) % this.camZoom;
				x = -this.camZoom + xOff;
				for (j = leftX; j <= rightX; j += 1) {
					this.drawVLine(x | 0, 0, this.displayHeight - 1, gridCols[i]);
					x += this.camZoom;
					i += 1;
				}

				// draw horizontal grid lines
				i = 0;
				yOff = (((this.height / 2 - (this.yOff + this.originY)) * this.camZoom) + (this.displayHeight / 2)) % this.camZoom;
				y = -this.camZoom + yOff;
				for (j = bottomY; j <= topY; j += 1) {
					this.drawHLine(0, this.displayWidth - 1, y | 0, gridRows[i]);
					y += this.camZoom;
					i += 1;
				}
			}

			// draw snow if enabled
			if (drawingSnow) {
				this.drawSnow();
			}
		} else {
			// no need to draw the grid since it's already been rendered and there are no overlays (snow, stars, gridlines)
			this.doDrawGrid = false;
		}
	};

	// project the life grid onto the canvas with transformation
	Life.prototype.renderGridOverlayProjectionClip = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dxy = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ transparentTarget = 0,

			// get states 3, 4, 5 and 6
			/** @type {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @type {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @type {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			// layer parameters
			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// max grid size
			/** @type {number} */ maxGridSize = this.maxGridSize,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// index of overlay colour
			/** @type {number} */ over = 0,

			// computed pixel colour
			/** @type {number} */ pixel = 0,

			// first alive colour
			/** @type {number} */ aliveStart = this.aliveStart,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// create the comparison masks for clipping
			/** @type {number} */ wt = ~mask,
			/** @type {number} */ ht = ~mask,

			// pixel when off max grid
			/** @type {number} */ offMaxGrid = this.boundaryColour,

			// pixel when off grid
			/** @type {number} */ offGrid = pixelColours[0],

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = layersGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = bottomGrid,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction based on rotation
		dxy = Math.sin(this.camAngle / 180 * Math.PI) / this.camZoom;
		dyy = Math.cos(this.camAngle / 180 * Math.PI) / this.camZoom;

		// compute starting position
		sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);
		offGrid = this.pixelColours[0];

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}
		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			x = sx;
			for (w = 0; w < w8; w += 1) {
				// clip to the grid
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					// lookup the colour and overlay
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];

					// states 4 and 6
					if (over === state4 || over === state6) {
						// if alive cell then use state 3
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						// states 3 and 5
						if (over === state3 || over === state5) {
							// if dead cell then use state 4
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}

					// set the pixel colour
					data32[idx] = pixel;
				} else {
					// use the off grid colour
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;

				// update row position
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];
					if (over === state4 || over === state6) {
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						if (over === state3 || over === state5) {
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}
					data32[idx] = pixel;
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];
					if (over === state4 || over === state6) {
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						if (over === state3 || over === state5) {
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}
					data32[idx] = pixel;
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];
					if (over === state4 || over === state6) {
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						if (over === state3 || over === state5) {
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}
					data32[idx] = pixel;
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];
					if (over === state4 || over === state6) {
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						if (over === state3 || over === state5) {
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}
					data32[idx] = pixel;
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];
					if (over === state4 || over === state6) {
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						if (over === state3 || over === state5) {
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}
					data32[idx] = pixel;
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];
					if (over === state4 || over === state6) {
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						if (over === state3 || over === state5) {
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}
					data32[idx] = pixel;
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
					col = colourGrid[y & hm][x & wm];
					over = overlayGrid[y & hm][x & wm];
					if (over === state4 || over === state6) {
						if (col >= aliveStart) {
							over = state3;
						}
						pixel = pixelColours[over] | 0;
					} else {
						if (over === state3 || over === state5) {
							if (col < aliveStart) {
								over = state4;
							}
							pixel = pixelColours[over] | 0;
						} else {
							pixel = pixelColours[col] | 0;
						}
					}
					data32[idx] = pixel;
				} else {
					if (x + xadj >= 0 && x + xadj < maxGridSize && y + yadj >= 0 && y + yadj < maxGridSize) {
						data32[idx] = offGrid;
					} else {
						data32[idx] = offMaxGrid;
					}
				}
				idx += 1;
				x += dyy;
				y -= dxy;
			}

			// update column position
			sx += dxy;
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// compute the transparent target
			if (this.multiNumStates > 2) {
				// use number of generations states as maximum
				if (this.historyStates === 0) {
					transparentTarget = (i * (this.multiNumStates / this.layers)) | 0;
				} else {
					if (i < layerTarget / 2) {
						transparentTarget = (i * 2 * (this.historyStates / this.layers)) | 0;
					} else {
						transparentTarget = this.historyStates + (i * (this.multiNumStates / this.layers)) | 0;
					}
				}
			} else {
				if (this.cellRenderer === LifeConstants.shaderRainbow) {  // TBD rainbow
					transparentTarget = 1;
				} else {
					transparentTarget = (i * ((this.aliveMax + 1) / this.layers)) | 0;
				}
			}

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dxy = dxy / this.camLayerDepth;
			dyy = dyy / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// create the comparison masks for clipping
			wt = ~mask;
			ht = ~mask;

			// compute starting position
			sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				x = sx;
				for (w = 0; w < w8; w += 1) {
					// clip to the grid
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						// lookup the colour
						col = colourGrid[y & hm][x & wm] | 0;

						// draw pixel if above the transparent target
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					// next pixel
					idx += 1;

					// update row position
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					if ((x >= 0 && ((x & wt) === (x & wm))) && (y >= 0 && ((y & ht) === (y & hm)))) {
						col = colourGrid[y & hm][x & wm] | 0;
						if (col >= transparentTarget) {
							data32[idx] = pixelColours[col];
						}
					}
					idx += 1;
					x += dyy;
					y -= dxy;

				}

				// update column position
				sx += dxy;
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the life grid onto the canvas with transformation but no clipping
	Life.prototype.renderGridOverlayProjectionNoClip = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dxy = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ transparentTarget = 0,

			// get states 3, 4, 5 and 6
			/** @type {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @type {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @type {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// index of overlay colour
			/** @type {number} */ over = 0,

			// computed pixel colour
			/** @type {number} */ pixel = 0,

			// first alive colour
			/** @type {number} */ aliveStart = this.aliveStart,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = layersGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = bottomGrid,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction based on rotation
		dxy = Math.sin(this.camAngle / 180 * Math.PI) / this.camZoom;
		dyy = Math.cos(this.camAngle / 180 * Math.PI) / this.camZoom;

		// compute starting position
		sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			x = sx;
			for (w = 0; w < w8; w += 1) {
				// lookup the colour and overlay
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];

				// states 4 and 6
				if (over === state4 || over === state6) {
					// if alive cell then use state 3
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					// states 3 and 5
					if (over === state3 || over === state5) {
						// if dead cell then use state 4
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}

				// set the pixel colour
				data32[idx] = pixel;
				idx += 1;

				// update row position
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyy;
				y -= dxy;

				// loop unroll
				col = colourGrid[y & hm][x & wm];
				over = overlayGrid[y & hm][x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyy;
				y -= dxy;
			}

			// update column position
			sx += dxy;
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// compute the transparent target
			if (this.multiNumStates > 2) {
				// use number of generations states as maximum
				if (this.historyStates === 0) {
					transparentTarget = (i * (this.multiNumStates / this.layers)) | 0;
				} else {
					if (i < layerTarget / 2) {
						transparentTarget = (i * 2 * (this.historyStates / this.layers)) | 0;
					} else {
						transparentTarget = this.historyStates + (i * (this.multiNumStates / this.layers)) | 0;
					}
				}
			} else {
				if (this.cellRenderer === LifeConstants.shaderRainbow) {  // TBD rainbow
					transparentTarget = 1;
				} else {
					transparentTarget = (i * ((this.aliveMax + 1) / this.layers)) | 0;
				}
			}

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dxy = dxy / this.camLayerDepth;
			dyy = dyy / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// compute starting position
			sy = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				x = sx;
				for (w = 0; w < w8; w += 1) {
					// lookup the colour
					col = colourGrid[y & hm][x & wm] | 0;

					// draw pixel if above the transparent target
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					// next pixel
					idx += 1;

					// update row position
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

					// loop unroll
					col = colourGrid[y & hm][x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyy;
					y -= dxy;

				}

				// update column position
				sx += dxy;
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the life grid onto the canvas with transformation but no clipping or rotation
	Life.prototype.renderGridOverlayProjectionNoClipNoRotate = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ dyx = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ transparentTarget = 0,

			// get states 3, 4, 5 and 6
			/** @type {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @type {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @type {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// index of overlay colour
			/** @type {number} */ over = 0,

			// computed pixel colour
			/** @type {number} */ pixel = 0,

			// first alive colour
			/** @type {number} */ aliveStart = this.aliveStart,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = layersGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = bottomGrid,

			// colour and overlay grid rows
			/** @type {Uint8Array} */ colourGridRow = null,
			/** @type {Uint8Array} */ overlayGridRow = null,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction based on rotation
		dyy = 1 / this.camZoom;
		dyx = 1/ this.camZoom;

		if (this.isTriangular) {
			dyy /= ViewConstants.triangularYFactor;
		}

		if (this.isHex) {
			dyy /= ViewConstants.hexagonalYFactor;
			if (dyy > 1 / ViewConstants.minZoom) {
				dyy = 1 / ViewConstants.minZoom;
				dyx = dyy * ViewConstants.hexagonalYFactor
			}
		}

		// compute starting position
		sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			// get the colour grid row
			colourGridRow = colourGrid[y & hm];
			overlayGridRow = overlayGrid[y & hm];
			x = sx;

			// offset if hex rule
			if (this.isHex) {
				x += 0.5 * (y | 0);
			}

			// process the row
			for (w = 0; w < w8; w += 1) {
				// lookup the colour and overlay
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];

				// states 4 and 6
				if (over === state4 || over === state6) {
					// if alive cell then use state 3
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					// states 3 and 5
					if (over === state3 || over === state5) {
						// if dead cell then use state 4
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}

				// set the pixel colour
				data32[idx] = pixel;
				idx += 1;

				// update row position
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyx;

				// loop unroll
				col = colourGridRow[x & wm];
				over = overlayGridRow[x & wm];
				if (over === state4 || over === state6) {
					if (col >= aliveStart) {
						over = state3;
					}
					pixel = pixelColours[over] | 0;
				} else {
					if (over === state3 || over === state5) {
						if (col < aliveStart) {
							over = state4;
						}
						pixel = pixelColours[over] | 0;
					} else {
						pixel = pixelColours[col] | 0;
					}
				}
				data32[idx] = pixel;
				idx += 1;
				x += dyx;
			}

			// update column position
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// compute the transparent target
			if (this.multiNumStates > 2) {
				// use number of generations states as maximum
				if (this.historyStates === 0) {
					transparentTarget = (i * (this.multiNumStates / this.layers)) | 0;
				} else {
					if (i < layerTarget / 2) {
						transparentTarget = (i * 2 * (this.historyStates / this.layers)) | 0;
					} else {
						transparentTarget = this.historyStates + (i * (this.multiNumStates / this.layers)) | 0;
					}
				}
			} else {
				if (this.cellRenderer === LifeConstants.shaderRainbow) {  // TBD rainbow
					transparentTarget = 1;
				} else {
					transparentTarget = (i * ((this.aliveMax + 1) / this.layers)) | 0;
				}
			}

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dyy = dyy / this.camLayerDepth;
			dyx = dyx / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// compute starting position
			sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				// get the colour grid row
				colourGridRow = colourGrid[y & hm];
				x = sx;

				// offset if hex rule
				if (this.isHex) {
					x += 0.5 * (y | 0);
				}

				// process the row
				for (w = 0; w < w8; w += 1) {
					// lookup the colour
					col = colourGridRow[x & wm] | 0;

					// draw pixel if above the transparent target
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}

					// next pixel
					idx += 1;

					// update row position
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;

					// loop unroll
					col = colourGridRow[x & wm] | 0;
					if (col >= transparentTarget) {
						data32[idx] = pixelColours[col];
					}
					idx += 1;
					x += dyx;
				}

				// update column position
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the life grid onto the canvas with transformation with clipping but no rotation
	Life.prototype.renderGridOverlayProjectionClipNoRotate = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow) {
		var	/** @type {number} */ w8 = this.displayWidth >> 3,
			/** @type {Uint32Array} */ pixelColours = this.pixelColours,
			/** @type {Uint32Array} */ data32 = this.data32,
			/** @type {number} */ i = 0,
			/** @type {number} */ h = 0,
			/** @type {number} */ w = 0,
			/** @type {number} */ x = 0,
			/** @type {number} */ y = 0,
			/** @type {number} */ dyy = 0,
			/** @type {number} */ dyx = 0,
			/** @type {number} */ sy = 0,
			/** @type {number} */ sx = 0,
			/** @type {number} */ xadj = 0,
			/** @type {number} */ yadj = 0,
			/** @type {number} */ xg = this.width,
			/** @type {number} */ yg = this.height,
			/** @type {number} */ transparentTarget = 0,

			// get states 3, 4, 5 and 6
			/** @type {number} */ state3 = ViewConstants.stateMap[3] + 128,
			/** @type {number} */ state4 = ViewConstants.stateMap[4] + 128,
			/** @type {number} */ state5 = ViewConstants.stateMap[5] + 128,
			/** @type {number} */ state6 = ViewConstants.stateMap[6] + 128,

			// index in pixel buffer
			/** @type {number} */ idx = 0,

			// layer parameters
			/** @type {number} */ layerTarget = 1,
			/** @type {number} */ brightness = 1,
			/** @type {number} */ brightInc = 0,

			// max grid size
			/** @type {number} */ maxGridSize = this.maxGridSize,

			// index of pixel colour
			/** @type {number} */ col = 0,

			// index of overlay colour
			/** @type {number} */ over = 0,

			// computed pixel colour
			/** @type {number} */ pixel = 0,

			// pixel when off max grid
			/** @type {number} */ offMaxGrid = this.boundaryColour,

			// pixel when off grid
			/** @type {number} */ offGrid = pixelColours[0],

			// first alive colour
			/** @type {number} */ aliveStart = this.aliveStart,

			// create the width and height masks
			/** @type {number} */ wm = this.widthMask & ~mask,
			/** @type {number} */ hm = this.heightMask & ~mask,

			// create the comparison masks for clipping
			/** @type {number} */ wt = ~mask,
			/** @type {number} */ ht = ~mask,

			// start with bottom grid
			/** @type {Array<Uint8Array>} */ colourGrid = layersGrid,
			/** @type {Array<Uint8Array>} */ overlayGrid = bottomGrid,

			// colour and overlay grid rows
			/** @type {Uint8Array} */ colourGridRow = null,
			/** @type {Uint8Array} */ overlayGridRow = null,

			// layer preparation result
			/** @type {Array} */ prepResult = [],

			// current layer zoom
			/** @type {number} */ layerZoom = this.camZoom,
			/** @type {boolean} */ firstLayer = true;

		// check whether to draw layers
		if (this.layersOn && this.camLayerDepth > 1) {
			layerTarget = this.layers;
		}

		// compute deltas in horizontal and vertical direction based on rotation
		dyy = 1 / this.camZoom;
		dyx = 1 / this.camZoom;

		if (this.isTriangular) {
			dyy /= ViewConstants.triangularYFactor;
		}

		if (this.isHex) {
			dyy /= ViewConstants.hexagonalYFactor;
			if (dyy > 1 / ViewConstants.minZoom) {
				dyy = 1 / ViewConstants.minZoom;
				dyx = dyy * ViewConstants.hexagonalYFactor
			}
		}

		// compute starting position
		sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
		sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

		// check if depth shading is on
		if (this.depthOn && this.layers > 1 && this.camLayerDepth > 1) {
			brightInc = 0.4 / (this.layers - 1);
			brightness = 0.6;
		}

		// create pixel colours
		this.createPixelColours(brightness);
		offGrid = this.pixelColours[0];

		// compute the x and y adjustments for full grid size
		while (xg < maxGridSize) {
			xadj += xg >> 1;
			xg <<= 1;
		}
		while (yg < maxGridSize) {
			yadj += yg >> 1;
			yg <<= 1;
		}

		// draw each pixel
		idx = 0;
		y = sy;

		for (h = 0; h < this.displayHeight; h += 1) {
			// clip rows to the grid
			if (y >= 0 && ((y & ht) === (y & hm))) {
				// get the colour grid row
				colourGridRow = colourGrid[y & hm];
				overlayGridRow = overlayGrid[y & hm];
				x = sx;

				// offset if hex rule
				if (this.isHex) {
					x += 0.5 * (y | 0);
				}

				// process the row
				for (w = 0; w < w8; w += 1) {
					// clip to the grid
					if (x >= 0 && ((x & wt) === (x & wm))) {
						// lookup the colour and overlay
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];

						// states 4 and 6
						if (over === state4 || over === state6) {
							// if alive cell then use state 3
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							// states 3 and 5
							if (over === state3 || over === state5) {
								// if dead cell then use state 4
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						// use the off grid colour
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}

					// set the pixel colour
					data32[idx] = pixel;
					idx += 1;

					// update row position
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];
						if (over === state4 || over === state6) {
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							if (over === state3 || over === state5) {
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}
					data32[idx] = pixel;
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];
						if (over === state4 || over === state6) {
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							if (over === state3 || over === state5) {
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}
					data32[idx] = pixel;
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];
						if (over === state4 || over === state6) {
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							if (over === state3 || over === state5) {
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}
					data32[idx] = pixel;
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];
						if (over === state4 || over === state6) {
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							if (over === state3 || over === state5) {
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}
					data32[idx] = pixel;
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];
						if (over === state4 || over === state6) {
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							if (over === state3 || over === state5) {
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}
					data32[idx] = pixel;
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];
						if (over === state4 || over === state6) {
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							if (over === state3 || over === state5) {
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}
					data32[idx] = pixel;
					idx += 1;
					x += dyx;

					// loop unroll
					if (x >= 0 && ((x & wt) === (x & wm))) {
						col = colourGridRow[x & wm];
						over = overlayGridRow[x & wm];
						if (over === state4 || over === state6) {
							if (col >= aliveStart) {
								over = state3;
							}
							pixel = pixelColours[over] | 0;
						} else {
							if (over === state3 || over === state5) {
								if (col < aliveStart) {
									over = state4;
								}
								pixel = pixelColours[over] | 0;
							} else {
								pixel = pixelColours[col] | 0;
							}
						}
					} else {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							pixel = offGrid;
						} else {
							pixel = offMaxGrid;
						}
					}
					data32[idx] = pixel;
					idx += 1;
					x += dyx;
				}
			} else {
				// draw off grid row
				if (y + yadj >= 0 && y + yadj < maxGridSize) {
					x = sx;

					// offset if hex rule
					if (this.isHex) {
						x += 0.5 * (y | 0);
					}

					// process the row
					for (w = 0; w < this.displayWidth; w += 1) {
						if (x + xadj >= 0 && x + xadj < maxGridSize) {
							data32[idx] = offGrid;
						} else {
							data32[idx] = offMaxGrid;
						}
						idx += 1;
						x += dyx;
					}
				} else {
					data32.fill(offMaxGrid, idx, idx + this.displayWidth + 1);
					idx += this.displayWidth;
				}
			}

			// update column position
			sy += dyy;
			y = sy;
		}

		// draw grid lines and snow
		this.drawGridLinesAndSnow(drawingSnow);

		// switch to layers grid
		colourGrid = layersGrid;

		// render each layer
		for (i = 1; i < layerTarget; i += 1) {
			// compute the transparent target
			if (this.multiNumStates > 2) {
				// use number of generations states as maximum
				if (this.historyStates === 0) {
					transparentTarget = (i * (this.multiNumStates / this.layers)) | 0;
				} else {
					if (i < layerTarget / 2) {
						transparentTarget = (i * 2 * (this.historyStates / this.layers)) | 0;
					} else {
						transparentTarget = this.historyStates + (i * (this.multiNumStates / this.layers)) | 0;
					}
				}
			} else {
				if (this.cellRenderer === LifeConstants.shaderRainbow) {  // TBD rainbow
					transparentTarget = 1;
				} else {
					transparentTarget = (i * ((this.aliveMax + 1) / this.layers)) | 0;
				}
			}

			// update brightness
			brightness += brightInc;

			// create pixel colours
			this.createPixelColours(brightness);

			// zoom for the next layer
			dyy = dyy / this.camLayerDepth;
			dyx = dyx / this.camLayerDepth;

			// update layer zoom
			layerZoom *= this.camLayerDepth;

			// check whether to switch grid resolution based on zoom
			prepResult = this.prepareNextLayer(layerZoom, firstLayer, mask, colourGrid);
			mask = prepResult[0];
			colourGrid = prepResult[1];
			firstLayer = false;

			// create the width and height masks
			wm = this.widthMask & ~mask;
			hm = this.heightMask & ~mask;

			// create the comparison masks for clipping
			wt = ~mask;
			ht = ~mask;

			// compute starting position
			sy = -((this.displayHeight / 2) * dyy) + this.camYOff;
			sx = -((this.displayWidth / 2) * dyx) + this.camXOff;

			// draw each pixel
			idx = 0;
			y = sy;
			for (h = 0; h < this.displayHeight; h += 1) {
				// clip rows to the grid
				if (y >= 0 && ((y & ht) === (y & hm))) {
					// get the colour grid row
					colourGridRow = colourGrid[y & hm];
					x = sx;

					// offset if hex rule
					if (this.isHex) {
						x += 0.5 * (y | 0);
					}

					// process the row
					for (w = 0; w < w8; w += 1) {
						// clip to the grid
						if (x >= 0 && ((x & wt) === (x & wm))) {
							// lookup the colour
							col = colourGridRow[x & wm] | 0;

							// draw pixel if above the transparent target
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}

						// next pixel
						idx += 1;

						// update row position
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGridRow[x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGridRow[x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGridRow[x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGridRow[x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGridRow[x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGridRow[x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;

						// loop unroll
						if (x >= 0 && ((x & wt) === (x & wm))) {
							col = colourGridRow[x & wm] | 0;
							if (col >= transparentTarget) {
								data32[idx] = pixelColours[col];
							}
						}
						idx += 1;
						x += dyx;
					}
				} else {
					// skip blank row
					idx += (w8 << 3);
				}

				// update column position
				sy += dyy;
				y = sy;
			}
		}
	};

	// project the overlay onto the canvas with transformation
	Life.prototype.renderGridOverlayProjection = function(/** @type {Array<Uint8Array>} */ bottomGrid, /** @type {Array<Uint8Array>} */ layersGrid, /** @type {number} */ mask, /** @type {boolean} */ drawingSnow, /** @type {boolean} */ drawingStars) {
		// compute deltas in horizontal and vertical direction based on rotation
		var	/** @type {number} */ dxy = Math.sin(this.camAngle / 180 * Math.PI) / this.camZoom,
			/** @type {number} */ dyy = Math.cos(this.camAngle / 180 * Math.PI) / this.camZoom,

			// display width and height
			/** @type {number} */ width = this.displayWidth,
			/** @type {number} */ height = this.displayHeight,

			// compute bottom left
			/** @type {number} */ bottomLeftY = -((this.displayWidth / 2) * (-dxy) + (this.displayHeight / 2) * dyy) + this.camYOff,
			/** @type {number} */ bottomLeftX = -((this.displayWidth / 2) * dyy + (this.displayHeight / 2) * dxy) + this.camXOff,

			// compute bottom right
			/** @type {number} */ bottomRightY = bottomLeftY + width * (-dxy),
			/** @type {number} */ bottomRightX = bottomLeftX + width * dyy,

			// compute top left
			/** @type {number} */ topLeftY = bottomLeftY + height * dyy,
			/** @type {number} */ topLeftX = bottomLeftX + height * dxy,

			// compute top right
			/** @type {number} */ topRightY = topLeftY + width * (-dxy),
			/** @type {number} */ topRightX = topLeftX + width * dyy,

			// initialise the bounding box
			/** @type {number} */ boundTop = topLeftY,
			/** @type {number} */ boundLeft = topLeftX,
			/** @type {number} */ boundBottom = topLeftY,
			/** @type {number} */ boundRight = topLeftX,

			// timing
			/** @type {number} */ timing = performance.now();

		// set the left X extent
		if (topRightX < boundLeft) {
			boundLeft = topRightX;
		}
		if (bottomLeftX < boundLeft) {
			boundLeft = bottomLeftX;
		}
		if (bottomRightX < boundLeft) {
			boundLeft = bottomLeftX;
		}

		// set the right X extent
		if (topRightX > boundRight) {
			boundRight = topRightX;
		}
		if (bottomLeftX > boundRight) {
			boundRight = bottomLeftX;
		}
		if (bottomRightX > boundRight) {
			boundRight = bottomRightX;
		}

		// set the top Y extent
		if (topRightY > boundTop) {
			boundTop = topRightY;
		}
		if (bottomLeftY > boundTop) {
			boundTop = bottomLeftY;
		}
		if (bottomRightY > boundTop) {
			boundTop = bottomRightY;
		}

		// set the bottom Y extent
		if (topRightY < boundBottom) {
			 boundBottom = topRightY;
		}
		if (bottomLeftY < boundBottom) {
			boundBottom = bottomLeftY;
		}
		if (bottomRightY < boundBottom) {
			boundBottom = bottomRightY;
		}

		// check whether clipping is required
		if ((boundLeft | 0) < 0 || (boundRight | 0) >= this.width || (boundBottom | 0) < 0 || (boundTop | 0) >= this.height) {
			// check angle
			if (this.camAngle === 0) {
				// render with clipping and no rotation
				if (this.pretty && this.camZoom >= 1 && this.layers === 1 && this.tilt === 0) {
					this.createPixelColours(1);
					this.renderGridOverlayProjectionPretty(bottomGrid, layersGrid, boundLeft, boundBottom, boundRight, boundTop, drawingSnow, drawingStars);
				} else {
					// check whether to draw layers
					if ((this.layersOn && this.layers > 1 && this.camLayerDepth > 1) || this.isHex || this.isTriangular || !(Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled)) {
						this.renderGridOverlayProjectionClipNoRotate(bottomGrid, layersGrid, mask, drawingSnow);
					} else {
						this.createPixelColours(1);
						WASM.renderOverlayClipNoRotate(
							layersGrid.whole.byteOffset | 0,
							bottomGrid[0].length | 0,
							bottomGrid.whole.byteOffset | 0,
							mask | 0,
							this.pixelColours.byteOffset | 0,
							this.data32.byteOffset | 0,
							this.displayWidth | 0,
							this.displayHeight | 0,
							this.camXOff,
							this.camYOff,
							this.widthMask | 0,
							this.heightMask | 0,
							this.camZoom,
							(ViewConstants.stateMap[3] + 128) | 0,
							(ViewConstants.stateMap[4] + 128) | 0,
							(ViewConstants.stateMap[5] + 128) | 0,
							(ViewConstants.stateMap[6] + 128) | 0,
							LifeConstants.aliveStart | 0,
							this.maxGridSize | 0,
							this.width | 0,
							this.height | 0,
							this.boundaryColour | 0,
							this.xOffsets.byteOffset | 0,
							this.xMaxOffsets.byteOffset | 0
						);

						// draw grid lines and snow
						this.drawGridLinesAndSnow(drawingSnow);
					}

					timing = performance.now() - timing;
					if (Controller.wasmTiming) {
						this.view.menuManager.updateTimingItem("renderGridClip", timing, Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled);
					}
				}
			} else {
				// render with clipping
				this.renderGridOverlayProjectionClip(bottomGrid, layersGrid, mask, drawingSnow);
			}
		} else {
			// check angle
			if (this.camAngle === 0) {
				// render with no clipping and no rotation
				if (this.pretty && this.camZoom >= 1 && this.layers === 1 && this.tilt === 0) {
					this.createPixelColours(1);
					this.renderGridOverlayProjectionPretty(bottomGrid, layersGrid, boundLeft, boundBottom, boundRight, boundTop, drawingSnow, drawingStars);
				} else {
					// check whether to draw layers
					if ((this.layersOn && this.layers > 1 && this.camLayerDepth > 1) || this.isHex || this.isTriangular || !(Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled)) {
						this.renderGridOverlayProjectionNoClipNoRotate(bottomGrid, layersGrid, mask, drawingSnow);
					} else {
						this.createPixelColours(1);
						WASM.renderOverlayNoClipNoRotate(
							layersGrid.whole.byteOffset | 0,
							bottomGrid[0].length | 0,
							bottomGrid.whole.byteOffset | 0,
							mask | 0,
							this.pixelColours.byteOffset | 0,
							this.data32.byteOffset | 0,
							this.displayWidth | 0,
							this.displayHeight | 0,
							this.camXOff,
							this.camYOff,
							this.widthMask | 0,
							this.heightMask | 0,
							this.camZoom,
							(ViewConstants.stateMap[3] + 128) | 0,
							(ViewConstants.stateMap[4] + 128) | 0,
							(ViewConstants.stateMap[5] + 128) | 0,
							(ViewConstants.stateMap[6] + 128) | 0,
							LifeConstants.aliveStart | 0,
							this.xOffsets.byteOffset | 0
						);

						// draw grid lines and snow
						this.drawGridLinesAndSnow(drawingSnow);
					}

					timing = performance.now() - timing;
					if (Controller.wasmTiming) {
						this.view.menuManager.updateTimingItem("renderGridNoClip", timing, Controller.useWASM && Controller.wasmEnableRenderGrid && this.view.wasmEnabled);
					}
				}
			} else {
				// render with no clipping
				this.renderGridOverlayProjectionNoClip(bottomGrid, layersGrid, mask, drawingSnow);
			}
		}
	};

	// copy back to mode 7 buffer
	// TBD src has no type information because of a type check issue
	Life.prototype.mode7Update = function(/** @type {Uint32Array} */ dest, src) {
		dest.set(src);
	};

	// draw canvas
	Life.prototype.drawGrid = function(/** @type {boolean} */ allowTilt) {
		var	/** @type {number} */ i = 0,
			/** @type {number} */ l,
			/** @type {Uint8ClampedArray} */ s,
			/** @type {Uint8Array} */ t;

		if (this.doDrawGrid) {
			// check if copy required
			if (!this.imageData.data.buffer) {
				s = this.imageData.data;
				t = this.data8;

				// get buffer length
				l = t.length;

				// copy buffer
				i = 0;
				while (i < l) {
					s[i] = t[i];
					i += 1;
					s[i] = t[i];
					i += 1;
					s[i] = t[i];
					i += 1;
					s[i] = t[i];
					i += 1;
				}
			}

			// tilt
			if (this.tilt !== 0 && allowTilt) {
				var	/** @type {number} */ y,
					/** @type {number} */ py,
					/** @type {number} */ sy,
					/** @type {number} */ x,
					/** @type {number} */ px,
					/** @type {number} */ sx,
					/** @type {number} */ ex,
					/** @type {number} */ pz,
					/** @type {number} */ width = this.context.canvas.width,
					/** @type {number} */ height = this.context.canvas.height,
					/** @type {Uint32Array} */ data32 = this.data32,
					/** @type {Uint32Array} */ mode7 = this.mode7Buffer,
					/** @type {number} */ halfwidth = width >> 1,
					/** @type {number} */ halfheight = height >> 1,
					/** @type {number} */ rowoffset = 0,
					/** @type {number} */ projrowoffset = 0,
					/** @type {number} */ mode7Angle = this.tilt - 1,
					/** @type {number} */ dx = 0,

					// use pixel 0 colour for pixels not on projected grid
					/** @type {number} */ offGrid = this.boundaryColour,

					// set the number of steps based on the tilt angle
					/** @type {number} */ mode7Step = (mode7Angle * 2 + 2) / height;

				// create the mode7 buffer if it has not been done so before
				if (this.mode7Buffer === null) {
					this.mode7Buffer = /** @type {!Uint32Array} */ (this.allocator.allocate(Type.Uint32, width * height, "Life.tiltBuffer", false));
					mode7 = this.mode7Buffer;
				}

				// ensure tilt around middle row
				pz = -mode7Angle;

				// tilt image
				for (y = 0; y < height; y += 1) {
					py = y - halfheight;
					if (pz <= 0) {
						sy = height;
					} else {
						sy = ((py / pz) + halfheight) | 0;
					}
					rowoffset = y * width;

					// check if row is on the image
					if (sy < 0 || sy >= height) {
						// not on image so fill with background pixels
						for (x = 0; x < width; x += 1) {
							mode7[rowoffset + x] = offGrid;
						}
					} else {
						// on the image so project row
						projrowoffset = sy * width;
						sx = ((-halfwidth / pz) + halfwidth);
						ex = ((halfwidth / pz) + halfwidth);
						dx = (ex - sx) / width;
						for (x = 0; x < width; x += 1) {
							// check if pixel is on the image
							px = sx | 0;
							if (px < 0 || px >= width) {
								mode7[rowoffset + x] = offGrid;
							} else {
								mode7[rowoffset + x] = data32[projrowoffset + px];
							}
							sx += dx;
						}
					}

					pz += mode7Step;
				}

				// copy projected image to image data
				this.mode7Update(data32, mode7);
			}

			// draw the image on the canvas
			this.context.putImageData(this.imageData, 0, 0);
		}
	};

	// clear pixels
	Life.prototype.clearPixels = function(/** @type {number} */ pixelColour) {
		var	/** @type {Uint32Array} */ data32 = this.data32;

		data32.fill(pixelColour);
	};
