// FillPatternEditor JavaScript file.

// the underlying data structure is a flattened array, representing the grid of pixels;
// true/on is black, false/off is white
var gridSize = 8;  // i.e. 8x8
var numberOfPixels = gridSize * gridSize;
var pixelsArray = new Array(numberOfPixels);
var encodedPattern = new Array(gridSize);  // each row is encoded as an integer

var isSafari = false;
var isSafari4 = false;
var isMobileSafari = false;

// the following variables and event handlers allow for continuous mouse operations
var currentWritingMode;
var mouseDown = false;
document.onmousedown = function() { mouseDown = true; };
document.onmouseup = function() { mouseDown = false; };

// setup undo/redo data structures
var undoStack = new Array();
var redoStack = new Array();
var recordThePattern = true;

// a convenience function to copy an array
Array.prototype.copy = function() { return [].concat(this); };

// a convenience function to compare an array to another array
Array.prototype.equals = function(anotherArray) { return (this.toString() === anotherArray.toString()); };

// For browsers that don't support map() for Arrays.
// This prototype is provided by the Mozilla foundation and
// is distributed under the MIT license.
// http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
if (!Array.prototype.map)
{
  Array.prototype.map = function(fun /*, thisp*/)
  {
    var len = this.length;
    if (typeof fun != "function") throw new TypeError();
    var res = new Array(len);
    var thisp = arguments[1];
    for (var i = 0; i < len; i++)
    {
      if (i in this) res[i] = fun.call(thisp, this[i], i, this);
    }
    return res;
  };
}

// called when the page loads
function init()
{
  isSafari = navigator.userAgent.indexOf("Safari") > -1;
  isSafari4 = isSafari && (navigator.userAgent.indexOf("Version/4") > -1);
  initPixelGrid();
  updateEncodedPatternValue(false);
  updateEncodedPatternDisplayDiv();
  populateExamples();
  var randomIndex = Math.floor((exampleTileEncodings.length-1) * Math.random())+1;  // exclude pattern 0
  if ((randomIndex < 1) || (randomIndex >= exampleTileEncodings.length)) randomIndex = 1;  //just in case
  loadExample(exampleTileEncodings[randomIndex].encodedArray);
}

// called when page loads - special case for Mobile Safari
function initMobileSafari()
{
  isMobileSafari = true;
  init();
}

// initialises the pixel editing grid
function initPixelGrid()
{
  var gridTableBody = document.getElementById("gridTableBody");
  while (gridTableBody.hasChildNodes())
  {
     gridTableBody.removeChild(gridTableBody.lastChild);
  }
  var row = null;
  for (var i=0; i<gridSize; i++)
  {
    row = document.createElement("tr");
    for (var j=0; j<gridSize; j++)
    {
      var cell = document.createElement("td");
      cell.className = "pixelOff";
      cell.innerHTML = "";
      cell.xPos = j;
      cell.yPos = i;
      cell.pixelsArrayIndex = translateRowAndColToIndex(i, j);
      cell.onmousedown = function() {selectPixel(this);};
      cell.onmouseover = function() {selectIfMouseDown(this);};
      cell.onmouseup = function() {currentWritingMode = null;mouseDown = false;};
      pixelsArray[cell.pixelsArrayIndex] = false;
      row.appendChild(cell);
    }
    gridTableBody.appendChild(row);
  }
}

// updates the pixel editing grid following a user request
function updatePixelGrid()
{
  var gridTableBody = document.getElementById("gridTableBody");
  var rows = gridTableBody.childNodes;
  for (var i=0; i<rows.length; i++)
  {
    var cols = rows[i].childNodes;
    for (var j=0; j<cols.length; j++)
    {
      var cell = cols[j];
      cell.className = (pixelsArray[cell.pixelsArrayIndex] == true) ? "pixelOn" : "pixelOff";
    }
  }
  updateHiddenStagingArea(pixelsArray);
}

