]> git.sesse.net Git - remoteglot/blobdiff - www/js/chessboard-0.3.0.js
Unbreak dragging again.
[remoteglot] / www / js / chessboard-0.3.0.js
index cbd46f4f4f064f0ac67620b7bdd93c4842635af7..f69d0ed3802968a07c2ee533464565c82cb7a88e 100644 (file)
@@ -26,7 +26,7 @@ function validMove(move) {
   var tmp = move.split('-');
   if (tmp.length !== 2) return false;
 
-  return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true);
+  return validSquare(tmp[0]) && validSquare(tmp[1]);
 }
 
 function validSquare(square) {
@@ -67,9 +67,9 @@ function validPositionObject(pos) {
   if (typeof pos !== 'object') return false;
 
   for (var i in pos) {
-    if (pos.hasOwnProperty(i) !== true) continue;
+    if (!pos.hasOwnProperty(i)) continue;
 
-    if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) {
+    if (!validSquare(i) || !validPieceCode(pos[i])) {
       return false;
     }
   }
@@ -104,7 +104,7 @@ function pieceCodeToFen(piece) {
 // convert FEN string to position object
 // returns false if the FEN string is invalid
 function fenToObj(fen) {
-  if (validFen(fen) !== true) {
+  if (!validFen(fen)) {
     return false;
   }
 
@@ -144,11 +144,12 @@ function fenToObj(fen) {
 // position object to FEN string
 // returns false if the obj is not a valid position object
 function objToFen(obj) {
-  if (validPositionObject(obj) !== true) {
+  if (!validPositionObject(obj)) {
     return false;
   }
 
   var fen = '';
+  let num_empty = 0;
 
   var currentRow = 8;
   for (var i = 0; i < 8; i++) {
@@ -156,33 +157,31 @@ function objToFen(obj) {
       var square = COLUMNS[j] + currentRow;
 
       // piece exists
-      if (obj.hasOwnProperty(square) === true) {
+      if (obj.hasOwnProperty(square)) {
+        if (num_empty > 0) {
+          fen += num_empty;
+          num_empty = 0;
+        }
         fen += pieceCodeToFen(obj[square]);
       }
 
       // empty space
       else {
-        fen += '1';
+        ++num_empty;
       }
     }
 
     if (i !== 7) {
+      if (num_empty > 0) {
+        fen += num_empty;
+        num_empty = 0;
+      }
       fen += '/';
     }
 
     currentRow--;
   }
 
-  // squeeze the numbers together
-  // haha, I love this solution...
-  fen = fen.replace(/11111111/g, '8');
-  fen = fen.replace(/1111111/g, '7');
-  fen = fen.replace(/111111/g, '6');
-  fen = fen.replace(/11111/g, '5');
-  fen = fen.replace(/1111/g, '4');
-  fen = fen.replace(/111/g, '3');
-  fen = fen.replace(/11/g, '2');
-
   return fen;
 }
 
@@ -199,8 +198,7 @@ cfg = cfg || {};
 // Constants
 //------------------------------------------------------------------------------
 
-var MINIMUM_JQUERY_VERSION = '1.7.0',
-  START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR',
+var START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR',
   START_POSITION = fenToObj(START_FEN);
 
 // use unique class names to prevent clashing with anything else on the page
@@ -209,16 +207,11 @@ var CSS = {
   alpha: 'alpha-d2270',
   board: 'board-b72b1',
   chessboard: 'chessboard-63f37',
-  clearfix: 'clearfix-7da63',
   highlight1: 'highlight1-32417',
   highlight2: 'highlight2-9c5d2',
   notation: 'notation-322f9',
   numeric: 'numeric-fc462',
   piece: 'piece-417db',
-  row: 'row-5277c',
-  sparePieces: 'spare-pieces-7492f',
-  sparePiecesBottom: 'spare-pieces-bottom-ae20f',
-  sparePiecesTop: 'spare-pieces-top-4028b',
   square: 'square-55d63'
 };
 var CSSColor = {};
@@ -232,9 +225,7 @@ CSSColor['black'] = 'black-3c85d';
 // DOM elements
 var containerEl,
   boardEl,
-  draggedPieceEl,
-  sparePiecesTopEl,
-  sparePiecesBottomEl;
+  draggedPieceEl;
 
 // constructor return object
 var widget = {};
@@ -243,57 +234,27 @@ var widget = {};
 // Stateful
 //------------------------------------------------------------------------------
 
-var ANIMATION_HAPPENING = false,
-  BOARD_BORDER_SIZE = 2,
-  CURRENT_ORIENTATION = 'white',
+var CURRENT_ORIENTATION = 'white',
   CURRENT_POSITION = {},
-  SQUARE_SIZE,
   DRAGGED_PIECE,
   DRAGGED_PIECE_LOCATION,
   DRAGGED_PIECE_SOURCE,
   DRAGGING_A_PIECE = false,
-  SPARE_PIECE_ELS_IDS = {},
-  SQUARE_ELS_IDS = {},
-  SQUARE_ELS_OFFSETS;
+  PIECE_ON_SQUARE = {};
 
 //------------------------------------------------------------------------------
 // JS Util Functions
 //------------------------------------------------------------------------------
 
-// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
+let id_counter = 0;
 function createId() {
-  return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function(c) {
-    var r = Math.random() * 16 | 0;
-    return r.toString(16);
-  });
+  return 'chesspiece-id-' + (id_counter++);
 }
 
 function deepCopy(thing) {
   return JSON.parse(JSON.stringify(thing));
 }
 
-function parseSemVer(version) {
-  var tmp = version.split('.');
-  return {
-    major: parseInt(tmp[0], 10),
-    minor: parseInt(tmp[1], 10),
-    patch: parseInt(tmp[2], 10)
-  };
-}
-
-// returns true if version is >= minimum
-function compareSemVer(version, minimum) {
-  version = parseSemVer(version);
-  minimum = parseSemVer(minimum);
-
-  var versionNum = (version.major * 10000 * 10000) +
-    (version.minor * 10000) + version.patch;
-  var minimumNum = (minimum.major * 10000 * 10000) +
-    (minimum.minor * 10000) + minimum.patch;
-
-  return (versionNum >= minimumNum);
-}
-
 //------------------------------------------------------------------------------
 // Validation / Errors
 //------------------------------------------------------------------------------
@@ -305,7 +266,7 @@ function compareSemVer(version, minimum) {
  */
 function error(code, msg, obj) {
   // do nothing if showErrors is not set
-  if (cfg.hasOwnProperty('showErrors') !== true ||
+  if (!cfg.hasOwnProperty('showErrors') ||
       cfg.showErrors === false) {
     return;
   }
@@ -385,7 +346,7 @@ function validAnimationSpeed(speed) {
 
 // validate config / set default options
 function expandConfig() {
-  if (typeof cfg === 'string' || validPositionObject(cfg) === true) {
+  if (typeof cfg === 'string' || validPositionObject(cfg)) {
     cfg = {
       position: cfg
     };
@@ -407,61 +368,46 @@ function expandConfig() {
     cfg.draggable = false;
   }
 
-  // default for dropOffBoard is 'snapback'
-  if (cfg.dropOffBoard !== 'trash') {
-    cfg.dropOffBoard = 'snapback';
-  }
-
-  // default for sparePieces is false
-  if (cfg.sparePieces !== true) {
-    cfg.sparePieces = false;
-  }
-
-  // draggable must be true if sparePieces is enabled
-  if (cfg.sparePieces === true) {
-    cfg.draggable = true;
-  }
-
   // default piece theme is wikipedia
-  if (cfg.hasOwnProperty('pieceTheme') !== true ||
+  if (!cfg.hasOwnProperty('pieceTheme') ||
       (typeof cfg.pieceTheme !== 'string' &&
        typeof cfg.pieceTheme !== 'function')) {
     cfg.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png';
   }
 
   // animation speeds
-  if (cfg.hasOwnProperty('appearSpeed') !== true ||
-      validAnimationSpeed(cfg.appearSpeed) !== true) {
+  if (!cfg.hasOwnProperty('appearSpeed') ||
+      !validAnimationSpeed(cfg.appearSpeed)) {
     cfg.appearSpeed = 200;
   }
-  if (cfg.hasOwnProperty('moveSpeed') !== true ||
-      validAnimationSpeed(cfg.moveSpeed) !== true) {
+  if (!cfg.hasOwnProperty('moveSpeed') ||
+      !validAnimationSpeed(cfg.moveSpeed)) {
     cfg.moveSpeed = 200;
   }
-  if (cfg.hasOwnProperty('snapbackSpeed') !== true ||
-      validAnimationSpeed(cfg.snapbackSpeed) !== true) {
+  if (!cfg.hasOwnProperty('snapbackSpeed') ||
+      !validAnimationSpeed(cfg.snapbackSpeed)) {
     cfg.snapbackSpeed = 50;
   }
-  if (cfg.hasOwnProperty('snapSpeed') !== true ||
-      validAnimationSpeed(cfg.snapSpeed) !== true) {
+  if (!cfg.hasOwnProperty('snapSpeed') ||
+      !validAnimationSpeed(cfg.snapSpeed)) {
     cfg.snapSpeed = 25;
   }
-  if (cfg.hasOwnProperty('trashSpeed') !== true ||
-      validAnimationSpeed(cfg.trashSpeed) !== true) {
+  if (!cfg.hasOwnProperty('trashSpeed') ||
+      !validAnimationSpeed(cfg.trashSpeed)) {
     cfg.trashSpeed = 100;
   }
 
   // make sure position is valid
-  if (cfg.hasOwnProperty('position') === true) {
+  if (cfg.hasOwnProperty('position')) {
     if (cfg.position === 'start') {
       CURRENT_POSITION = deepCopy(START_POSITION);
     }
 
-    else if (validFen(cfg.position) === true) {
+    else if (validFen(cfg.position)) {
       CURRENT_POSITION = fenToObj(cfg.position);
     }
 
-    else if (validPositionObject(cfg.position) === true) {
+    else if (validPositionObject(cfg.position)) {
       CURRENT_POSITION = deepCopy(cfg.position);
     }
 
@@ -473,53 +419,6 @@ function expandConfig() {
   return true;
 }
 
-//------------------------------------------------------------------------------
-// DOM Misc
-//------------------------------------------------------------------------------
-
-// calculates square size based on the width of the container
-// got a little CSS black magic here, so let me explain:
-// get the width of the container element (could be anything), reduce by 1 for
-// fudge factor, and then keep reducing until we find an exact mod 8 for
-// our square size
-function calculateSquareSize() {
-  var containerWidth = parseInt(getComputedStyle(containerEl).width, 10);
-
-  // defensive, prevent infinite loop
-  if (! containerWidth || containerWidth <= 0) {
-    return 0;
-  }
-
-  // pad one pixel
-  var boardWidth = containerWidth - 1;
-
-  while (boardWidth % 8 !== 0 && boardWidth > 0) {
-    boardWidth--;
-  }
-
-  return (boardWidth / 8);
-}
-
-// create random IDs for elements
-function createElIds() {
-  // squares on the board
-  for (var i = 0; i < COLUMNS.length; i++) {
-    for (var j = 1; j <= 8; j++) {
-      var square = COLUMNS[i] + j;
-      SQUARE_ELS_IDS[square] = square + '-' + createId();
-    }
-  }
-
-  // spare pieces
-  var pieces = 'KQRBNP'.split('');
-  for (var i = 0; i < pieces.length; i++) {
-    var whitePiece = 'w' + pieces[i];
-    var blackPiece = 'b' + pieces[i];
-    SPARE_PIECE_ELS_IDS[whitePiece] = whitePiece + '-' + createId();
-    SPARE_PIECE_ELS_IDS[blackPiece] = blackPiece + '-' + createId();
-  }
-}
-
 //------------------------------------------------------------------------------
 // Markup Building
 //------------------------------------------------------------------------------
@@ -527,39 +426,13 @@ function createElIds() {
 function buildBoardContainer() {
   var html = '<div class="' + CSS.chessboard + '">';
 
-  if (cfg.sparePieces === true) {
-    html += '<div class="' + CSS.sparePieces + ' ' +
-      CSS.sparePiecesTop + '"></div>';
-  }
-
   html += '<div class="' + CSS.board + '"></div>';
 
-  if (cfg.sparePieces === true) {
-    html += '<div class="' + CSS.sparePieces + ' ' +
-      CSS.sparePiecesBottom + '"></div>';
-  }
-
   html += '</div>';
 
   return html;
 }
 
-/*
-var buildSquare = function(color, size, id) {
-  var html = '<div class="' + CSS.square + ' ' + CSSColor[color] + '" ' +
-  'style="width: ' + size + 'px; height: ' + size + 'px" ' +
-  'id="' + id + '">';
-
-  if (cfg.showNotation === true) {
-
-  }
-
-  html += '</div>';
-
-  return html;
-};
-*/
-
 function buildBoard(orientation) {
   if (orientation !== 'black') {
     orientation = 'white';
@@ -577,27 +450,29 @@ function buildBoard(orientation) {
 
   var squareColor = 'white';
   for (var i = 0; i < 8; i++) {
-    html += '<div class="' + CSS.row + '">';
     for (var j = 0; j < 8; j++) {
       var square = alpha[j] + row;
 
       html += '<div class="' + CSS.square + ' ' + CSSColor[squareColor] + ' ' +
         'square-' + square + '" ' +
-        'style="width: ' + SQUARE_SIZE + 'px; height: ' + SQUARE_SIZE + 'px" ' +
-        'id="' + SQUARE_ELS_IDS[square] + '" ' +
+        'style="grid-row: ' + (i+1) + '; grid-column: ' + (j+1) + ';" ' +
         'data-square="' + square + '">';
 
-      if (cfg.showNotation === true) {
+      if (cfg.showNotation) {
         // alpha notation
         if ((orientation === 'white' && row === 1) ||
             (orientation === 'black' && row === 8)) {
-          html += '<div class="' + CSS.notation + ' ' + CSS.alpha + '">' +
+          let bottom = 'calc(' + (12.5 * (7-i)) + '% + 1px)';
+          let right = 'calc(' + (12.5 * (7-j)) + '% + 3px)';
+          html += '<div class="' + CSS.alpha + '" style="right: ' + right + '; bottom: ' + bottom + ';">' +
             alpha[j] + '</div>';
         }
 
         // numeric notation
         if (j === 0) {
-          html += '<div class="' + CSS.notation + ' ' + CSS.numeric + '">' +
+          let top = 'calc(' + (12.5 * i) + '% + 2px)';
+          let left = 'calc(' + (12.5 * j) + '% + 2px)';
+          html += '<div class="' + CSS.numeric + '" style="top: ' + top + '; left: ' + left + ';">' +
             row + '</div>';
         }
       }
@@ -606,7 +481,6 @@ function buildBoard(orientation) {
 
       squareColor = (squareColor === 'white' ? 'black' : 'white');
     }
-    html += '<div class="' + CSS.clearfix + '"></div></div>';
 
     squareColor = (squareColor === 'white' ? 'black' : 'white');
 
@@ -638,39 +512,18 @@ function buildPieceImgSrc(piece) {
 /**
  * @param {!string} piece
  * @param {boolean=} hidden
- * @param {!string=} id
  */
-function buildPiece(piece, hidden, id) {
+function buildPiece(piece, hidden) {
   let img = document.createElement('img');
   img.src = buildPieceImgSrc(piece);
-  if (id && typeof id === 'string') {
-    img.setAttribute('id', id);
-  }
   img.setAttribute('alt', '');
   img.classList.add(CSS.piece);
-  img.setAttribute('data-piece', piece);
-  img.style.width = SQUARE_SIZE + 'px';
-  img.style.height = SQUARE_SIZE + 'px';
   if (hidden === true) {
     img.style.display = 'none';
   }
   return img;
 }
 
-function buildSparePieces(color) {
-  var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP'];
-  if (color === 'black') {
-    pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP'];
-  }
-
-  var html = '';
-  for (var i = 0; i < pieces.length; i++) {
-    html += buildPiece(pieces[i], false, SPARE_PIECE_ELS_IDS[pieces[i]]);
-  }
-
-  return html;
-}
-
 //------------------------------------------------------------------------------
 // Animations
 //------------------------------------------------------------------------------
@@ -684,186 +537,149 @@ function offset(el) {  // From https://youmightnotneedjquery.com/.
   };
 }
 
-function animateSquareToSquare(src, dest, piece, completeFn) {
-  // get information about the source and destination squares
-  var srcSquareEl = document.getElementById(SQUARE_ELS_IDS[src]);
-  var srcSquarePosition = offset(srcSquareEl);
-  var destSquareEl = document.getElementById(SQUARE_ELS_IDS[dest]);
-  var destSquarePosition = offset(destSquareEl);
-
-  // create the animated piece and absolutely position it
-  // over the source square
-  var animatedPieceId = createId();
-  document.body.append(buildPiece(piece, true, animatedPieceId));
-  var animatedPieceEl = document.getElementById(animatedPieceId);
-  animatedPieceEl.style.display = null;
-  animatedPieceEl.style.position = 'absolute';
-  animatedPieceEl.style.top = srcSquarePosition.top + 'px';
-  animatedPieceEl.style.left = srcSquarePosition.left + 'px';
-
-  // remove original piece(s) from source square
-  // TODO: multiple pieces should never really happen, but it will if we are moving
-  // while another animation still isn't done
-  srcSquareEl.querySelectorAll('.' + CSS.piece).forEach((piece) => piece.remove());
-
-  // on complete
-  var complete = function() {
-    // add the "real" piece to the destination square
-    destSquareEl.append(buildPiece(piece));
-
-    // remove the animated piece
-    animatedPieceEl.remove();
-
-    // run complete function
-    if (typeof completeFn === 'function') {
-      completeFn();
-    }
+function findSquarePosition(square) {
+  let s1 = square.split('');
+  var s1x = COLUMNS.indexOf(s1[0]);
+  var s1y = parseInt(s1[1], 10) - 1;
+  if (CURRENT_ORIENTATION === 'white') {
+    s1y = 7 - s1y;
+  }
+  return {
+    top: (s1y * 12.5) + '%',
+    left: (s1x * 12.5) + '%',
   };
-
-  // animate the piece to the destination square
-  animatedPieceEl.addEventListener('transitionend', complete, {once: true});
-  requestAnimationFrame(() => {
-    animatedPieceEl.style.transitionProperty = 'top, left';
-    animatedPieceEl.style.transitionDuration = cfg.moveSpeed + 'ms';
-    animatedPieceEl.style.top = destSquarePosition.top + 'px';
-    animatedPieceEl.style.left = destSquarePosition.left + 'px';
-  });
 }
 
-function animateSparePieceToSquare(piece, dest, completeFn) {
-  var srcOffset = offset(document.getelementById(SPARE_PIECE_ELS_IDS[piece]));
-  var destSquareEl = document.getElementById(SQUARE_ELS_IDS[dest]);
-  var destOffset = offset(destSquareEl);
-
-  // create the animate piece
-  var pieceId = createId();
-  document.body.append(buildPiece(piece, true, pieceId));
-  var animatedPieceEl = document.getElementById(pieceId);
-  animatedPieceEl.style.display = '';
-  animatedPieceEl.style.position = 'absolute';
-  animatedPieceEl.style.left = srcOffset.left;
-  animatedPieceEl.style.top = srcOffset.top;
-
-  // on complete
-  var complete = function() {
-    // add the "real" piece to the destination square
-    destSquareEl.querySelector('.' + CSS.piece).remove();
-    destSquareEl.append(buildPiece(piece));
-
-    // remove the animated piece
-    animatedPieceEl.remove();
-
-    // run complete function
-    if (typeof completeFn === 'function') {
-      completeFn();
+// execute an array of animations
+function doAnimations(a, oldPos, newPos) {
+  let fadeout_pieces = [];
+  let fadein_pieces = [];
+  let move_pieces = [];
+  let squares_to_clear = [];
+  let squares_to_fill = {};
+  let removed_pieces = [];
+
+  for (var i = 0; i < a.length; i++) {
+    // clear a piece
+    if (a[i].type === 'clear') {
+      let square = a[i].square;
+      let piece = PIECE_ON_SQUARE[square];
+      if (piece) {
+        fadeout_pieces.push(piece);
+        squares_to_clear.push(square);
+        removed_pieces.push(piece);
+      }
     }
-  };
-
-  // animate the piece to the destination square
-  // FIXME: support this for non-jquery
-  var opts = {
-    duration: cfg.moveSpeed,
-    complete: complete
-  };
-  //$(animatedPieceEl).animate(destOffset, opts);
-}
 
-function fadeIn(pieces, onFinish) {
-  pieces.forEach((piece) => {
-    piece.style.opacity = 0;
-    piece.style.display = null;
-    piece.addEventListener('transitionend', onFinish, {once: true});
-  });
-  requestAnimationFrame(() => {
-    pieces.forEach((piece) => {
-      piece.style.transitionProperty = 'opacity';
-      piece.style.transitionDuration = cfg.appearSpeed + 'ms';
-      piece.style.opacity = 1;
-    });
-  });
-}
+    // add a piece
+    if (a[i].type === 'add') {
+      let square = a[i].square;
+      let pos = findSquarePosition(square);
+      let piece = buildPiece(a[i].piece, true);
+      piece.style.left = pos.left;
+      piece.style.top = pos.top;
+      boardEl.append(piece);
+      squares_to_fill[square] = piece;
+      fadein_pieces.push(piece);
+    }
 
-function fadeOut(pieces, onFinish) {
-  pieces.forEach((piece) => {
-    piece.style.opacity = 1;
-    piece.style.display = null;
-    piece.addEventListener('transitionend', onFinish, {once: true});
-  });
-  requestAnimationFrame(() => {
-    pieces.forEach((piece) => {
-      piece.style.transitionProperty = 'opacity';
-      piece.style.transitionDuration = cfg.trashSpeed + 'ms';
-      piece.style.opacity = 0;
-    });
-  });
-}
+    // move a piece
+    if (a[i].type === 'move') {
+      let piece = PIECE_ON_SQUARE[a[i].source];
+      move_pieces.push([piece, a[i].destination]);
+      squares_to_clear.push(a[i].source);
+      squares_to_fill[a[i].destination] = piece;
+
+      // This is O(n²), but OK.
+      let replaced_piece = PIECE_ON_SQUARE[a[i].destination];
+      if (replaced_piece && !a.some(e => e.type === 'move' && e.source === a[i].destination)) {
+        removed_pieces.push(replaced_piece);
+      }
+    }
+  }
 
-// execute an array of animations
-function doAnimations(a, oldPos, newPos) {
-  ANIMATION_HAPPENING = true;
+  for (const square of squares_to_clear) {
+    delete PIECE_ON_SQUARE[square];
+  }
+  for (const [square, piece] of Object.entries(squares_to_fill)) {
+    PIECE_ON_SQUARE[square] = piece;
+    piece.setAttribute('data-square', square);
+  }
 
   var numFinished = 0;
-  function onFinish(e) {
+  function onFinish(e, opt_force) {
     if (e && e.target) {
       e.target.transitionProperty = null;
+      e.target.transitionDuration = null;
     }
 
-    numFinished++;
-
-    // exit if all the animations aren't finished
-    if (numFinished !== a.length) return;
+    if (opt_force) {
+      // For whatever reason, the transition didn't seem to actually run
+      // (and thus, we didn't get an end event). Finish off now.
+      // (In particular, this seems to happen in Chrome if the tab is
+      // hidden and left alone for a while.)
+      if (numFinished == a.length) return;
+      numFinished = a.length;  // Make sure a very delayed later event will be ignored.
+    } else {
+      numFinished++;
+      if (numFinished !== a.length) return;
+    }
 
-    drawPositionInstant();
-    ANIMATION_HAPPENING = false;
+    for (let piece of removed_pieces) {
+      piece.remove();
+    }
 
     // run their onMoveEnd function
-    if (cfg.hasOwnProperty('onMoveEnd') === true &&
+    if (cfg.hasOwnProperty('onMoveEnd') &&
       typeof cfg.onMoveEnd === 'function') {
       cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos));
     }
   }
 
-  requestAnimationFrame(() => {  // Firefox workaround.
-    let fadeout_pieces = [];
-    let fadein_pieces = [];
-
-    for (var i = 0; i < a.length; i++) {
-      // clear a piece
-      if (a[i].type === 'clear') {
-        document.getElementById(SQUARE_ELS_IDS[a[i].square]).querySelectorAll('.' + CSS.piece).forEach(
-          (piece) => fadeout_pieces.push(piece)
-        );
-      }
+  if (fadeout_pieces.length == 0 || fadein_pieces.length > 0 || move_pieces.length > 0) {
+    requestAnimationFrame(() => {  // Firefox workaround.
+      // Backup in case the transition never runs.
+      setTimeout(() => onFinish(null, true),
+                 cfg.appearSpeed + cfg.trashSpeed + cfg.moveSpeed + 100);
 
-      // add a piece (no spare pieces)
-      if (a[i].type === 'add' && cfg.sparePieces !== true) {
-        let square = document.getElementById(SQUARE_ELS_IDS[a[i].square]);
-        square.append(buildPiece(a[i].piece, true));
-        let piece = square.querySelector('.' + CSS.piece);
-        fadein_pieces.push(piece);
-      }
-
-      // add a piece from a spare piece
-      if (a[i].type === 'add' && cfg.sparePieces === true) {
-        animateSparePieceToSquare(a[i].piece, a[i].square, onFinish);
-      }
-
-      // move a piece
-      if (a[i].type === 'move') {
-        animateSquareToSquare(a[i].source, a[i].destination, a[i].piece,
-          onFinish);
+      fadein_pieces.forEach((piece) => {
+        piece.style.opacity = 0;
+        piece.style.display = null;
+        piece.addEventListener('transitionend', onFinish, {once: true});
+      });
+      fadeout_pieces.forEach((piece) => {
+        piece.style.opacity = 1;
+        piece.style.display = null;
+        piece.addEventListener('transitionend', onFinish, {once: true});
+      });
+      for (const [piece, destination] of move_pieces) {
+        // Move it to the end of the stack, which changes the implicit z-index
+        // so that it will go on top of any pieces it's replacing.
+        piece.remove();
+        boardEl.appendChild(piece);
+        piece.addEventListener('transitionend', onFinish, {once: true});
       }
-    }
-
-    // TODO: Batch moves as well, not just fade in/out.
-    // (We batch them because requestAnimationFrame seemingly costs real time.)
-    if (fadeout_pieces.length > 0) {
-      fadeOut(fadeout_pieces, onFinish);
-    }
-    if (fadein_pieces.length > 0) {
-      fadeIn(fadein_pieces, onFinish);
-    }
-  });
+      requestAnimationFrame(() => {
+        fadein_pieces.forEach((piece) => {
+          piece.style.transitionProperty = 'opacity';
+          piece.style.transitionDuration = cfg.appearSpeed + 'ms';
+          piece.style.opacity = 1;
+        });
+        fadeout_pieces.forEach((piece) => {
+          piece.style.transitionProperty = 'opacity';
+          piece.style.transitionDuration = cfg.trashSpeed + 'ms';
+          piece.style.opacity = 0;
+        });
+        for (const [piece, destination] of move_pieces) {
+          let destSquarePosition = findSquarePosition(destination);
+          piece.style.transitionProperty = 'top, left';
+          piece.style.transitionDuration = cfg.moveSpeed + 'ms';
+          piece.style.top = destSquarePosition.top;
+          piece.style.left = destSquarePosition.left;
+        }
+      });
+    });
+  }
 }
 
 // returns the distance between two squares
@@ -883,55 +699,26 @@ function squareDistance(s1, s2) {
   return yDelta;
 }
 
-// returns an array of closest squares from square
-function createRadius(square) {
-  var squares = [];
-
-  // calculate distance of all squares
-  for (var i = 0; i < 8; i++) {
-    for (var j = 0; j < 8; j++) {
-      var s = COLUMNS[i] + (j + 1);
-
-      // skip the square we're starting from
-      if (square === s) continue;
-
-      squares.push({
-        square: s,
-        distance: squareDistance(square, s)
-      });
-    }
-  }
-
-  // sort by distance
-  squares.sort(function(a, b) {
-    return a.distance - b.distance;
-  });
-
-  // just return the square code
-  var squares2 = [];
-  for (var i = 0; i < squares.length; i++) {
-    squares2.push(squares[i].square);
-  }
-
-  return squares2;
-}
-
 // returns the square of the closest instance of piece
 // returns false if no instance of piece is found in position
 function findClosestPiece(position, piece, square) {
-  // create array of closest squares from square
-  var closestSquares = createRadius(square);
-
-  // search through the position in order of distance for the piece
-  for (var i = 0; i < closestSquares.length; i++) {
-    var s = closestSquares[i];
+  let best_square = false;
+  let best_dist = 1e9;
+  for (var i = 0; i < COLUMNS.length; i++) {
+    for (var j = 1; j <= 8; j++) {
+      let other_square = COLUMNS[i] + j;
 
-    if (position.hasOwnProperty(s) === true && position[s] === piece) {
-      return s;
+      if (position[other_square] === piece && square != other_square) {
+        let dist = squareDistance(square, other_square);
+        if (dist < best_dist) {
+          best_square = other_square;
+          best_dist = dist;
+        }
+      }
     }
   }
 
-  return false;
+  return best_square;
 }
 
 // calculate an array of animations that need to happen in order to get
@@ -946,9 +733,9 @@ function calculateAnimations(pos1, pos2) {
 
   // remove pieces that are the same in both positions
   for (var i in pos2) {
-    if (pos2.hasOwnProperty(i) !== true) continue;
+    if (!pos2.hasOwnProperty(i)) continue;
 
-    if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) {
+    if (pos1.hasOwnProperty(i) && pos1[i] === pos2[i]) {
       delete pos1[i];
       delete pos2[i];
     }
@@ -956,7 +743,7 @@ function calculateAnimations(pos1, pos2) {
 
   // find all the "move" animations
   for (var i in pos2) {
-    if (pos2.hasOwnProperty(i) !== true) continue;
+    if (!pos2.hasOwnProperty(i)) continue;
 
     var closestPiece = findClosestPiece(pos1, pos2[i], i);
     if (closestPiece !== false) {
@@ -975,7 +762,7 @@ function calculateAnimations(pos1, pos2) {
 
   // add pieces to pos2
   for (var i in pos2) {
-    if (pos2.hasOwnProperty(i) !== true) continue;
+    if (!pos2.hasOwnProperty(i)) continue;
 
     animations.push({
       type: 'add',
@@ -988,11 +775,11 @@ function calculateAnimations(pos1, pos2) {
 
   // clear pieces from pos1
   for (var i in pos1) {
-    if (pos1.hasOwnProperty(i) !== true) continue;
+    if (!pos1.hasOwnProperty(i)) continue;
 
     // do not clear a piece if it is on a square that is the result
     // of a "move", ie: a piece capture
-    if (squaresMovedTo.hasOwnProperty(i) === true) continue;
+    if (squaresMovedTo.hasOwnProperty(i)) continue;
 
     animations.push({
       type: 'clear',
@@ -1015,27 +802,20 @@ function drawPositionInstant() {
   boardEl.querySelectorAll('.' + CSS.piece).forEach((piece) => piece.remove());
 
   // add the pieces
-  for (var i in CURRENT_POSITION) {
-    if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue;
-
-    document.getElementById(SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i]));
+  for (const [square, piece] of Object.entries(CURRENT_POSITION)) {
+    let pos = findSquarePosition(square);
+    let pieceEl = buildPiece(piece);
+    pieceEl.style.left = pos.left;
+    pieceEl.style.top = pos.top;
+    pieceEl.setAttribute('data-square', square);
+    boardEl.append(pieceEl);
+    PIECE_ON_SQUARE[square] = pieceEl;
   }
 }
 
 function drawBoard() {
   boardEl.innerHTML = buildBoard(CURRENT_ORIENTATION);
   drawPositionInstant();
-
-  if (cfg.sparePieces === true) {
-    if (CURRENT_ORIENTATION === 'white') {
-      sparePiecesTopEl.innerHTML = buildSparePieces('black');
-      sparePiecesBottomEl.innerHTML = buildSparePieces('white');
-    }
-    else {
-      sparePiecesTopEl.innerHTML = buildSparePieces('white');
-      sparePiecesBottomEl.innerHTML = buildSparePieces('black');
-    }
-  }
 }
 
 // given a position and a set of moves, return a new position
@@ -1044,10 +824,10 @@ function calculatePositionFromMoves(position, moves) {
   position = deepCopy(position);
 
   for (var i in moves) {
-    if (moves.hasOwnProperty(i) !== true) continue;
+    if (!moves.hasOwnProperty(i)) continue;
 
     // skip the move if the position doesn't have a piece on the source square
-    if (position.hasOwnProperty(i) !== true) continue;
+    if (!position.hasOwnProperty(i)) continue;
 
     var piece = position[i];
     delete position[i];
@@ -1067,7 +847,7 @@ function setCurrentPosition(position) {
   if (oldFen === newFen) return;
 
   // run their onChange function
-  if (cfg.hasOwnProperty('onChange') === true &&
+  if (cfg.hasOwnProperty('onChange') &&
     typeof cfg.onChange === 'function') {
     cfg.onChange(oldPos, newPos);
   }
@@ -1076,31 +856,6 @@ function setCurrentPosition(position) {
   CURRENT_POSITION = position;
 }
 
-function isXYOnSquare(x, y) {
-  for (var i in SQUARE_ELS_OFFSETS) {
-    if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue;
-
-    var s = SQUARE_ELS_OFFSETS[i];
-    if (x >= s.left && x < s.left + SQUARE_SIZE &&
-        y >= s.top && y < s.top + SQUARE_SIZE) {
-      return i;
-    }
-  }
-
-  return 'offboard';
-}
-
-// records the XY coords of every square into memory
-function captureSquareOffsets() {
-  SQUARE_ELS_OFFSETS = {};
-
-  for (var i in SQUARE_ELS_IDS) {
-    if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue;
-
-    SQUARE_ELS_OFFSETS[i] = offset(document.getElementById(SQUARE_ELS_IDS[i]));
-  }
-}
-
 function removeSquareHighlights() {
   boardEl.querySelectorAll('.' + CSS.square).forEach((piece) => {
     piece.classList.remove(CSS.highlight1);
@@ -1109,21 +864,14 @@ function removeSquareHighlights() {
 }
 
 function snapbackDraggedPiece() {
-  // there is no "snapback" for spare pieces
-  if (DRAGGED_PIECE_SOURCE === 'spare') {
-    trashDraggedPiece();
-    return;
-  }
-
   removeSquareHighlights();
 
   // animation complete
   function complete() {
     drawPositionInstant();
-    draggedPieceEl.style.display = 'none';
 
     // run their onSnapbackEnd function
-    if (cfg.hasOwnProperty('onSnapbackEnd') === true &&
+    if (cfg.hasOwnProperty('onSnapbackEnd') &&
       typeof cfg.onSnapbackEnd === 'function') {
       cfg.onSnapbackEnd(DRAGGED_PIECE, DRAGGED_PIECE_SOURCE,
         deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION);
@@ -1131,60 +879,52 @@ function snapbackDraggedPiece() {
   }
 
   // get source square position
-  var sourceSquarePosition =
-    offset(document.getElementById(SQUARE_ELS_IDS[DRAGGED_PIECE_SOURCE]));
+  var sourceSquarePosition = findSquarePosition(DRAGGED_PIECE_SOURCE);
 
   // animate the piece to the target square
-  draggedPieceEl.addEventListener('transitionend', complete, {once: true});
+  DRAGGED_PIECE.addEventListener('transitionend', complete, {once: true});
   requestAnimationFrame(() => {
-    draggedPieceEl.style.transitionProperty = 'top, left';
-    draggedPieceEl.style.transitionDuration = cfg.snapbackSpeed + 'ms';
-    draggedPieceEl.style.top = sourceSquarePosition.top + 'px';
-    draggedPieceEl.style.left = sourceSquarePosition.left + 'px';
+    DRAGGED_PIECE.style.transitionProperty = 'top, left';
+    DRAGGED_PIECE.style.transitionDuration = cfg.snapbackSpeed + 'ms';
+    DRAGGED_PIECE.style.top = sourceSquarePosition.top;
+    DRAGGED_PIECE.style.left = sourceSquarePosition.left;
   });
 
   // set state
   DRAGGING_A_PIECE = false;
 }
 
-function trashDraggedPiece() {
+function dropDraggedPieceOnSquare(square) {
   removeSquareHighlights();
-
-  // remove the source piece
-  var newPosition = deepCopy(CURRENT_POSITION);
-  delete newPosition[DRAGGED_PIECE_SOURCE];
-  setCurrentPosition(newPosition);
-
-  // redraw the position
-  drawPositionInstant();
-
-  // hide the dragged piece
-  // FIXME: support this for non-jquery
-  //$(draggedPieceEl).fadeOut(cfg.trashSpeed);
-
-  // set state
   DRAGGING_A_PIECE = false;
-}
 
-function dropDraggedPieceOnSquare(square) {
-  removeSquareHighlights();
+  if (DRAGGED_PIECE_SOURCE === square) {
+    // Nothing to do, but call onSnapEnd anyway
+    if (cfg.hasOwnProperty('onSnapEnd') && typeof cfg.onSnapEnd === 'function') {
+      cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE);
+    }
+    return;
+  }
 
   // update position
   var newPosition = deepCopy(CURRENT_POSITION);
+  newPosition[square] = newPosition[DRAGGED_PIECE_SOURCE];
   delete newPosition[DRAGGED_PIECE_SOURCE];
-  newPosition[square] = DRAGGED_PIECE;
   setCurrentPosition(newPosition);
 
+  delete PIECE_ON_SQUARE[DRAGGED_PIECE_SOURCE];
+  PIECE_ON_SQUARE[square] = DRAGGED_PIECE;
+  DRAGGED_PIECE.setAttribute('data-square', square);
+
   // get target square information
-  var targetSquarePosition = offset(document.getElementById(SQUARE_ELS_IDS[square]));
+  var targetSquarePosition = findSquarePosition(square);
 
   // animation complete
   var complete = function() {
     drawPositionInstant();
-    draggedPieceEl.style.display = 'none';
 
     // execute their onSnapEnd function
-    if (cfg.hasOwnProperty('onSnapEnd') === true &&
+    if (cfg.hasOwnProperty('onSnapEnd') &&
       typeof cfg.onSnapEnd === 'function') {
       requestAnimationFrame(() => {  // HACK: so that we don't add event handlers from the callback...
         cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE);
@@ -1193,16 +933,13 @@ function dropDraggedPieceOnSquare(square) {
   };
 
   // snap the piece to the target square
-  draggedPieceEl.addEventListener('transitionend', complete, {once: true});
+  DRAGGED_PIECE.addEventListener('transitionend', complete, {once: true});
   requestAnimationFrame(() => {
-    draggedPieceEl.style.transitionProperty = 'top, left';
-    draggedPieceEl.style.transitionDuration = cfg.snapSpeed + 'ms';
-    draggedPieceEl.style.top = targetSquarePosition.top + 'px';
-    draggedPieceEl.style.left = targetSquarePosition.left + 'px';
+    DRAGGED_PIECE.style.transitionProperty = 'top, left';
+    DRAGGED_PIECE.style.transitionDuration = cfg.snapSpeed + 'ms';
+    DRAGGED_PIECE.style.top = targetSquarePosition.top;
+    DRAGGED_PIECE.style.left = targetSquarePosition.left;
   });
-
-  // set state
-  DRAGGING_A_PIECE = false;
 }
 
 function beginDraggingPiece(source, piece, x, y) {
@@ -1216,55 +953,62 @@ function beginDraggingPiece(source, piece, x, y) {
 
   // set state
   DRAGGING_A_PIECE = true;
-  DRAGGED_PIECE = piece;
+  DRAGGED_PIECE = PIECE_ON_SQUARE[source];
   DRAGGED_PIECE_SOURCE = source;
+  DRAGGED_PIECE_LOCATION = source;
+  DRAGGED_PIECE.style.transitionProperty = null;
+  DRAGGED_PIECE.style.transitionDuration = null;
+
+  // Move it to the end of the stack, which changes the implicit z-index
+  // so that it will go on top of any pieces it's replacing.
+  DRAGGED_PIECE.remove();
+  boardEl.appendChild(DRAGGED_PIECE);
+
+  // highlight the source square
+  let square = document.querySelector('.' + CSS.square + '[data-square="' + source + '"]');
+  square.classList.add(CSS.highlight1);
+}
 
-  // if the piece came from spare pieces, location is offboard
-  if (source === 'spare') {
-    DRAGGED_PIECE_LOCATION = 'offboard';
-  }
-  else {
-    DRAGGED_PIECE_LOCATION = source;
-  }
-
-  // capture the x, y coords of all squares in memory
-  captureSquareOffsets();
-
-  // create the dragged piece
-  draggedPieceEl.setAttribute('src', buildPieceImgSrc(piece));
-  draggedPieceEl.style.display = null;
-  draggedPieceEl.style.position = 'absolute';
-  draggedPieceEl.style.left = (x - (SQUARE_SIZE / 2)) + 'px';
-  draggedPieceEl.style.top = (y - (SQUARE_SIZE / 2)) + 'px';
+function findSquareFromEvent(pageX, pageY) {
+  let o = offset(boardEl);
+  let x = pageX - o.left;
+  let y = pageY - o.top;
 
-  if (source !== 'spare') {
-    // highlight the source square and hide the piece
-    let square = document.getElementById(SQUARE_ELS_IDS[source]);
-    square.classList.add(CSS.highlight1);
-    square.querySelector('.' + CSS.piece).style.display = 'none';
+  let position = {
+    x: x,
+    y: y,
+    left: Math.floor(x * 8 / boardEl.getBoundingClientRect().width),
+    top: Math.floor(y * 8 / boardEl.getBoundingClientRect().width)
+  };
+  if (CURRENT_ORIENTATION === 'white') {
+    position.top = 7 - position.top;
+  }
+  if (position.left >= 0 && position.left < 8 && position.top >= 0 && position.top < 8) {
+    position.square = COLUMNS[position.left] + (position.top + 1);
+  } else {
+    position.square = 'offboard';
   }
+  return position;
 }
 
-function updateDraggedPiece(x, y) {
+function updateDraggedPiece(position) {
   // put the dragged piece over the mouse cursor
-  draggedPieceEl.style.left = (x - (SQUARE_SIZE / 2)) + 'px';
-  draggedPieceEl.style.top = (y - (SQUARE_SIZE / 2)) + 'px';
-
-  // get location
-  var location = isXYOnSquare(x, y);
+  DRAGGED_PIECE.style.left = 'calc(' + position.x + 'px - 6.25%)';
+  DRAGGED_PIECE.style.top = 'calc(' + position.y + 'px - 6.25%)';
 
   // do nothing if the location has not changed
-  if (location === DRAGGED_PIECE_LOCATION) return;
+  if (position === DRAGGED_PIECE_LOCATION) return;
 
   // remove highlight from previous square
-  if (validSquare(DRAGGED_PIECE_LOCATION) === true) {
-    document.getElementById(SQUARE_ELS_IDS[DRAGGED_PIECE_LOCATION])
+  if (validSquare(DRAGGED_PIECE_LOCATION)) {
+    document.querySelector('.' + CSS.square + '[data-square="' + DRAGGED_PIECE_LOCATION + '"]')
       .classList.remove(CSS.highlight2);
   }
 
   // add highlight to new square
-  if (validSquare(location) === true) {
-    document.getElementById(SQUARE_ELS_IDS[location]).classList.add(CSS.highlight2);
+  if (validSquare(position.square)) {
+    document.querySelector('.' + CSS.square + '[data-square="' + position.square + '"]')
+      .classList.add(CSS.highlight2);
   }
 
   // run onDragMove
@@ -1275,53 +1019,39 @@ function updateDraggedPiece(x, y) {
   }
 
   // update state
-  DRAGGED_PIECE_LOCATION = location;
+  DRAGGED_PIECE_LOCATION = position.square;
 }
 
 function stopDraggedPiece(location) {
   // determine what the action should be
   var action = 'drop';
-  if (location === 'offboard' && cfg.dropOffBoard === 'snapback') {
+  if (location.square === 'offboard' && cfg.dropOffBoard === 'snapback') {
     action = 'snapback';
   }
-  if (location === 'offboard' && cfg.dropOffBoard === 'trash') {
-    action = 'trash';
-  }
 
   // run their onDrop function, which can potentially change the drop action
-  if (cfg.hasOwnProperty('onDrop') === true &&
+  if (cfg.hasOwnProperty('onDrop') &&
     typeof cfg.onDrop === 'function') {
     var newPosition = deepCopy(CURRENT_POSITION);
 
-    // source piece is a spare piece and position is off the board
-    //if (DRAGGED_PIECE_SOURCE === 'spare' && location === 'offboard') {...}
-    // position has not changed; do nothing
-
-    // source piece is a spare piece and position is on the board
-    if (DRAGGED_PIECE_SOURCE === 'spare' && validSquare(location) === true) {
-      // add the piece to the board
-      newPosition[location] = DRAGGED_PIECE;
-    }
-
-    // source piece was on the board and position is off the board
-    if (validSquare(DRAGGED_PIECE_SOURCE) === true && location === 'offboard') {
-      // remove the piece from the board
-      delete newPosition[DRAGGED_PIECE_SOURCE];
-    }
-
     // source piece was on the board and position is on the board
-    if (validSquare(DRAGGED_PIECE_SOURCE) === true &&
-      validSquare(location) === true) {
+    if (validSquare(DRAGGED_PIECE_SOURCE) &&
+      validSquare(location.square)) {
       // move the piece
       delete newPosition[DRAGGED_PIECE_SOURCE];
-      newPosition[location] = DRAGGED_PIECE;
+      newPosition[location.square] = DRAGGED_PIECE;
+      if (location.square !== DRAGGED_PIECE_SOURCE) {
+        PIECE_ON_SQUARE[location.square] = PIECE_ON_SQUARE[DRAGGED_PIECE_SOURCE];
+        DRAGGED_PIECE.setAttribute('data-square', location.square);
+        delete PIECE_ON_SQUARE[DRAGGED_PIECE_SOURCE];
+      }
     }
 
     var oldPosition = deepCopy(CURRENT_POSITION);
 
-    var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location, DRAGGED_PIECE,
+    var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location.square, DRAGGED_PIECE,
       newPosition, oldPosition, CURRENT_ORIENTATION);
-    if (result === 'snapback' || result === 'trash') {
+    if (result === 'snapback') {
       action = result;
     }
   }
@@ -1330,11 +1060,8 @@ function stopDraggedPiece(location) {
   if (action === 'snapback') {
     snapbackDraggedPiece();
   }
-  else if (action === 'trash') {
-    trashDraggedPiece();
-  }
   else if (action === 'drop') {
-    dropDraggedPieceOnSquare(location);
+    dropDraggedPieceOnSquare(location.square);
   }
 }
 
@@ -1399,7 +1126,7 @@ widget.move = function() {
     }
 
     // skip invalid arguments
-    if (validMove(arguments[i]) !== true) {
+    if (!validMove(arguments[i])) {
       error(2826, 'Invalid move passed to the move method.', arguments[i]);
       continue;
     }
@@ -1467,17 +1194,17 @@ widget.position = function(position, useAnimation) {
   }
 
   // convert FEN to position object
-  if (validFen(position) === true) {
+  if (validFen(position)) {
     position = fenToObj(position);
   }
 
   // validate position object
-  if (validPositionObject(position) !== true) {
+  if (!validPositionObject(position)) {
     error(6482, 'Invalid value passed to the position method.', position);
     return;
   }
 
-  if (useAnimation === true) {
+  if (useAnimation) {
     // start the animations
     doAnimations(calculateAnimations(CURRENT_POSITION, position),
       CURRENT_POSITION, position);
@@ -1493,24 +1220,6 @@ widget.position = function(position, useAnimation) {
 };
 
 widget.resize = function() {
-  // calulate the new square size
-  SQUARE_SIZE = calculateSquareSize();
-
-  // set board width
-  boardEl.style.width = (SQUARE_SIZE * 8) + 'px';
-
-  // set drag piece size
-  if (draggedPieceEl !== null) {
-    draggedPieceEl.style.height = SQUARE_SIZE + 'px';
-    draggedPieceEl.style.width = SQUARE_SIZE + 'px';
-  }
-
-  // spare pieces
-  if (cfg.sparePieces === true) {
-    containerEl.querySelector('.' + CSS.sparePieces)
-      .style.paddingLeft = (SQUARE_SIZE + BOARD_BORDER_SIZE) + 'px';
-  }
-
   // redraw the board
   drawBoard();
 };
@@ -1529,22 +1238,17 @@ function isTouchDevice() {
 }
 
 function mousedownSquare(e) {
-  let target = e.target.closest('.' + CSS.square);
-  if (!target) {
-    return;
-  }
-
-  // do nothing if we're not draggable
-  if (cfg.draggable !== true) return;
-
-  var square = target.getAttribute('data-square');
+  let square = e.target.getAttribute('data-square');
 
   // no piece on this square
-  if (validSquare(square) !== true ||
-      CURRENT_POSITION.hasOwnProperty(square) !== true) {
+  if (!validSquare(square) ||
+      !CURRENT_POSITION.hasOwnProperty(square)) {
     return;
   }
 
+  // do nothing if we're not draggable
+  if (!cfg.draggable) return;
+
   beginDraggingPiece(square, CURRENT_POSITION[square], e.pageX, e.pageY);
 }
 
@@ -1555,13 +1259,13 @@ function touchstartSquare(e) {
   }
 
   // do nothing if we're not draggable
-  if (cfg.draggable !== true) return;
+  if (!cfg.draggable) return;
 
   var square = target.getAttribute('data-square');
 
   // no piece on this square
-  if (validSquare(square) !== true ||
-      CURRENT_POSITION.hasOwnProperty(square) !== true) {
+  if (!validSquare(square) ||
+      !CURRENT_POSITION.hasOwnProperty(square)) {
     return;
   }
 
@@ -1569,70 +1273,37 @@ function touchstartSquare(e) {
     e.changedTouches[0].pageX, e.changedTouches[0].pageY);
 }
 
-function mousedownSparePiece(e) {
-  if (!e.target.matches('.' + CSS.sparePieces + ' .' + CSS.piece)) {
-    return;
-  }
-
-  // do nothing if sparePieces is not enabled
-  if (cfg.sparePieces !== true) return;
-
-  var piece = e.target.getAttribute('data-piece');
-
-  beginDraggingPiece('spare', piece, e.pageX, e.pageY);
-}
-
-function touchstartSparePiece(e) {
-  if (!e.target.matches('.' + CSS.sparePieces + ' .' + CSS.piece)) {
-    return;
-  }
-
-  // do nothing if sparePieces is not enabled
-  if (cfg.sparePieces !== true) return;
-
-  var piece = e.target.getAttribute('data-piece');
-
-  beginDraggingPiece('spare', piece,
-    e.changedTouches[0].pageX, e.changedTouches[0].pageY);
-}
-
 function mousemoveWindow(e) {
   // do nothing if we are not dragging a piece
-  if (DRAGGING_A_PIECE !== true) return;
+  if (!DRAGGING_A_PIECE) return;
 
-  updateDraggedPiece(e.pageX, e.pageY);
+  updateDraggedPiece(findSquareFromEvent(e.pageX, e.pageY));
 }
 
 function touchmoveWindow(e) {
   // do nothing if we are not dragging a piece
-  if (DRAGGING_A_PIECE !== true) return;
+  if (!DRAGGING_A_PIECE) return;
 
   // prevent screen from scrolling
   e.preventDefault();
 
-  updateDraggedPiece(e.changedTouches[0].pageX,
-    e.changedTouches[0].pageY);
+  updateDraggedPiece(findSquareFromEvent(e.changedTouches[0].pageX,
+    e.changedTouches[0].pageY));
 }
 
 function mouseupWindow(e) {
   // do nothing if we are not dragging a piece
-  if (DRAGGING_A_PIECE !== true) return;
+  if (!DRAGGING_A_PIECE) return;
 
-  // get the location
-  var location = isXYOnSquare(e.pageX, e.pageY);
-
-  stopDraggedPiece(location);
+  stopDraggedPiece(findSquareFromEvent(e.pageX, e.pageY));
 }
 
 function touchendWindow(e) {
   // do nothing if we are not dragging a piece
-  if (DRAGGING_A_PIECE !== true) return;
-
-  // get the location
-  var location = isXYOnSquare(e.changedTouches[0].pageX,
-    e.changedTouches[0].pageY);
+  if (!DRAGGING_A_PIECE) return;
 
-  stopDraggedPiece(location);
+  stopDraggedPiece(findSquareFromEvent(e.changedTouches[0].pageX,
+    e.changedTouches[0].pageY));
 }
 
 function mouseenterSquare(e) {
@@ -1645,18 +1316,18 @@ function mouseenterSquare(e) {
   // NOTE: this should never happen, but it's a safeguard
   if (DRAGGING_A_PIECE !== false) return;
 
-  if (cfg.hasOwnProperty('onMouseoverSquare') !== true ||
+  if (!cfg.hasOwnProperty('onMouseoverSquare') ||
     typeof cfg.onMouseoverSquare !== 'function') return;
 
   // get the square
   var square = target.getAttribute('data-square');
 
   // NOTE: this should never happen; defensive
-  if (validSquare(square) !== true) return;
+  if (!validSquare(square)) return;
 
   // get the piece on this square
   var piece = false;
-  if (CURRENT_POSITION.hasOwnProperty(square) === true) {
+  if (CURRENT_POSITION.hasOwnProperty(square)) {
     piece = CURRENT_POSITION[square];
   }
 
@@ -1675,18 +1346,18 @@ function mouseleaveSquare(e) {
   // NOTE: this should never happen, but it's a safeguard
   if (DRAGGING_A_PIECE !== false) return;
 
-  if (cfg.hasOwnProperty('onMouseoutSquare') !== true ||
+  if (!cfg.hasOwnProperty('onMouseoutSquare') ||
     typeof cfg.onMouseoutSquare !== 'function') return;
 
   // get the square
   var square = target.getAttribute('data-square');
 
   // NOTE: this should never happen; defensive
-  if (validSquare(square) !== true) return;
+  if (!validSquare(square)) return;
 
   // get the piece on this square
   var piece = false;
-  if (CURRENT_POSITION.hasOwnProperty(square) === true) {
+  if (CURRENT_POSITION.hasOwnProperty(square)) {
     piece = CURRENT_POSITION[square];
   }
 
@@ -1711,7 +1382,6 @@ function addEvents() {
 
   // mouse drag pieces
   boardEl.addEventListener('mousedown', mousedownSquare);
-  containerEl.addEventListener('mousedown', mousedownSparePiece);
 
   // mouse enter / leave square
   boardEl.addEventListener('mouseenter', mouseenterSquare);
@@ -1721,9 +1391,8 @@ function addEvents() {
   window.addEventListener('mouseup', mouseupWindow);
 
   // touch drag pieces
-  if (isTouchDevice() === true) {
+  if (isTouchDevice()) {
     boardEl.addEventListener('touchstart', touchstartSquare);
-    containerEl.addEventListener('touchstart', touchstartSparePiece);
     window.addEventListener('touchmove', touchmoveWindow);
     window.addEventListener('touchend', touchendWindow);
   }
@@ -1734,29 +1403,12 @@ function initDom() {
   containerEl.innerHTML = buildBoardContainer();
   boardEl = containerEl.querySelector('.' + CSS.board);
 
-  if (cfg.sparePieces === true) {
-    sparePiecesTopEl = containerEl.querySelector('.' + CSS.sparePiecesTop);
-    sparePiecesBottomEl = containerEl.querySelector('.' + CSS.sparePiecesBottom);
-  }
-
-  // create the drag piece
-  var draggedPieceId = createId();
-  document.body.append(buildPiece('wP', true, draggedPieceId));
-  draggedPieceEl = document.getElementById(draggedPieceId);
-
-  // get the border size
-  BOARD_BORDER_SIZE = parseInt(boardEl.style.borderLeftWidth, 10);
-
   // set the size and draw the board
   widget.resize();
 }
 
 function init() {
-  if (checkDeps() !== true ||
-      expandConfig() !== true) return;
-
-  // create unique IDs for all the elements we will create
-  createElIds();
+  if (!checkDeps() || !expandConfig()) return;
 
   initDom();
   addEvents();