(function()()

in slides/asset/common/Array2D.js [5:2154]


(function() {

  // Baseline setup
  // ==============

  // Establish the root object, `window` in the browser, or `exports` on the server.
  var root = this;

  // Save the previous value of the `Array2D` variable.
  var previousArray2D = root.Array2D;

  // Create a safe reference to the Array2D object for use below.
  var Array2D = function() {
    if (!(this instanceof Array2D)) return new Array2D();
  };

  // Export the Array2D object for Node.js, with backwards-compatibility for the
  // old `require()` API. If we're in the browser, add `Array2D` as a global object.
  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = Array2D;
    }
    exports.Array2D = Array2D;
  } else {
    root.Array2D = Array2D;
  }

  // Current version.
  Array2D.VERSION = '0.0.5';

  // Run Array2D.js in *noConflict* mode, returning the `Array2D` variable to its
  // previous owner. Returns a reference to the Array2D object.
  Array2D.noConflict = function() {
    root.Array2D = previousArray2D;
    return this;
  };  

  // Private utilities
  // =================

  // Return T/F if the passed `thing` is an array.
  function isArray(thing) {
    return Object.prototype.toString.call(thing) === '[object Array]';
  }

  // Return T/F if the passed `thing` is `null`.
  function isNull(thing) {
    return thing === null;
  }

  // Return T/F if the passed `thing` is `undefined`.
  function isUndefined(thing) {
    return thing === undefined;
  }

  // Return T/F if the passed `thing` is `null` or `undefined`.
  function isBlank(thing) {
    return isNull(thing) || isUndefined(thing);
  }

  // Return T/F if the passed `thing` is neither `null` nor `undefined`.
  function isPresent(thing) {
    return !isBlank(thing);
  }

  // Return T/F if the passed thing is not `undefined`.
  function isExistent(thing) {
    return !isUndefined(thing);
  }

  // Clone the given (flat) array.
  function cloneArray(array) {
    var clone = [];

    for (var i = 0, l = array.length; i < l; i++) {
      clone[i] = array[i];
    }

    return clone;
  }

  // Constants / enums
  // =================

  Array2D.AXES = {
    X: 1,
    Y: 2
  };

  Array2D.BEARINGS = {
    NORTH: 1,
    NORTHWEST: 2,
    NORTHEAST: 3,
    SOUTH: 4,
    SOUTHWEST: 5,
    SOUTHEAST: 6,
    EAST: 7,
    WEST: 8
  };

  Array2D.BOUNDARIES = {
    UPPER: 1,
    LOWER: 2,
    LEFT: 3,
    RIGHT: 4
  };

  Array2D.CORNERS = {
    TOP_LEFT: 1,
    TOP_RIGHT: 2,
    BOTTOM_LEFT: 3,
    BOTTOM_RIGHT: 4
  };

  Array2D.CROOKS = {
    UPPER_LEFT: 1,
    UPPER_RIGHT: 2,
    LOWER_LEFT: 3,
    LOWER_RIGHT: 4
  };

  Array2D.DIRECTIONS = {
    UP: 1,
    DOWN: 2,
    LEFT: 3,
    RIGHT: 4
  };

  Array2D.EDGES = {
    TOP: 1,
    BOTTOM: 2,
    LEFT: 3,
    RIGHT: 4
  };

  Array2D.QUADRANTS = {
    I: 1,
    II: 2,
    III: 3,
    IV: 4
  };

  // Basic functions
  // ===============

  // Return the value of the cell at (`r`,`c`).
  Array2D.get = function(grid, r, c) {
    if (!isArray(grid[r])) {
      return undefined;
    }

    return grid[r][c];
  };

  // Return a grid with the cell at (`r`,`c`) set to `value`.
  Array2D.set = function(grid, r, c, value) {
    var clone = Array2D.clone(grid);

    if(!isArray(clone[r])) {
      clone[r] = [];
    }

    clone[r][c] = value;

    return clone;
  };

  // Return the width of the grid.
  Array2D.width = function(grid) {
    return Array2D.widest(grid).length;
  };

  // Return the height of the grid.
  Array2D.height = function(grid) {
    return Array2D.tallest(grid).length;
  };

  // Return the dimensions of the grid (width and height),
  // iterating over the grid in a single pass. Faster than
  // calling `width` and `height` individually.
  Array2D.dimensions = function(grid) {
    var h = 0;
    var w = 0;

    for (var i = 0, rs = grid.length; i < rs; i++) {
      var l = grid[i].length;

      if (l > 0) h = i + 1; // The last row with any content is the longest
      if (l > w) w = l; // Check if the previous max width is beaten
    }

    return [w, h];
  }

  // Return the area of the grid.
  Array2D.area = function(grid) {
    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    return width * height;
  };

  // Return the number of present cells in the grid.
  Array2D.cells = function(grid) {
    var count = 0;

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        if (isExistent(cell)) {
          count++
        }
      }
    }

    return count;
  };

  // Essentials
  // ==========

  // Crop a subgrid of the given dimensions from the grid, but exclude
  // anything that would fall outside of the grid's bounds.
  Array2D.crop = function(grid, r, c, w, h) {
    var out = [];

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);    

    for (var i = 0; i < h; i++) {
      var ro = r + i; // Offset row

      // Skip any out-of-bounds cells.
      if (ro < height && ro >= 0) {
        out.push([]);

        for (var j = 0; j < w; j++) {
          var co = c + j; // Offset column

          // Skip any out-of-bounds cells.
          if (co < width && co >= 0) {
            var last = out[out.length - 1];
            var cell = grid[ro][co];
            last.push(cell);
          }
        }
      }
    }

    return out;
  };

  // Harvest a subgrid of the given dimensions from the grid. If access
  // goes outside of the grid's bounds, set those overlap cells to `null`.
  Array2D.harvest = function(grid, r, c, w, h) {
    var out = [];

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    for (var i = 0; i < h; i++) {
      out[i] = [];

      for (var j = 0; j < w; j++) {
        var ro = r + i; // Offset row
        var co = c + j; // Offset column

        // Set to `null` any out-of-bounds cell.
        if (ro >= height || ro < 0) {
          out[i][j] = null;
        }
        // Set to `null` any out-of-bounds cell.
        else if (co >= width || co < 0) {
          out[i][j] = null;
        }
        else {
          var cell = grid[ro][co];
          out[i][j] = cell;
        }
      }
    }

    return out;
  };

  // Rotate the grid one quarter-turn in the given `direction`.
  Array2D.rotate = function(grid, direction) {
    if (direction === Array2D.DIRECTIONS.LEFT) return Array2D.lrotate(grid);
    if (direction === Array2D.DIRECTIONS.RIGHT) return Array2D.rrotate(grid);
    throw("Array2D.js: Invalid direction provided for `rotate`");
  };

  // Rotate the grid to the left one quarter-turn.
  Array2D.lrotate = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.vflip(transposed);
  };

  // Rotate the grid to the right one quarter-turn.
  Array2D.rrotate = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.hflip(transposed);
  };

  // Flip the grid about the given axis `axis`.
  Array2D.flip = function(grid, axis) {
    if (axis === Array2D.AXES.X) return Array2D.vflip(grid);
    if (axis === Array2D.AXES.Y) return Array2D.hflip(grid);
    throw("Array2D.js: Invalid axis provided for `flip`");
  };

  // Flip the grid vertically, i.e., about its x-axis.
  Array2D.vflip = function(grid) {
    var out = [];

    for (var i = 0, l = grid.length; i < l; i++) {
      var opp = i - l + 1;
      out[i] = grid[Math.abs(opp)];
    }

    return out;
  };

  // Flip the grid horizontally, i.e., about its y-axis.
  Array2D.hflip = function(grid) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var opp = j - l2 + 1;
        out[i][j] = grid[i][Math.abs(opp)];
      }
    }

    return out;
  };

  // Pan the array in the given direction, the given number of steps.
  Array2D.pan = function(grid, direction, steps) {
    switch (direction) {
      case Array2D.DIRECTIONS.LEFT: return Array2D.lpan(grid, steps);
      case Array2D.DIRECTIONS.RIGHT: return Array2D.rpan(grid, steps);
      case Array2D.DIRECTIONS.UP: return Array2D.upan(grid, steps);
      case Array2D.DIRECTIONS.DOWN: return Array2D.dpan(grid, steps);
      default:
        throw("Array2D.js: Invalid direction provided for `pan`");
    }
  };

  // Pan the array up by the number of steps.
  Array2D.upan = function(grid, steps) {
    var panned = Array2D.clone(grid);

    steps || (steps = 1);
    while (steps > 0) {
      var last = panned.pop();
      panned.unshift(last);
      steps--;
    }

    return panned;
  };

  // Pan the array left by the number of steps.
  Array2D.lpan = function(grid, steps) {
    var transposed = Array2D.transpose(grid);

    var shifted = Array2D.upan(transposed, steps);

    return Array2D.transpose(shifted);
  };

  // Pan the array down by the number of steps.
  Array2D.dpan = function(grid, steps) {
    var panned = Array2D.clone(grid);

    steps || (steps = 1);
    while (steps > 0) {
      var first = panned.shift();
      panned.push(first);
      steps--;
    }

    return panned;
  };

  // Pan the array right by the number of steps.
  Array2D.rpan = function(grid, steps) {
    var transposed = Array2D.transpose(grid);

    var shifted = Array2D.dpan(transposed, steps);

    return Array2D.transpose(shifted);
  };

  // Slide performs a pan, but in the reverse direction specified.
  Array2D.slide = function(grid, direction, steps) {
    switch (direction) {
      case Array2D.DIRECTIONS.LEFT: return Array2D.lslide(grid, steps);
      case Array2D.DIRECTIONS.RIGHT: return Array2D.rslide(grid, steps);
      case Array2D.DIRECTIONS.UP: return Array2D.uslide(grid, steps);
      case Array2D.DIRECTIONS.DOWN: return Array2D.dslide(grid, steps);
      default:
        throw("Array2D.js: Invalid direction provided for `slide`");
    }
  };

  // Slide the grid right, the number of steps.
  Array2D.rslide = function(grid, steps) {
    return Array2D.lpan(grid, steps);
  };

  // Slide the grid left, the number of steps.
  Array2D.lslide = function(grid, steps) {
    return Array2D.rpan(grid, steps);
  };

  // Slide the grid down, the number of steps.
  Array2D.dslide = function(grid, steps) {
    return Array2D.upan(grid, steps);
  };

  // Slide the grid up, the number of steps.
  Array2D.uslide = function(grid, steps) {
    return Array2D.dpan(grid, steps);
  };

  // Return a new grid with the elements transposed (flipped about
  // their main diagonal).
  Array2D.transpose = function(grid) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        if (!out[j]) out[j] = [];

        out[j][i] = row[j];
      }
    }

    return out;
  };

  // Return a new grid with the elements transposed about their
  // *secondary* diagonal.
  Array2D.antitranspose = function(grid) {
    var rotated = Array2D.rrotate(grid);

    return Array2D.vflip(rotated);
  };

  // Fill the entire grid with a value.
  Array2D.fill = function(grid, value) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];
      out[i] = [];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        out[i][j] = value;
      }
    }

    return out;
  };

  // Fill an area within the grid with a value.
  Array2D.fillArea = function(grid, r, c, w, h, value) {
    var built = Array2D.build(w, h, value);

    return Array2D.paste(grid, built, r, c);
  };

  // Add padding to the given `side` of the grid, the specified
  // number of `times`.
  Array2D.pad = function(grid, side, times, value) {
    switch (side) {
      case Array2D.EDGES.TOP: return Array2D.upad(grid, times, value);
      case Array2D.EDGES.BOTTOM: return Array2D.dpad(grid, times, value);
      case Array2D.EDGES.LEFT: return Array2D.lpad(grid, times, value);
      case Array2D.EDGES.RIGHT: return Array2D.rpad(grid, times, value);
      default:
        throw("Array2D.js: Invalid side provided for `pad`");
    }
  };

  // Add padding to the top of the grid.
  Array2D.upad = function(grid, times, value) {
    var out = [];

    var d = Array2D.dimensions(grid);
    var w = d[0];
    var h = d[1];

    for (var i = -times; i < h; i++) {
      var r = i + times;
      out[r] = [];

      for (var j = 0; j < w; j++) {
        // We're in the original grid.
        if (i > -1) {
          out[r][j] = grid[i][j];
        }
        // We're in the 'padding' zone.
        else {
          if (!isUndefined(value)) {
            out[r][j] = value;
          }
          else {
            out[r][j] = null;
          }
        }
      }
    }

    return out;
  };

  // Add padding to the bottom of the grid.
  Array2D.dpad = function(grid, times, value) {
    var out = [];

    var d = Array2D.dimensions(grid);
    var w = d[0];
    var h = d[1];

    for (var i = 0; i < h + times; i++) {
      out[i] = [];

      for (var j = 0; j < w; j++) {
        // We're in the original grid.
        if (i < h) {
          out[i][j] = grid[i][j];
        }
        // We're in the 'padding' zone.
        else {
          if (!isUndefined(value)) {
            out[i][j] = value;
          }
          else {
            out[i][j] = null;
          }
        }
      }
    }

    return out;
  };

  // Add padding to the grid's left side
  Array2D.lpad = function(grid, times, value) {
    var out = [];

    var d = Array2D.dimensions(grid);
    var w = d[0];
    var h = d[1];

    for (var i = 0; i < h; i++) {
      out[i] = [];

      for (var j = -times; j < w; j++) {
        var c = j + times;

        // We're in the original grid.
        if (j > -1) {
          out[i][c] = grid[i][j];
        }
        // We're in the 'padding' zone.
        else {
          if (!isUndefined(value)) {
            out[i][c] = value;
          }
          else {
            out[i][c] = null;
          }
        }
      }
    }

    return out;
  };

  // Add padding to the grid's right side
  Array2D.rpad = function(grid, times, value) {
    var out = [];

    var d = Array2D.dimensions(grid);
    var w = d[0];
    var h = d[1];

    for (var i = 0; i < h; i++) {
      out[i] = [];

      for (var j = 0; j < w + times; j++) {
        // We're in the original grid.
        if (j < w) {
          out[i][j] = grid[i][j];
        }
        // We're in the 'padding' zone.
        else {
          if (!isUndefined(value)) {
            out[i][j] = value;
          }
          else {
            out[i][j] = null;
          }
        }
      }
    }

    return out;
  };

  // Trim rows/columns off the specified side of the grid.
  Array2D.trim = function(grid, side, num) {
    switch (side) {
      case Array2D.EDGES.TOP: return Array2D.utrim(grid, num);
      case Array2D.EDGES.BOTTOM: return Array2D.dtrim(grid, num);
      case Array2D.EDGES.LEFT: return Array2D.ltrim(grid, num);
      case Array2D.EDGES.RIGHT: return Array2D.rtrim(grid, num);
      default:
        throw("Array2D.js: Invalid edge provided for `trim`");
    }
  };

  // Trim rows off the top of the grid.
  Array2D.utrim = function(grid, num) {
    var out = [];

    num || (num = 1);

    for (var i = num, l = grid.length; i < l; i++) {
      out[i - num] = cloneArray(grid[i]);
    }

    return out;
  };

  // Trim rows off the bottom of the grid.
  Array2D.dtrim = function(grid, num) {
    var out = [];

    num || (num = 1);

    for (var i = 0, l = grid.length - num; i < l; i++) {
      out[i] = cloneArray(grid[i]);
    }

    return out;
  };

  // Trim columns off the left side of the grid.
  Array2D.ltrim = function(grid, num) {
    var out = [];

    num || (num = 1);

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = num, l2 = row.length; j < l2; j++) {
        out[i][j - num] = row[j];
      }
    }

    return out;
  };

  // Trim columns off the right side of the grid.
  Array2D.rtrim = function(grid, num) {
    var out = [];

    num || (num = 1);

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = 0, l2 = row.length - num; j < l2; j++) {
        out[i][j] = row[j];
      }
    }

    return out;
  };

  // Stitch the second grid to the given edge of the first.
  Array2D.stitch = function(grid1, grid2, edge) {
    switch (edge) {
      case Array2D.EDGES.TOP: return Array2D.ustitch(grid1, grid2);
      case Array2D.EDGES.BOTTOM: return Array2D.dstitch(grid1, grid2);
      case Array2D.EDGES.LEFT: return Array2D.lstitch(grid1, grid2);
      case Array2D.EDGES.RIGHT: return Array2D.rstitch(grid1, grid2);
      default:
        throw("Array2D.js: Invalid edge provided for `stitch`");
    }
  };

  // Stitch the second grid to the top of the first.
  Array2D.ustitch = function(grid1, grid2) {
    var h = Array2D.dimensions(grid2)[1];

    return Array2D.glue(grid1, grid2, -h, 0);
  };

  // Stitch the second grid to the bottom of the first.
  Array2D.dstitch = function(grid1, grid2) {
    var h = Array2D.dimensions(grid1)[1];

    return Array2D.glue(grid1, grid2, h, 0);
  };

  // Stitch the second grid to the left side of the first.
  Array2D.lstitch = function(grid1, grid2) {
    var w = Array2D.dimensions(grid2)[0];

    return Array2D.glue(grid1, grid2, 0, -w);
  };

  // Sticth the second grid to the right side of the first.
  Array2D.rstitch = function(grid1, grid2) {
    var w = Array2D.dimensions(grid1)[0];

    return Array2D.glue(grid1, grid2, 0, w);
  };

  // Paste the contents of the second grid onto the first.
  Array2D.paste = function(grid1, grid2, sr, sc) {
    var out = [];

    for (var i = 0, l1 = grid1.length; i < l1; i++) {
      out[i] = [];

      var rlen = grid1[i].length;
      var tr = i - sr;

      for (var j = 0; j < rlen; j++) {
        var tc = j - sc;

        if (isArray(grid2[tr]) &&
            !isUndefined(grid2[tr][tc]) &&
            i >= sr &&
            j >= sc &&
            tr < l1 &&
            tc < rlen) {

          out[i][j] = grid2[tr][tc];
        }
        else {
          out[i][j] = grid1[i][j];
        }
      }
    }

    return out;
  };

  // Paste the contents of the second grid onto the first,
  // but allow for overlap, and pad any extra cells with `null`
  // so the resulting grid is rectangular.
  Array2D.glue = function(grid1, grid2, r, c) {
    var d1 = Array2D.dimensions(grid1);
    var d2 = Array2D.dimensions(grid2);

    var mw = (d1[0] > d2[0]) ? d1[0] : d2[0]; // Greater width
    var mh = (d1[1] > d2[1]) ? d1[1] : d2[1]; // Greater height

    var w = Math.abs(c) + mw; // Width of new grid
    var h = Math.abs(r) + mh; // Height of new grid

    var n = Array2D.build(w, h); // A blank array

    var r1 = (r < 0) ? -r : 0;
    var c1 = (c < 0) ? -c : 0;
    var o = Array2D.paste(n, grid1, r1, c1);

    var r2 = (r > 0) ? r : 0;
    var c2 = (c > 0) ? c : 0;
    var p = Array2D.paste(o, grid2, r2, c2);

    return p;
  };

  // Shuffle (randomize) the grid, preserving the dimensions.
  Array2D.shuffle = function(grid) {
    // Ensure the row-lengths are preserved
    var rowLens = [];
    for (var i = 0, l = grid.length; i < l; i++) {
      rowLens.push(grid[i].length);
    }

    // Fisher-Yates shuffle
    var shuffled = Array2D.flatten(grid);
    for (var i = shuffled.length - 1; i > 0; i--) {
      var j = Math.floor(Math.random() * (i + 1));
      var t = shuffled[i];
      shuffled[i] = shuffled[j];
      shuffled[j] = t;
    }

    // Push the shuffled elements into a new grid
    var out = []
    for (var i = 0, l = rowLens.length; i < l; i++) {
      var row = [];
      var rowLen = rowLens[i];
      while (rowLen--) {
        row.push(shuffled.pop());
      }
      out.push(row);
    }

    return out;
  };

  // Return a tidied-up clone of the grid, that is, a rectangular
  // grid with no `undefined` cells.
  Array2D.tidy = function(grid) {
    var out = [];

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    for (var i = 0, l1 = width; i < l1; i++) {
      out[i] = [];

      for (var j = 0, l2 = height; j < l2; j++) {
        var previous = Array2D.get(grid, i, j);

        if (isUndefined(previous)) {
          out[i][j] = null;
        }
        else {
          out[i][j] = previous;
        }
      }
    }

    return out;
  };

  // Construction / casting
  // ======================

  // Return a clone of the grid.
  Array2D.clone = function(grid) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        out[i][j] = cell;
      }
    }

    return out;
  };

  // Initialize a new grid of the given dimensions (w,h)
  Array2D.build = function(w, h, value) {
    var out = [];

    if (isUndefined(value)) {
      value = null;
    }

    for (var i = 0, l1 = h; i < l1; i++) {
      out[i] = [];

      for (var j = 0, l2 = w; j < l2; j++) {
        out[i][j] = value;
      }
    }

    return out;
  };

  // Initialize a new grid of the given dimensions (w,h),
  // using the passed function to initialize each cell.
  Array2D.buildWith = function(w, h, fn) {
    var out = [];

    for (var i = 0, l1 = h; i < l1; i++) {
      out[i] = [];

      for (var j = 0, l2 = w; j < l2; j++) {
        if (fn) {
          out[i][j] = fn(i, j, out);
        }
        else {
          out[i][j] = null;
        }
      }
    }

    return out;
  };

  // Serialize the grid to a string.
  Array2D.serialize = function(grid) {
    return JSON.stringify(grid);
  };

  // Convert all the cells of the grid to `null`.
  Array2D.nullify = function(grid) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        if (isExistent(cell)) {
          out[i][j] = null;
        }
      }
    }

    return out;
  };

  // Return a new grid with the cells converted to integers,
  // using `parseInt`.
  Array2D.integerize = function(grid) {
    var out = []

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        out[i][j] = parseInt(cell);
      }
    }

    return out;
  };

  // Return a new grid with the cells converted to strings, using
  // the `String` constructor.
  Array2D.stringize = function(grid) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        out[i][j] = String(cell);
      }
    }

    return out;
  };

  // Inspection / comparison / analysis
  // ==================================

  // Determine whether the passed object is a grid (an array of arrays).
  Array2D.check = function(o) {
    if (!isArray(o)) return false;
    if (!isArray(o[0])) return false;
    return true;
  };

  // Determine whether the grid is ragged (has rows of
  // differing lengths).
  Array2D.ragged = function(grid) {
    var widest = Array2D.widest(grid);
    var thinnest = Array2D.thinnest(grid);

    return widest.length !== thinnest.length;
  };

  // Determine whether the grid is rectangular (all its rows
  // have the same length).
  Array2D.rectangular = function(grid) {
    return !Array2D.ragged(grid);
  };

  // Return true if the grid has no cells.
  Array2D.empty = function(grid) {
    if (grid.length < 1) return true;
    if (grid.length === 1 && grid[0].length < 1) return true;
    return false;
  };

  // Return true if all of the grid's cells are `null` or `undefined`.
  Array2D.blank = function(grid) {
    var blank = true;

    var empty = Array2D.empty(grid);
    if (empty) return true;

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        if (!isBlank(cell)) {
          blank = false;
        }
      }
    }

    return blank;
  };

  // Determine whether the grid has only one cell.
  Array2D.singular = function(grid) {
    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    return width === 1 && height === 1;
  };

  // Determine whether the grid has more than one cell.
  Array2D.multitudinous = function(grid) {
    return !Array2D.singular(grid);
  };

  // Determine whether the grid has any `null` or `undefined` cells.
  Array2D.sparse = function(grid) {
    var sparse = false;

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        if (isBlank(cell)) {
          sparse = true;
        }
      }
    }

    return sparse;
  };

  // Determine if the grid is dense, i.e., without any `null` or
  // `undefined` cells.
  Array2D.dense = function(grid) {
    return !Array2D.sparse(grid);
  };

  // Determine whether both grids' cells are all strictly equal.
  Array2D.same = function(grid1, grid2) {
    var w1 = Array2D.width(grid1);
    var h1 = Array2D.height(grid1);

    var w2 = Array2D.width(grid2);
    var h2 = Array2D.height(grid2);

    if (w1 !== w2) return false;
    if (h1 !== h2) return false;

    for (var i = 0; i < w1; i++) {
      for (var j = 0; j < w2; j++) {
        if (grid1[i][j] !== grid2[i][j]) {
          return false;
        }
      }
    }

    return true;
  };

  // Determine whether both grids' cells are not strictly equal.
  Array2D.different = function(grid1, grid2) {
    return !Array2D.same(grid1, grid2);
  };

  // Return the coordinates of cells that are different between
  // the two grids.
  Array2D.diff = function(grid1, grid2) {
    var diffs = [];

    var d1 = Array2D.dimensions(grid1);
    var d2 = Array2D.dimensions(grid2);

    var w = (d1[0] > d2[0]) ? d1[0] : d2[0];
    var h = (d1[1] > d2[1]) ? d2[1] : d2[1];

    for (var i = 0; i < h; i++) {
      var row1 = grid1[i];
      var row2 = grid2[i];
      var row1isArray = isArray(row1);
      var row2isArray = isArray(row2);

      for (var j = 0; j < w; j++) {
        if (row1isArray && row2isArray) {
          var cell1 = row1[j];
          var cell2 = row2[j];

          if (cell1 !== cell2) {
            diffs.push([i, j]);
          }
        }
        else {
          diffs.push([i, j]);
        }
      }
    }

    return diffs;
  };

  // Return true if the grid contains the value.
  Array2D.contains = function(grid, value) {
    var contains = false;

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        if (cell === value) {
          contains = true;
        }
      }
    }

    return contains;
  };

  // Detect whether the first grid contains the second grid.
  Array2D.includes = function(grid1, grid2) {
    // Dimensions cache.
    var d1 = Array2D.dimensions(grid1);
    var d2 = Array2D.dimensions(grid2);
    var w1 = d1[0];
    var h1 = d1[1];
    var w2 = d2[0];
    var h2 = d2[1];

    // Size conditions under which we don't bother checking.
    if (w2 < 1) return false;
    if (h2 < 1) return false;
    if (w2 > w1) return false;
    if (h2 > h1) return false;

    var first = grid2[0][0];
    var starters = [];

    // Start by checking each cell of the outer grid.
    for (var i = 0; i < grid1.length; i++) {
      for (var j = 0; j < grid1[i].length; j++) {
        var cell1 = grid1[i][j];

        // If the first cell is a match, proceed.
        if (cell1 === first) {
          starters.push([i, j]);
        }
      }
    }

    // If no initial matches, no point checking the rest.
    var startersLen = starters.length;
    if (startersLen < 1) return false;

    // Check whether the comparee is present in the grid.
    for (var x = 0; x < startersLen; x++) {
      // Starting coordinates in the *outer* grid.
      var sr = starters[x][0];
      var sc = starters[x][1];

      // Assume a match for this starting point, then invalidate.
      var match = true;

      // Loop over the inner grid, comparing each cell.
      for (var i = 0; i < grid2.length; i++) {
        var row1 = grid1[i + sr];
        var row2 = grid2[i];

        // Fail early if we've already overstepped the bounds.
        if (!isArray(row1)) break;
        if (!isArray(row2)) break;

        for (var j = 0; j < grid2[i].length; j++) {
          var cell1 = row1[j + sc];
          var cell2 = row2[j];

          if (cell1 !== cell2) match = false;
        }
      }

      // Return as soon as we find our first match.
      if (match === true) {
        return true;
      }
    }

    // If we got this far, we never found a match.
    return false;
  };

  // Detect whether the grid is symmetrical, when reflected
  // around the given axis.
  Array2D.symmetrical = function(grid, axis) {
    switch (axis) {
      case Array2D.AXES.Y: return Array2D.hsymmetrical(grid);
      case Array2D.AXES.X: return Array2D.vsymmetrical(grid);
      default:
        throw("Array2D.js: Invalid axis given for `symmetrical`");
    }
  };

  // Determine whether the grid is horizontally symmetrical
  Array2D.hsymmetrical = function(grid) {
    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];
        var opposite = row[l2 - 1 - j];

        if (cell !== opposite) {
          return false;
        }
      }
    }

    return true;
  };

  // Determine whether the grid is vertically symmetrical
  Array2D.vsymmetrical = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.hsymmetrical(transposed);
  };

  // Iteration / collection
  // ======================

  // Iterate over each cell in the grid, passing the cell to the
  // iterator function.
  Array2D.eachCell = function(grid, iterator) {
    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        iterator(cell, i, j, grid);
      }
    }
  };

  // Iterate over every nth cell in the grid.
  Array2D.nthCell = function(grid, n, s, iterator) {
    var x = 0;

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        var isPastStart = x >= s;
        var isAtNth = ((x - s) % n) === 0

        if (isPastStart && isAtNth) {
          iterator(cell, i, j, grid);
        }

        x += 1;
      }
    }
  };

  // Iterate over each row in the grid, passing the row-array to
  // the iterator function.
  Array2D.eachRow = function(grid, iterator) {
    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      iterator(cloneArray(row), i, grid);
    }
  };

  // Iterate over each column in the grid, passing the column-array
  // to the iterator function.
  Array2D.eachColumn = function(grid, iterator) {
    var transposed = Array2D.transpose(grid);

    for (var i = 0, l1 = transposed.length; i < l1; i++) {
      var row = transposed[i];

      iterator(cloneArray(row), i, grid);
    }
  };

  // Iterate over every cell in the given area.
  Array2D.forArea = function(grid, r, c, w, h, iterator) {
    var cropped = Array2D.crop(grid, r, c, w, h);

    for (var i = 0, l1 = cropped.length; i < l1; i++) {
      var row = cropped[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        iterator(cell, i, j, grid);
      }
    }
  };

  // Iterate over every cell in the given row.
  Array2D.forRow = function(grid, r, iterator) {
    var row = Array2D.row(grid, r);

    for (var i = 0, l = row.length; i < l; i++) {
      iterator(row[i], r, i, grid);
    }
  };

  // Iterate over every cell in the given column.
  Array2D.forColumn = function(grid, c, iterator) {
    var column = Array2D.column(grid, c);

    for (var i = 0, l = column.length; i < l; i++) {
      iterator(column[i], i, c, grid);
    }
  };

  // Flatten the grid to an array in row-major order.
  Array2D.flatten = function(grid) {
    var flattened = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        flattened.push(row[j]);
      }
    }

    return flattened;
  };

  // Same as flatten, but in column-major order.
  Array2D.squash = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.flatten(transposed);
  };

  // Remap the grid to a new grid by returning a new value for each cell.
  Array2D.map = function(grid, iterator) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      out[i] = [];
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        var result;
        if (iterator) {
          result = iterator(cell, i, j, grid);
        }
        else {
          result = cell;
        }

        out[i][j] = result;
      }
    }

    return out;
  };

  // Reduce the grid to a flat array by reducing each
  // row to a single value.
  Array2D.reduce = function(grid, iterator) {
    var reduced = [];

    for (var i = 0, l = grid.length; i < l; i++) {
      reduced[i] = iterator(grid[i], i, grid);
    }

    return reduced;
  };

  // Similar to reduce, but column-by-column.
  Array2D.boildown = function(grid, iterator) {
    var transposed = Array2D.transpose(grid);

    return Array2D.reduce(transposed, iterator);
  };

  // Rows / columns
  // ==============

  // Return the row of the given row-coordinate.
  Array2D.row = function(grid, r) {
    return cloneArray(grid[r]);
  };

  // Return the column of the given column-coordinate.
  Array2D.column = function(grid, c) {
    var transposed = Array2D.transpose(grid);

    return Array2D.row(transposed, c);
  };

  // Return the top row of the grid.
  Array2D.top = function(grid) {
    return cloneArray(grid[0]);
  };

  // Return the bottom row of the grid.
  Array2D.bottom = function(grid) {
    return cloneArray(grid[grid.length - 1]);
  };

  // Return the left column of the grid.
  Array2D.left = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.top(grid);
  };

  // Return the right column of the grid.
  Array2D.right = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.bottom(grid);
  };

  // Return the longest row of the grid.
  Array2D.widest = function(grid) {
    var widest = grid[0];

    for (var i = 0, l = grid.length; i < l; i++) {
      var row = grid[i];

      if (row.length > widest.length) {
        widest = row;
      }
    }

    return cloneArray(widest);
  };

  // Return the shortest row of the grid.
  Array2D.thinnest = function(grid) {
    var thinnest = grid[0];

    for (var i = 0, l = grid.length; i < l; i++) {
      var row = grid[i];

      if (row.length < thinnest.length) {
        thinnest = row;
      }
    }

    return cloneArray(thinnest);
  };

  // Return the tallest column of the grid.
  Array2D.tallest = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.widest(transposed);
  };

  // Return the shortest column of the grid.
  Array2D.shortest = function(grid) {
    var transposed = Array2D.transpose(grid);

    return Array2D.thinnest(transposed);
  };

  // Set a row to the given array.
  Array2D.setRow = function(grid, r, array) {
    var out = [];

    for (var i = 0, l = grid.length; i < l; i++) {
      if (i === r) {
        out[i] = cloneArray(array);
      }
      else {
        out[i] = cloneArray(grid[i]);
      }
    }

    return out;
  };

  // Set a column to the given array.
  Array2D.setColumn = function(grid, c, array) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];
      out[i] = [];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        if (j === c) {
          out[i][j] = array[j];
        }
        else {
          out[i][j] = row[j];
        }
      }
    }

    return out;
  };

  // Fill a row with the given value.
  Array2D.fillRow = function(grid, r, value) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];
      out[i] = [];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        if (i === r) {
          out[i][j] = value;
        }
        else {
          out[i][j] = row[j];
        }
      }
    }

    return out;
  };

  // Fill a column with the given value.
  Array2D.fillColumn = function(grid, c, value) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];
      out[i] = [];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        if (j === c) {
          out[i][j] = value;
        }
        else {
          out[i][j] = row[j];
        }
      }
    }

    return out;
  };

  // Insert a row (array).
  Array2D.spliceRow = function(grid, r, array) {
    var out = [];

    for (var i = 0, l = grid.length; i < l; i++) {
      if (i === r) {
        out.push(array);
      }

      out.push(grid[i]);
    }

    return out;
  };

  // Insert a column (array).
  Array2D.spliceColumn = function(grid, c, array) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];
      out[i] = [];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        if (j === c) {
          out[i].push(array[j]);
        }

        out[i].push(row[j]);
      }
    }

    return out;
  };

  // Delete a row.
  Array2D.deleteRow = function(grid, r) {
    var out = [];

    for (var i = 0, l = grid.length; i < l; i++) {
      if (i !== r) {
        out.push(grid[i]);
      }
    }

    return out;
  };

  // Delete a column.
  Array2D.deleteColumn = function(grid, c) {
    var out = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];
      out[i] = [];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        if (j !== c) {
          out[i].push(row[j]);
        }
      }
    }

    return out;
  };

  // Cells
  // =====

  // Determine whether the coordinate cell exists (is not
  // `undefined`).
  Array2D.exists = function(grid, r, c) {
    return !isUndefined(Array2D.get(grid, r, c));
  };

  // Determine whether the coordinate is occupied (not `null` or 
  // `undefined`).
  Array2D.occupied = function(grid, r, c) {
    return isPresent(Array2D.get(grid, r, c));
  };

  // Return T/F whether the given cell is within the grid's area.
  Array2D.inBounds = function(grid, r, c) {
    if (r < 0 || c < 0) return false;
    if (!isArray(grid[r])) return false;
    if (c > grid[r].length - 1) return false;
    return true;
  };

  // Copy one cell value over another coordinate.
  Array2D.copy = function(grid, r1, c1, r2, c2) {
    var cell = Array2D.get(grid, r1, c1);
    return Array2D.set(grid, r2, c2, cell);
  };

  // Move one cell value to another coordinate, nullifying the first.
  Array2D.move = function(grid, r1, c1, r2, c2) {
    var cell = Array2D.get(grid, r1, c1);
    var copied = Array2D.set(grid, r2, c2, cell);
    return Array2D.set(copied, r1, c1, null);
  };

  // Swap the contents of two cells.
  Array2D.swap = function(grid, r1, c1, r2, c2) {
    var cell1 = Array2D.get(grid, r1, c1);
    var cell2 = Array2D.get(grid, r2, c2);
    var first = Array2D.set(grid, r2, c2, cell1);
    return Array2D.set(first, r1, c1, cell2);
  };

  // Location / relationships
  // ========================

  // Determine whether the coordinate is on an edge.
  Array2D.edge = function(grid, r, c) {
    if (r === 0) return true;
    if (c === 0) return true;

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    if (r === height - 1) return true;
    if (c === width - 1) return true;

    return false;
  };

  // Return the list of edges that the coordinate is on.
  Array2D.edges = function(grid, r, c) {
    var edges = [];

    if (r === 0) edges.push(Array2D.EDGES.TOP);
    if (c === 0) edges.push(Array2D.EDGES.LEFT);

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    if (r === height - 1) edges.push(Array2D.EDGES.BOTTOM);
    if (c === width - 1) edges.push(Array2D.EDGES.RIGHT);

    return edges;
  };

  // Determine whether the coordinate is on a corner.
  Array2D.corner = function(grid, r, c) {
    if (r === 0 && c === 0) return true;

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    if (r === 0 && c === width - 1) return true;
    if (r === height - 1 && c === width - 1) return true;
    if (r === height - 1 && c === 0) return true;

    return false;
  };

  // Return the list of corners that the coordinate is on.
  Array2D.corners = function(grid, r, c) {
    var corners = [];

    if (r === 0 && c === 0) corners.push(Array2D.CORNERS.TOP_LEFT);

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    if (r === 0 && c === width - 1) corners.push(Array2D.CORNERS.TOP_RIGHT);
    if (r === height - 1 && c === width - 1) corners.push(Array2D.CORNERS.BOTTOM_RIGHT);
    if (r === height - 1 && c === 0) corners.push(Array2D.CORNERS.BOTTOM_LEFT);

    return corners;
  };

  // Determine whether the given cell is on a grid boundary, i.e.,
  // the first/last cell in its row/column. If you need to detect
  // edge-ness of a cell in a ragged grid, prefer this function.
  Array2D.boundary = function(grid, r, c) {
    if (r === 0) return true;
    if (c === 0) return true;

    var row = Array2D.row(grid, r);
    var right = row.length - 1;
    if (c === right) return true;

    var col = Array2D.column(grid, c);
    var bottom = col.length - 1;
    if (r === bottom) return true;

    return false;
  };

  // Return a list of boundaries that the cell is on. If you need to
  // detect edges of a cell in a ragged grid, prefer this function.
  Array2D.boundaries = function(grid, r, c) {
    var boundaries = [];

    if (r === 0) boundaries.push(Array2D.BOUNDARIES.UPPER);
    if (c === 0) boundaries.push(Array2D.BOUNDARIES.LEFT);

    var row = Array2D.row(grid, r);
    var right = row.length - 1;
    if (c === right) boundaries.push(Array2D.BOUNDARIES.RIGHT);

    var col = Array2D.column(grid, c);
    var bottom = col.length - 1;
    if (r === bottom) boundaries.push(Array2D.BOUNDARIES.LOWER);

    return boundaries;
  };

  // Detect whether the cell is on a 'crook', i.e. the first or last
  // cell in a row *and* the first or last cell in a column. If you need to
  // detect corner-ness of a cell in a ragged grid, prefer this function.
  Array2D.crook = function(grid, r, c) {
    if (r === 0 && c === 0) return true;

    var row = Array2D.row(grid, r);
    var right = row.length - 1;
    var col = Array2D.column(grid, c);
    var bottom = col.length - 1;

    if (r === 0 && c === bottom) return true;
    if (r === right && c === 0) return true;
    if (r === right && c === bottom) return true;

    return false;
  };

  // Return a list of 'crooks' that the cell is on. If you need to
  // detect corners of a cell in a ragged grid, prefer this function.
  Array2D.crooks = function(grid, r, c) {
    var crooks = [];

    var row = Array2D.row(grid, r);
    var right = row.length - 1;
    var col = Array2D.column(grid, c);
    var bottom = col.length - 1;

    if (r === 0 && c === 0) crooks.push(Array2D.CROOKS.UPPER_LEFT);
    if (r === 0 && c === bottom) crooks.push(Array2D.CROOKS.LOWER_LEFt);
    if (r === right && c === 0) crooks.push(Array2D.CROOKS.UPPER_RIGHT);
    if (r === right && c === bottom) crooks.push(Array2D.CROOKS.LOWER_RIGHT);

    return crooks;
  };

  // Determine whether the given coordinate is at the grid's center.
  Array2D.center = function(grid, r, c) {
    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    if (width % 2 === 0) return false;
    if (height % 2 === 0) return false;

    if (Math.floor(height / 2) !== r) return false;
    if (Math.floor(width / 2) !== c) return false;

    return true;
  };

  // Determine whether the given coordinate is interior (not on an
  // edge or a corner).
  Array2D.interior = function(grid, r, c) {
    if (r === 0) return false;
    if (c === 0) return false;

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    if (width < 3) return false; // 2xH grids have no interior
    if (height < 3) return false; // Wx2 grids have no interior

    if (r >= height - 1) return false;
    if (c >= width - 1) return false;

    return true;
  };

  // Return a list of all the quadrants the given cell is in.
  Array2D.quadrants = function(grid, r, c) {
    var quadrants = [];

    var width = Array2D.width(grid);
    var height = Array2D.height(grid);

    var midcolumn = Math.floor(width / 2);
    var midrow = Math.floor(height / 2);

    if (r <= midrow && c > midcolumn) quadrants.push(Array2D.QUADRANTS.I);
    if (r <= midrow && c <= midcolumn) quadrants.push(Array2D.QUADRANTS.II);
    if (r > midrow && c <= midcolumn) quadrants.push(Array2D.QUADRANTS.III);
    if (r > midrow && c > midcolumn) quadrants.push(Array2D.QUADRANTS.IV);

    return quadrants;
  };

  // Return an array of all orthogonal cells to the coordinate.
  Array2D.orthogonals = function(grid, r, c) {
    var orthogonals = [];

    orthogonals[0] = Array2D.get(grid, r - 1, c); // North
    orthogonals[1] = Array2D.get(grid, r, c - 1); // West
    orthogonals[2] = Array2D.get(grid, r, c + 1); // East
    orthogonals[3] = Array2D.get(grid, r + 1, c); // South

    return orthogonals;
  };

  // Return an array of all diagonal cells to the coordinate.
  Array2D.diagonals = function(grid, r, c) {
    var diagonals = [];

    diagonals[0] = Array2D.get(grid, r - 1, c - 1); // Northwest
    diagonals[1] = Array2D.get(grid, r - 1, c + 1); // Northeast
    diagonals[2] = Array2D.get(grid, r + 1, c - 1); // Southwest
    diagonals[3] = Array2D.get(grid, r + 1, c + 1); // Southeast

    return diagonals;
  };

  // Return an array of all orthogonal and diagonal neighbors of the cell.
  Array2D.neighbors = function(grid, r, c) {
    var orthogonals = Array2D.orthogonals(grid, r, c);
    var diagonals = Array2D.diagonals(grid, r, c);

    var neighbors = [];
    neighbors[0] = diagonals[0]; // Northwest
    neighbors[1] = orthogonals[0]; // North
    neighbors[2] = diagonals[1]; // Northeast
    neighbors[3] = orthogonals[1]; // West
    neighbors[4] = orthogonals[2]; // East
    neighbors[5] = diagonals[2]; // Southwest
    neighbors[6] = orthogonals[3]; // South
    neighbors[7] = diagonals[3]; // Southeast

    return neighbors;
  };

  // Return a subgrid representing all cells in the neighborhood of
  // the given row-column coordinate.
  Array2D.neighborhood = function(grid, r, c) {
    var cell = Array2D.get(grid, r, c);
    var neighbors = Array2D.neighbors(grid, r, c);

    return [
      [neighbors[0], neighbors[1], neighbors[2]],
      [neighbors[3], cell, neighbors[4]],
      [neighbors[5], neighbors[6], neighbors[7]]
    ];
  };

  // Return the Euclidean distance bewteen the two cell coordinates.
  Array2D.euclidean = function(grid, r1, c1, r2, c2) {
    return Math.sqrt(Math.pow(r2 - r1, 2) + Math.pow(c2 - c1, 2));
  };

  // Return the Chebyshev distance bewteen the two cell coordinates.
  Array2D.chebyshev = function(grid, r1, c1, r2, c2) {
    var v = Math.abs(r2 - r1);
    var h = Math.abs(c2 - c1);
    return (v > h) ? v : h;
  };

  // Return the Manhattan distance bewteen the two cell coordinates.
  Array2D.manhattan = function(grid, r1, c1, r2, c2) {
    return Math.abs(r2 - r1) + Math.abs(c2 - c1);
  };

  // Coordinates
  // ===========

  // Return the coordinates of every cell that the `finder` function
  // returns truthy.
  Array2D.find = function(grid, finder) {
    var found = [];

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        if (finder(cell, i, j, grid)) {
          found.push([i, j]);
        }
      }
    }

    return found;
  };

  // Return groups of coordinates, where groups are coordinates of
  // all _orthogonally_ adjacent cells that return truthy for the `finder`
  // function.
  Array2D.contiguous = function(grid, finder, countDiagonals) {
    var contiguous = [];
    var checked = [];

    // Dimensions are used for bounds-checking below.
    var dimensions = Array2D.dimensions(grid);
    var w = dimensions[0];
    var h = dimensions[1];

    // Iterate over the whole grid, checking for contiguous groups.
    for (var i = 0; i < h; i++) {
      for (var j = 0; j < w; j++) {
        _findContiguous(grid[i][j], i, j, grid, w, h, contiguous, checked, finder, countDiagonals);
      }
    }

    return contiguous;
  };

  // PRIVATE - RECURSIVE check for contiguous cells
  function _findContiguous(cell, r, c, grid, w, h, contiguous, checked, finder, countDiagonals, group) {
    if (!_hasChecked(checked, r, c)) {
      checked[r] || (checked[r] = []);
      checked[r][c] = true; // Avoid repeat checks

      // No need to check out-of-bounds cells
      if (c > -1 && c < w && r > -1 && r < h) {

        // A truthy return value is a match
        if (finder(cell, r, c, grid)) {

          // Spawn a new group
          if (!group) {
            group = [];

            // Push group into the collection
            contiguous.push(group);
          }

          // Add cell's coordinate to the group
          group.push([r, c]);

          // Direction cache
          var up = r - 1;
          var down = r + 1;
          var left = c - 1;
          var right = c + 1;

          // Orthogonal neighbors
          if (up > -1 && up < h)       _findContiguous(grid[up][c], up, c, grid, w, h, contiguous, checked, finder, countDiagonals, group);
          if (down > -1 && down < h)   _findContiguous(grid[down][c], down, c, grid, w, h, contiguous, checked, finder, countDiagonals, group);
          if (left > -1 && left < w)   _findContiguous(grid[r][left], r, left, grid, w, h, contiguous, checked, finder, countDiagonals, group);
          if (right > -1 && right < w) _findContiguous(grid[r][right], r, right, grid, w, h, contiguous, checked, finder, countDiagonals, group);

          // Diagonal neighbors (if desired)
          if (countDiagonals) {
            if (up > -1 && up < h && left > -1 && left < w)       _findContiguous(grid[up][left], up, left, grid, w, h, contiguous, checked, finder, countDiagonals, group);
            if (up > -1 && up < h && right > -1 && right < w)     _findContiguous(grid[up][right], up, right, grid, w, h, contiguous, checked, finder, countDiagonals, group);
            if (down > -1 && down < h && left > -1 && left < w)   _findContiguous(grid[down][left], down, left, grid, w, h, contiguous, checked, finder, countDiagonals, group);
            if (down > -1 && down < h && right > -1 && right < w) _findContiguous(grid[down][right], down, right, grid, w, h, contiguous, checked, finder, countDiagonals, group);
          }

        } else { /* The cell did not match; skip. */ }
      } else { /* The cell was out-of-bounds; skip. */ }
    } else { /* The cell was already checked; skip. */ }
  }

  // Return groups of coordinates, where groups are coordinates of
  // all adjacent cells that return truthy for the `finder`
  // function.
  Array2D.touching = function(grid, finder) {
    return Array2D.contiguous(grid, finder, true);
  };

  // PRIVATE - T/F if a coordinate has been checked.
  function _hasChecked(checked, r, c) {
    return checked[r] && checked[r][c] === true;
  }

  // Return coordinates of all surrounding cells
  Array2D.surrounds = function(grid, r, c, allowOutOfBounds) {
    var surrounds = [];

    var d = Array2D.dimensions(grid);
    var w = d[0];
    var h = d[1];
    var right = w - 1;
    var bottm = h - 1;

    if ((r > 0 && c > 0)         || allowOutOfBounds) surrounds.push([r - 1, c - 1]); // nw
    if ((r > 0)                  || allowOutOfBounds) surrounds.push([r - 1, c]); // n
    if ((r > 0 && c < right)     || allowOutOfBounds) surrounds.push([r - 1, c + 1]); // ne
    if ((c > 0)                  || allowOutOfBounds) surrounds.push([r, c - 1]); // w
    if ((c < right)              || allowOutOfBounds) surrounds.push([r, c + 1]); // e
    if ((r < bottm && c > 0)     || allowOutOfBounds) surrounds.push([r + 1, c - 1]); // sw
    if ((r < bottm)              || allowOutOfBounds) surrounds.push([r + 1, c]); // s
    if ((r < bottm && c < right) || allowOutOfBounds) surrounds.push([r + 1, c + 1]); // se

    return surrounds;
  };

  // Import / export
  // ===============

  // Convert the given array (flat) into the standard grid format.
  Array2D.fromArray = function(arr, rows, columns) {
    var out = [];

    for (var i = 0; i < rows; i++) {
      out[i] = [];

      for (var j = 0; j < columns; j++) {
        out[i][j] = arr[i * columns + j];
      }
    }

    return out;
  };

  // Convert the canvas pixel data into an Array2D-formatted grid.
  Array2D.fromCanvas = function(canvas) {
    var context = canvas.getContext('2d');
    var image = context.getImageData(0, 0, canvas.width, canvas.width);

    var width = image.width;
    var height = image.height;
    var data = image.data;

    var colors = [];

    for (var i = 0, l = data.length; i < l; i += 4) {
      var r = data[i]
      var g = data[i+1];
      var b = data[i+2];
      var a = data[i+3];

      var color = [r,g,b,a];

      colors.push(color);
    }

    return Array2D.fromArray(colors, height, width);
  };

  // Paint the grid data to the given canvas, running each cell
  // through a `converter` function to produce a _rgba_ color array.
  // That is, on output, every cell needs to look something like this:
  // `[255,255,255,255]`
  Array2D.toCanvas = function(grid, canvas, converter) {
    var context = canvas.getContext('2d');
    var width = canvas.width;
    var height = canvas.height;
    var image = context.createImageData(width, height);
    var data = image.data;
    var colors;

    for (var i = 0, l1 = grid.length; i < l1; i++) {
      var row = grid[i];

      for (var j = 0, l2 = row.length; j < l2; j++) {
        var cell = row[j];

        colors = (converter) ? converter(cell, i, j, grid) : cell;

        var idx = (i * width + j) * 4;

        data[idx + 0] = colors[0];
        data[idx + 1] = colors[1];
        data[idx + 2] = colors[2];
        data[idx + 3] = colors[3];
      }
    }

    context.putImageData(image, 0, 0);
  };

}.call(this));