// updates the encoding for the current pattern
function updateEncodedPatternValue(savePreviousPatternFlag)
{
  var pixelRows = new Array(gridSize);
  var savedEncodedPattern = encodedPattern.copy();
  for (var i=0; i<gridSize; i++)
  {
    pixelRows[i] = pixelsArray.slice(gridSize*i, gridSize*(i+1));
  }
  for (var i=0; i<pixelRows.length; i++)
  {
    var rowEncodedValue = 0;
    for (var j=0; j<pixelRows[i].length; j++)
    {
      if (pixelRows[i][j] == true) rowEncodedValue += Math.pow(2, j);
    }
    encodedPattern[i] = rowEncodedValue;
  }
  if (savePreviousPatternFlag && recordThePattern)
  {
    addUndoItem(savedEncodedPattern);
  }
}

// returns the flat array index for the given row-column grid reference
function translateRowAndColToIndex(r, c)
{
  return r * gridSize + c;
}

// returns the row-column grid reference for the given flat array index
function translateIndexToRowAndCol(i)
{
  var r = Math.floor(i / gridSize);
  var c = i % gridSize;
  return [r, c];
}

// returns the row number corresponding to the given flat array index
function rowForIndex(i)
{
  return Math.floor(i / gridSize);
}

// returns the column number corresponding to the given flat array index
function columnForIndex(i)
{
  return i % gridSize;
}

// called when a pixel on the editing grid is clicked
function selectPixel(obj)
{
  togglePixel(obj);
  currentWritingMode = obj.className;
  updateHiddenStagingArea(pixelsArray);
  updateTiledCanvas();
  updateEncodedPatternValue(true);
  updateEncodedPatternDisplayDiv();
}

// if mouse is still down, modify underlying pixels
function selectIfMouseDown(obj)
{
  // ISSUE: goes wonky in Firefox if cells become selected and dragged
  if (mouseDown)
  {
    if (!currentWritingMode)
    {
      if (obj.className === "pixelOn")
      {
        currentWritingMode = "pixelOff";
      }
      else
      {
        currentWritingMode = "pixelOn";
      }
    }
    obj.className = currentWritingMode;
    if (currentWritingMode === "pixelOn")
    {
      pixelsArray[obj.pixelsArrayIndex] = true;
    }
    else
    {
      pixelsArray[obj.pixelsArrayIndex] = false;
    }
    updateHiddenStagingArea(pixelsArray);
    updateTiledCanvas();
    updateEncodedPatternValue(true);
    updateEncodedPatternDisplayDiv();
  }
  else
  {
    currentWritingMode = null;
  }
}

// toggles the colour of a pixel in the editing grid; on = black, off = white
// Note: updates the underlying pixel array (otherwise would need to call updatePixelGrid())
function togglePixel(obj)
{
  if (obj.className === "pixelOn")
  {
    obj.className = "pixelOff";
    pixelsArray[obj.pixelsArrayIndex] = false;
  }
  else
  {
    obj.className = "pixelOn";
    pixelsArray[obj.pixelsArrayIndex] = true;
  }
}

// redraws the (invisible) staging canvas, representing the master tile used for repetition
function updateHiddenStagingArea(pixelsArray)
{
  var stagingCanvas = document.getElementById("stagingCanvas");
  var ctx = stagingCanvas.getContext("2d");
  ctx.save();
  ctx.translate(0, 0);
  for (var i=0; i<pixelsArray.length; i++)
  {
    var pixelYPos = (isSafari && !isSafari4) ? (stagingCanvas.height - rowForIndex(i) - 1) : rowForIndex(i);  // workaround for reflection problem
    if (pixelsArray[i] == true)
    {
      ctx.fillRect(columnForIndex(i), pixelYPos, 1, 1);
    }
    else
    {
      ctx.clearRect(columnForIndex(i), pixelYPos, 1, 1);
    }
  }
}

