User:קיפודנחש/chess-animator.js

/* this code was written entirely by User:קיפודנחש it is placed by the author in the public domain - you can take it, use it, abuse it, modify it etc. in any way you want. no attribution or acknowledgement required. as usual - no warranty or guarantee comes with it - use it at your own risk

$(function {

var allPositionClasses = '01234567' .split('') .map(function(r) { return 'pgn-prow-' + r + ' pgn-pfile-' + r; } ) .join(' ');

function processOneDiv { var $div = $(this), data = $div.data('chess'), boards, pieces = [], timer, delay = 800, boardDiv = $div.find('.pgn-board-img'), ctx, currentPlyNum;

function createPiece(letter) { var ll = letter.toLowerCase, color = letter == ll ? 'd' : 'l', piece = $(' ') .data('piece', letter) .addClass('pgn-chessPiece pgn-ptype-color-' + ll + color) .appendTo(boardDiv); pieces.push(piece); return piece; }

function processFen(fen) { var fenAr = fen.split('/'), board = [], l; for (var i in fenAr) { var j = 0; letters = fenAr[i].split(''); for (var li in letters) { l = letters[li]; if (/[prnbqk]/i.test(l)) { board[(7 - i) * 8 + j] = createPiece(l); j++; }				else j += parseInt(l); }		}		return board; }

function processPly(board, ply) { var newBoard = board.slice, source = ply[0], destination = ply[1], special = ply[2]; if (typeof(source) == typeof(ply)) { // castling. 2 source/dest pairs newBoard = processPly(newBoard, source); newBoard = processPly(newBoard, destination); } else { newBoard[destination] = newBoard[source]; delete newBoard[source]; if (special) { if (typeof(special) == "string") newBoard[destination] = createPiece(special); // promotion else delete newBoard[special]; // en passant }		}		return newBoard; }

function scrollNotationToView(notation) { var daddy = notation.closest('.pgn-notations'), daddysHeight = daddy.height, notationHeight = notation.height, notationTop = notation.position.top, toMove; if ( notationTop < 0 || notationTop + notationHeight > daddysHeight ) { toMove = (daddysHeight - notationHeight) / 2 - notationTop, scrollTop = daddy.prop('scrollTop'), newScrolltop = scrollTop - toMove; daddy.prop({scrollTop: newScrolltop}); }	}   function placeInSquare(element, square) { element .removeClass(allPositionClasses + ' pgn-piece-hidden') .addClass('pgn-prow-' + parseInt(square / 8) + ' pgn-pfile-' + square % 8 ); }

function resetCanvas { ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, 800, 800); }   function drawArrow(source, dest) { if (!ctx) return; resetCanvas; var x1 = 100 * (source % 8), y1 = 100 * parseInt(source / 8), x2 = 100 * (dest % 8), y2 = 100 * (parseInt(dest / 8)); if ($div.hasClass('pgn-flip')) { x1 = 700 - x1; x2 = 700 - x2; y1 = 700 - y1; y2 = 700 - y2; }        var dx = x2 - x1, dy = y2 - y1, len = Math.sqrt(dx*dx + dy*dy), angle = Math.atan2(dy, -dx), shape = [ [len, 20], [len, -20], [80, -20], [80, -50], [0, 0], [80, 50], [80, 20] ], point = shape.shift; gradient = ctx.createLinearGradient(0, 0, len, 0); gradient.addColorStop(0, 'rgba(80,80,0,0.7');       gradient.addColorStop(1, 'rgba(80,80,0,0.3'); ctx.fillStyle = gradient;

// start by reseting current transformation matrix to the identity matrix, and clear ctx.rotate(angle); var x2t = x2 + 50, y2t = 750 - y2, xt = Math.cos(angle) * x2t + Math.sin(angle) * y2t, yt = Math.cos(angle) * y2t - Math.sin(angle) * x2t; ctx.translate(xt, yt); ctx.beginPath; ctx.moveTo(point[0], point[1]); while ( point = shape.shift ) ctx.lineTo(point[0], point[1]); ctx.closePath; ctx.fill; }

function showPly { var showMarkers = currentPlyNum > 0 && !timer; $('.pgn-ply-source, .pgn-ply-destination', $div) .toggleClass('pgn-piece-hidden', !showMarkers); if (showMarkers) { var ply = data.plys[currentPlyNum - 1]; if (ply && typeof(ply[0]) == typeof(ply)) ply = ply[0]; // castling var source = ply[0], dest = ply[1]; placeInSquare($('.pgn-ply-source', $div), source); boards[currentPlyNum][dest].addClass('pgn-ply-destination'); drawArrow(source, dest); } else { if (ctx) resetCanvas; }   }

function gotoBoard(plyNum) { var previous = currentPlyNum, board = boards[plyNum], hidden = pieces.filter(function(piece) { return $.inArray(piece, board) == -1; }), appearNow = board.filter(function(piece) { 				return typeof(previous) == 'number' && boards[previous].indexOf(piece) === -1; 			} ), notation; currentPlyNum = plyNum; for (var i in hidden) hidden[i].addClass('pgn-piece-hidden'); for (var j in board) { board[j] .removeClass(allPositionClasses + ' pgn-piece-hidden pgn-ply-destination') .toggleClass('pgn-transition-immediate', appearNow.indexOf(board[j]) > -1) .addClass('pgn-prow-' + parseInt(j / 8) + ' pgn-pfile-' + j % 8 ); }		if (plyNum == boards.length - 1) stopAutoplay; $('.pgn-movelink', $div).removeClass('pgn-current-move'); notation = $('.pgn-movelink[data-ply=' + plyNum + ']', $div); if (notation.length) { notation.addClass('pgn-current-move'); scrollNotationToView(notation); }		showPly; }

function advance { if (currentPlyNum < boards.length - 1) gotoBoard(currentPlyNum + 1); }

function retreat { if (currentPlyNum > 0) gotoBoard(currentPlyNum - 1); }

function gotoStart { gotoBoard(0); stopAutoplay; }

function gotoEnd { gotoBoard(boards.length - 1); }

function clickPlay { if (currentPlyNum == boards.length - 1) gotoBoard(0); if (timer) stopAutoplay; else startAutoplay; }

function changeDelay { if (delay < 400) delay = 400; if (timer) { stopAutoplay; startAutoplay; }	}

function slower { delay += Math.min(delay, 1600); changeDelay; }

function faster { delay = delay > 3200 ? delay - 1600 : delay / 2; changeDelay; }

function stopAutoplay { clearTimeout(timer); $('.pgn-button-play', $div).removeClass('pgn-image-button-on'); timer = null; showPly; }

function startAutoplay { timer = setInterval(advance, delay); $('.pgn-button-play', $div).addClass('pgn-image-button-on'); }

function flipBoard { $div.toggleClass('pgn-flip'); $('.pgn-button-flip', $div).toggleClass('pgn-image-button-on'); showPly; }	function clickNotation { stopAutoplay; gotoBoard($(this).data('ply')); }

function connectButtons { $('.pgn-button-advance', $div).click(advance); $('.pgn-button-retreat', $div).click(retreat); $('.pgn-button-tostart', $div).click(gotoStart); $('.pgn-button-toend', $div).click(gotoEnd); $('.pgn-button-play', $div).click(clickPlay); $('.pgn-button-faster', $div).click(faster); $('.pgn-button-slower', $div).click(slower); $('.pgn-button-flip', $div).click(flipBoard); $('.pgn-movelink', $div).click(clickNotation); }

if (data) { var board, ply, display = data.display || data.plys.length;

$div.find('.pgn-chessPiece:not(.pgn-ply-source)').remove; // the parser put its own pieces for "noscript" viewers board = processFen(data.fen); boards = [board]; for (var ind in data.plys) { ply = data.plys[ind]; board = processPly(board, ply); boards.push(board); }		connectButtons; gotoBoard(display); // call before canvas mambo-jumbo, so if it throws exception, we'll still be on the right ply. var canvas = $(' ') .css( { height: '100%', width: '100%' } ) .attr( { width: 800, height: 800 } ) .prependTo(boardDiv); ctx = canvas[0].getContext("2d"); $.extend(ctx, { fillStyle: 'rgba(0,0,0,0.55)' } ); showPly; // was already called once from gotoBoard, but canvas was not ready, so call again. } }

$('.pgnviewer').each(processOneDiv);

});