// updates the large tiled display area
function updateTiledCanvas()
{
  tileCanvasUsingPattern(document.getElementById("tiledCanvas"));
}

// tiles the given canvas with the current pattern
function tileCanvasUsingPattern(theCanvas)
{
  var stagingCanvas = document.getElementById("stagingCanvas");
  var ctx = theCanvas.getContext("2d");
  ctx.save();
  ctx.translate(0, 0);
  ctx.clearRect(0, 0, theCanvas.width, theCanvas.height);
  ctx.fillStyle = ctx.createPattern(stagingCanvas, 'repeat');
  ctx.fillRect(0, 0, theCanvas.width, theCanvas.height);
}

// updates the encoding for the current pattern
function updateEncodedPatternDisplayDiv()
{
  var encodedPatternDisplayDiv = document.getElementById("encodedPatternDisplay");
  encodedPatternDisplayDiv.innerHTML = encodedPattern;
}

// loads the tile pattern into the editing grid
function loadEncodedPattern(encodedPatternAsString)
{
  var newPixelsArray = decodeEncodedPattern(encodedPatternAsString);
  if (newPixelsArray)
  {
    pixelsArray = newPixelsArray.copy();
    updateEncodedPatternValue(true);
    refreshViews();
  }
}

// decodes the given pattern encoding, and returns he corresponding array of pixels
function decodeEncodedPattern(encodedPatternAsString)
{
  var newEncodedPattern = encodedPatternAsString.split(",");
  var decodedPattern = new Array(gridSize);
  var newPixelsArray = new Array();
  if (newEncodedPattern.length < gridSize)
  {
    alert("insufficient data - item count = " + newEncodedPattern.length + ", expected " + gridSize);
    return null;
  }
  for (var i=0; i<newEncodedPattern.length; i++)
  {
    rowEncoding = parseInt(newEncodedPattern[i]);
    if ((rowEncoding < 0) || (rowEncoding > Math.pow(2, gridSize)-1))
    {
      alert("invalid data - item " + (i+1) + " = " + newEncodedPattern[i]);
      return null;
    }
    decodedPattern[i] = new Array();
    for (j=gridSize-1; j>=0; j--)
    {
      if (rowEncoding >= Math.pow(2, j))
      {
        decodedPattern[i].unshift(true);
        rowEncoding -= Math.pow(2, j);
      }
      else
      {
        decodedPattern[i].unshift(false);
      }
    }
    newPixelsArray = newPixelsArray.concat(decodedPattern[i]);
  }
  return newPixelsArray;
}

// updates all the relevant views of the pattern being edited
function refreshViews()
{
  updatePixelGrid();
  updateTiledCanvas();
  updateEncodedPatternDisplayDiv();
}

// ---- functions operating on underlying representation of pixels ----

// clears all the pixels in the grid, i.e. makes them white ("off")
function clearPixels()
{
  pixelsArray = pixelsArray.map(function(x) {return false;});
  updateEncodedPatternValue(true);
  refreshViews();
}

// fills all the pixels in the grid, i.e. makes them black ("on")
function fillPixels()
{
  pixelsArray = pixelsArray.map(function(x) {return true;});
  updateEncodedPatternValue(true);
  refreshViews();
}

// inverts all the pixels in the grid, i.e. white ("off") -> black ("on"), and vice-versa
function invertPixels()
{
  pixelsArray = pixelsArray.map(function(x) {return !x;});
  updateEncodedPatternValue(true);
  refreshViews();
}

// randomly sets each pixel black or white
function randomisePixels()
{
  pixelsArray = pixelsArray.map(function(x) {return Math.round(Math.random(1)) == 1});
  updateEncodedPatternValue(true);
  refreshViews();
}

// shifts all the pixels in the grid one column left; the first column wraps around to the last
function shiftPixelsLeft()
{
  for (var i=0; i<pixelsArray.length; i++)
  {
    var savedPixel;
    var c = columnForIndex(i);
    if (c == 0)
    {
      savedPixel = pixelsArray[i];
    }
    if (c == gridSize-1)
    {
      pixelsArray[i] = savedPixel;
    }
    else
    {
      pixelsArray[i] = pixelsArray[i+1];
    }
  }
  updateEncodedPatternValue(true);
  refreshViews();
}

// shifts all the pixels in the grid one column right; the last column wraps around to the first
function shiftPixelsRight()
{
  var rows = new Array(gridSize);
  for (var i=0; i<gridSize; i++)
  {
    rows[i] = pixelsArray.splice(0, gridSize);
  }
  for (var i=0; i<gridSize; i++)
  {
    var savedPixel = rows[i].pop();
    rows[i].unshift(savedPixel);
    pixelsArray = pixelsArray.concat(rows[i]);
  }
  updateEncodedPatternValue(true);
  refreshViews();
}

// shifts all the pixels in the grid one row up; the first row wraps around to the last
function shiftPixelsUp()
{
  var firstRow = pixelsArray.splice(0, gridSize);
  pixelsArray = pixelsArray.concat(firstRow);
  updateEncodedPatternValue(true);
  refreshViews();
}

// shifts all the pixels in the grid one row down; the last row wraps around to the first
function shiftPixelsDown()
{
  var lastRow = pixelsArray.splice(pixelsArray.length-gridSize, gridSize);
  pixelsArray = lastRow.concat(pixelsArray);
  updateEncodedPatternValue(true);
  refreshViews();
}

// rotates the pattern 90 degrees to the left (anti-clockwise)
function rotateLeft90Degrees()
{
  // adapted from Ruby algorithm http://stackoverflow.com/questions/42519/how-do-you-rotate-a-two-dimensional-array/48607#48607
  for (var i=0; i<=Math.floor(gridSize/2)-1; i++)
  {
    for (var j=i; j<=gridSize-i-2; j++)
    {
      var tmp = pixelsArray[translateRowAndColToIndex(i, j)];
      pixelsArray[translateRowAndColToIndex(i, j)] = pixelsArray[translateRowAndColToIndex(j, gridSize-i-1)];
      pixelsArray[translateRowAndColToIndex(j, gridSize-i-1)] = pixelsArray[translateRowAndColToIndex(gridSize-i-1, gridSize-j-1)];
      pixelsArray[translateRowAndColToIndex(gridSize-i-1, gridSize-j-1)] = pixelsArray[translateRowAndColToIndex(gridSize-j-1, i)];
      pixelsArray[translateRowAndColToIndex(gridSize-j-1, i)] = tmp;
    }
  }
  // Simpler but slower alternative: the code re-use way...
  //   rotateRight90Degrees();
  //   rotate180Degrees();
  updateEncodedPatternValue(true);
  refreshViews();
}

// rotates the pattern 90 degrees to the right (clockwise)
function rotateRight90Degrees()
{
  // based on Ruby algorithm http://stackoverflow.com/questions/42519/how-do-you-rotate-a-two-dimensional-array/48607#48607
  for (var i=0; i<=Math.floor(gridSize/2)-1; i++)
  {
    for (var j=i; j<=gridSize-i-2; j++)
    {
      var tmp = pixelsArray[translateRowAndColToIndex(i, j)];
      pixelsArray[translateRowAndColToIndex(i, j)] = pixelsArray[translateRowAndColToIndex(gridSize-j-1, i)];
      pixelsArray[translateRowAndColToIndex(gridSize-j-1, i)] = pixelsArray[translateRowAndColToIndex(gridSize-i-1, gridSize-j-1)];
      pixelsArray[translateRowAndColToIndex(gridSize-i-1, gridSize-j-1)] = pixelsArray[translateRowAndColToIndex(j, gridSize-i-1)];
      pixelsArray[translateRowAndColToIndex(j, gridSize-i-1)] = tmp;
    }
  }
  updateEncodedPatternValue(true);
  refreshViews();
}

// rotates the pattern 180 degrees
function rotate180Degrees()
{
  pixelsArray = pixelsArray.reverse();
  updateEncodedPatternValue(true);
  refreshViews();
}

// flips the pattern horizontally, i.e. reflects it in the y-axis
function flipHorizontal()
{
  var rows = new Array(gridSize);
  for (var i=0; i<gridSize; i++)
  {
    rows[i] = pixelsArray.splice(0, gridSize);
  }
  for (var i=0; i<gridSize; i++)
  {
    pixelsArray = pixelsArray.concat(rows[i].reverse());
  }
  updateEncodedPatternValue(true);
  refreshViews();
}

// flips the pattern vertically, i.e. reflects it in the x-axis
function flipVertical()
{
  var rows = new Array(gridSize);
  for (var i=0; i<gridSize; i++)
  {
    rows[i] = pixelsArray.splice(0, gridSize);
  }
  for (var i=0; i<gridSize; i++)
  {
    pixelsArray = rows[i].concat(pixelsArray);
  }
  updateEncodedPatternValue(true);
  refreshViews();
}

// ---- undo/redo functionality ----

function addUndoItem(newEncodedPattern)
{
  if (!encodedPattern.equals(newEncodedPattern))
  {
    undoStack.push(newEncodedPattern.copy());
    var undoButton = document.getElementById("undoButton");
    undoButton.disabled = false;
    redoStack = new Array();
    var redoButton = document.getElementById("redoButton");
    redoButton.disabled = true;
  }
}

function undo()
{
  // get previous pattern from undo stack
  var encodedArray = undoStack.pop();
  recordThePattern = false;
  var undoButton = document.getElementById("undoButton");
  undoButton.disabled = (undoStack.length == 0);

  // push the current state onto the redo stack
  redoStack.push(encodedPattern.copy());
  var redoButton = document.getElementById("redoButton");
  redoButton.disabled = false;

  // perform the undo
  if (encodedArray && (encodedArray.length == 8))
  {
    loadEncodedPattern(encodedArray.toString());
    encodedPattern = encodedArray.copy();
    refreshViews();
  }
  recordThePattern = true;
}

function redo()
{
  // get "next" pattern from redo stack
  var encodedArray = redoStack.pop();
  recordThePattern = false;
  if (redoStack.length == 0)
  {
    var redoButton = document.getElementById("redoButton");
    redoButton.disabled = true;
  }

  // perform the redo
  if (encodedArray && (encodedArray.length == 8))
  {
    undoStack.push(encodedPattern.copy());
    var undoButton = document.getElementById("undoButton");
    undoButton.disabled = false;
    loadEncodedPattern(encodedArray.toString());
    encodedPattern = encodedArray.copy();
    refreshViews();
  }
  recordThePattern = true;
}

// ---- examples/preset patterns section ----

// Populate example patterns
function populateExamples()
{
  var div = document.getElementById("thumbnailsArea");
  for (var i=0; i<exampleTileEncodings.length; i++)
  {
    var exampleLink = document.createElement("a");
    exampleLink.dataArray = exampleTileEncodings[i].encodedArray;
    exampleLink.title = exampleTileEncodings[i].name;
    var canvasElement = document.createElement("canvas");
    canvasElement.id = "tilePreviewCanvas" + i;
    canvasElement.width = "32";
    if (isMobileSafari)
    {
      exampleLink.type = "cancel";
      exampleLink.onclick = function() { loadExample(this.dataArray); };
      exampleLink.href = "#tiledViewPage";
      canvasElement.height = "24";
    }
    else
    {
      exampleLink.href = "#";
      exampleLink.onclick = function() { loadExample(this.dataArray); return false; };
      canvasElement.height = "32";
    }
    exampleLink.appendChild(canvasElement);
    div.appendChild(exampleLink);
    // render a preview thumbmail of the pattern
    previewExample(exampleTileEncodings[i].encodedArray, canvasElement.id);
  }
}

// loads the example tile pattern into the preview area
function previewExample(encodedArray, tilePreviewCanvasId)
{
  var newPixelsArray = decodeEncodedPattern(encodedArray.toString());
  if (newPixelsArray)
  {
    updateHiddenStagingArea(newPixelsArray);
    tileCanvasUsingPattern(document.getElementById(tilePreviewCanvasId));
  }
}

// loads the example tile pattern into the editing area
function loadExample(encodedArray)
{
  loadEncodedPattern(encodedArray.toString());
}

// defines wrapper object for the example tile patterns
function TilePattern(name, encodedArray)
{
  this.name = name;  // mainly for my benefit
  this.encodedArray = encodedArray;
}

// array of pre-defined example patterns
var exampleTileEncodings = new Array(
  new TilePattern("Mac OS/HyperCard (1,1)", [0,0,0,0,0,0,0,0]),
  new TilePattern("Mac OS/HyperCard (1,2)", [0,0,0,1,0,0,0,16]),
  new TilePattern("Mac OS/HyperCard (1,3)", [0,68,0,17,0,68,0,17]),
  new TilePattern("Mac OS/HyperCard (1,4)", [68,17,17,68,68,17,17,68]),
  new TilePattern("Mac OS/HyperCard (1,5)", [85,17,85,68,85,17,85,68]),
  new TilePattern("Mac OS/HyperCard (1,6)", [51,85,204,85,51,85,204,85]),
  new TilePattern("Mac OS/HyperCard (1,7)", [221,85,119,85,221,85,119,85]),
  new TilePattern("Mac OS/HyperCard (1,8)", [187,187,238,238,187,187,238,238]),
  new TilePattern("Mac OS/HyperCard (1,9)", [255,238,255,187,255,238,255,187]),
  new TilePattern("Mac OS/HyperCard (1,10)", [238,255,255,255,238,255,255,255]),
  new TilePattern("Mac OS/HyperCard (2,1)", [64,2,16,1,32,4,128,8]),
  new TilePattern("Mac OS/HyperCard (2,2)", [255,255,255,255,255,255,255,255]),
  new TilePattern("Mac OS/HyperCard (2,3)", [68,17,68,17,68,17,68,17]),
  new TilePattern("Mac OS/HyperCard (2,4)", [68,34,17,136,68,34,17,136]),
  new TilePattern("Mac OS/HyperCard (2,5)", [176,16,3,25,200,64,12,133]),
  new TilePattern("Mac OS/HyperCard (2,6)", [141,12,192,216,27,3,48,177]),
  new TilePattern("Mac OS/HyperCard (2,7)", [85,0,85,0,85,0,85,0]),
  new TilePattern("Mac OS/HyperCard (2,8)", [170,17,68,17,170,17,68,17]),
  new TilePattern("Mac OS/HyperCard (2,9)", [68,170,17,170,68,170,17,170]),
  new TilePattern("Mac OS/HyperCard (2,10)", [238,187,238,187,238,187,238,187]),
  new TilePattern("Mac OS/HyperCard (3,1)", [16,0,0,0,0,0,0,0]),
  new TilePattern("Mac OS/HyperCard (3,2)", [170,85,170,85,170,85,170,85]),
  new TilePattern("Mac OS/HyperCard (3,3)", [33,192,3,4,8,8,12,18]),
  new TilePattern("Mac OS/HyperCard (3,4)", [8,20,34,201,34,20,8,8]),
  new TilePattern("Mac OS/HyperCard (3,5)", [17,40,68,130,17,130,68,40]),
  new TilePattern("Mac OS/HyperCard (3,6)", [16,40,199,1,1,130,124,16]),
  new TilePattern("Mac OS/HyperCard (3,7)", [68,32,49,46,68,232,25,8]),
  new TilePattern("Mac OS/HyperCard (3,8)", [1,125,1,17,16,215,16,17]),
  new TilePattern("Mac OS/HyperCard (3,9)", [98,137,50,196,132,70,41,148]),
  new TilePattern("Mac OS/HyperCard (3,10)", [57,130,125,84,147,40,215,69]),
  new TilePattern("Mac OS/HyperCard (4,1)", [8,20,0,0,128,65,0,0]),
  new TilePattern("Mac OS/HyperCard (4,2)", [64,32,0,2,4,8,0,128]),
  new TilePattern("Mac OS/HyperCard (4,3)", [4,0,68,0,4,0,85,0]),
  new TilePattern("Mac OS/HyperCard (4,4)", [4,255,4,4,4,4,4,4]),
  new TilePattern("Mac OS/HyperCard (4,5)", [4,2,1,128,64,224,17,14]),
  new TilePattern("Mac OS/HyperCard (4,6)", [4,4,4,255,64,64,64,255]),
  new TilePattern("Mac OS/HyperCard (4,7)", [136,29,62,92,136,197,227,209]),
  new TilePattern("Mac OS/HyperCard (4,8)", [247,247,52,52,52,52,247,0]),
  new TilePattern("Mac OS/HyperCard (4,9)", [255,247,235,213,42,20,8,0]),
  new TilePattern("Mac OS/HyperCard (4,10)", [40,20,40,125,190,125,190,20]),
  new TilePattern("Variation of Mac OS/HyperCard (4,2)", [1,128,64,34,4,8,16,34]),
  new TilePattern("Packed tins - simplification of Mac OS/HyperCard (3,4)", [8,20,34,193,34,20,8,8]),
  new TilePattern("Thin crossed diagonals", [85,138,5,138,85,168,80,168]),
  new TilePattern("Simple weave - variation of Mac OS/HyperCard (4,7)", [42,17,162,68,138,17,168,68]),
  new TilePattern("Tall zigzag", [4,10,10,17,160,160,64,0]),
  new TilePattern("Zigzag", [0,4,10,17,160,64,0,0]),
  new TilePattern("Double zigzag", [160,68,10,17,160,68,10,17]),
  new TilePattern("Thick double zigzag", [177,228,78,27,177,228,78,27]),
  new TilePattern("Thin waves", [0,0,14,17,224,0,0,0]),
  new TilePattern("Thin double waves", [224,0,14,17,224,0,14,17]),
  new TilePattern("Plain mosaic tiles", [42,65,20,65,20,65,42,0]),
  new TilePattern("Plain wallpaper", [42,65,20,73,20,65,42,128]),
  new TilePattern("Diagonal -||-", [68,32,17,2,68,136,17,2]),
  new TilePattern("Net-like", [0,6,6,0,12,108,99,3]),
  new TilePattern("Little diamonds", [80,136,80,34,5,136,5,34]),
  new TilePattern("Hearts", [0,54,127,127,62,28,8,0]),
  new TilePattern("Arrows: up/down", [62,227,128,193,99,54,28,8]),
  new TilePattern("Little pins", [96,102,6,8,16,34,1,128]),
  new TilePattern("Left/Right chevrons", [85,34,68,136,85,136,68,34]),
  new TilePattern("Just another lattice", [85,0,85,136,37,80,37,136]),
  new TilePattern("Atari ST #2,1", [34,0,136,0,34,0,136,0]),
  new TilePattern("Atari ST #2,2", [170,0,170,0,170,0,170,0]),
  new TilePattern("Atari ST #2,3", [34,85,136,85,34,85,136,85]),
  new TilePattern("Atari ST #2,4", [34,136,34,136,34,136,34,136]),
  new TilePattern("Atari ST #2,5", [238,85,187,85,238,85,187,85]),
  new TilePattern("Atari ST #2,6", [255,170,255,170,255,170,255,170]),
  // new TilePattern("Atari ST #2,7", [238,255,187,255,238,255,187,255]),  // same as Mac OS/HyperCard (1,9)
  // new TilePattern("Atari ST #2,8", [255,255,255,255,255,255,255,255]),  // same as Mac OS/HyperCard (2,2)
  // new TilePattern("Atari ST #2,9", [32,255,2,2,2,255,32,32]),  // very similar to Mac OS/HyperCard (4,6)
  new TilePattern("Atari ST #2,10", [10,4,2,1,128,64,160,17]),
  new TilePattern("Atari ST #2,11", [0,0,32,80,0,0,2,5]),
  new TilePattern("Atari ST #2,12", [32,32,250,5,2,2,175,80]),
  new TilePattern("Atari ST #2,13", [16,32,64,0,4,2,1,0]),
  new TilePattern("Atari ST #2,14 (approx)", [48,54,198,192,12,108,99,3]),
  new TilePattern("Atari ST #2,15 (approx)", [0,2,0,0,0,0,32,0]),
  new TilePattern("Atari ST #2,16", [108,198,227,241,216,141,31,62]),
  new TilePattern("Atari ST #2,17", [34,0,170,0,34,5,136,80]),
  new TilePattern("Atari ST #2,18", [34,0,32,0,170,0,32,0]),
  new TilePattern("Atari ST #2,19", [238,25,31,31,238,145,241,241]),
  new TilePattern("Atari ST #2,20", [65,62,8,8,20,227,128,128]),
  new TilePattern("Atari ST #2,21", [2,2,3,132,72,48,192,1]),
  new TilePattern("Atari ST #2,22", [30,30,225,225,225,225,30,30]),
  new TilePattern("Atari ST #2,23", [241,224,64,224,241,251,255,251]),
  new TilePattern("Atari ST #2,24", [68,255,68,34,17,255,17,34]),
  new TilePattern("Atari ST #3,1", [68,34,17,136,68,34,17,136]),
  new TilePattern("Atari ST #3,2", [204,102,51,153,204,102,51,153]),
  new TilePattern("Atari ST #3,4", [68,68,68,68,68,68,68,68]),
  new TilePattern("Atari ST #3,5", [255,0,0,0,255,0,0,0]),
  // new TilePattern("Atari ST #3,6", [32,255,32,32,32,32,32,32]),  // very similar to Mac OS/HyperCard (4,4) 
  new TilePattern("Atari ST #3,7", [64,32,16,8,4,2,1,128]),
  new TilePattern("Atari ST #3,8", [192,96,48,24,12,6,3,129]),
  new TilePattern("Atari ST #3,9", [20,34,65,128,65,34,20,8]),
  new TilePattern("Atari ST #3,10", [4,4,4,4,4,4,4,4]),
  new TilePattern("Atari ST #3,11", [0,0,0,0,255,0,0,0]),
  // new TilePattern("Atari ST #3,12", [32,32,32,32,32,32,32,255]),  // very similar to Atari ST #3,6 
  new TilePattern("Diagonal bricks - mirror of Atari ST 2,10", [1,2,4,8,24,36,66,129]),
  new TilePattern("Diagonal bricks 2 - variation of Atari ST 2,10", [17,34,68,136,24,36,66,129]),
  new TilePattern("Diagonal bricks 3 - variation of Atari ST 2,10", [129,3,6,12,24,60,102,195]),
  new TilePattern("Variation of Atari ST 2,24", [255,17,34,17,255,136,68,136]),
  new TilePattern("Thin diagonal - mirror of Atari ST #3,7", [1,2,4,8,16,32,64,128]),
  new TilePattern("Thick diagonal - mirror of Atari ST #3,8", [129,3,6,12,24,48,96,192])
);
