370 lines
11 KiB
JavaScript
370 lines
11 KiB
JavaScript
|
var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView = function(rows, columns) {
|
|||
|
var self = this;
|
|||
|
|
|||
|
this.FRAME_DELAY = 50;
|
|||
|
|
|||
|
this._rows = rows;
|
|||
|
this._columns = columns;
|
|||
|
|
|||
|
this._videoListURL = "video/videolist.json";
|
|||
|
this._videoList = null;
|
|||
|
|
|||
|
// DOM elements
|
|||
|
this._container = document.getElementById('slidepuzzle-container');
|
|||
|
this._board = document.getElementById('slidepuzzle-board');
|
|||
|
this._video = document.getElementById('slidepuzzle-video');
|
|||
|
this._copyright = document.getElementById('slidepuzzle-copyright');
|
|||
|
this._videoSelect = document.getElementById('slidepuzzle-videolist');
|
|||
|
this._timer = document.getElementById('slidepuzzle-timer');
|
|||
|
this._newGameButton = document.getElementById('slidepuzzle-newgame');
|
|||
|
this._undoButton = document.getElementById('slidepuzzle-undo');
|
|||
|
|
|||
|
// _origPosition is the unshuffled pieces, used for slicing the video.
|
|||
|
// Its order never changes during a game
|
|||
|
this._origPosition = [];
|
|||
|
|
|||
|
// _currPosition is the shuffled position of the pieces.
|
|||
|
this._currPosition = [];
|
|||
|
|
|||
|
this._updateInterval = null;
|
|||
|
|
|||
|
this._moving = false;
|
|||
|
this._listeners = {
|
|||
|
pieceClicked: [],
|
|||
|
undo: [],
|
|||
|
newGame: [],
|
|||
|
doneMoving: []
|
|||
|
};
|
|||
|
|
|||
|
// VideoView.moving
|
|||
|
this.__defineGetter__('moving', function() {
|
|||
|
return this._moving;
|
|||
|
});
|
|||
|
|
|||
|
// VideoView.timerText
|
|||
|
this.__defineGetter__('timerText', function() {
|
|||
|
return this._timer.textContent;
|
|||
|
});
|
|||
|
this.__defineSetter__('timerText', function(value) {
|
|||
|
this._timer.textContent = value;
|
|||
|
});
|
|||
|
|
|||
|
// VideoView.showEmptyPiece
|
|||
|
this.__defineGetter__('showEmptyPiece', function() {
|
|||
|
document.getElementById('slidepuzzle-empty-piece').classList.contains('hidden');
|
|||
|
});
|
|||
|
this.__defineSetter__('showEmptyPiece', function(value) {
|
|||
|
var classes = document.getElementById('slidepuzzle-empty-piece').classList;
|
|||
|
if (value) {
|
|||
|
classes.remove('hidden');
|
|||
|
} else {
|
|||
|
classes.add('hidden')
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// VideoView.canUndo
|
|||
|
this.__defineGetter__('canUndo', function() {
|
|||
|
return !this._undoButton.disabled;
|
|||
|
});
|
|||
|
this.__defineSetter__('canUndo', function(value) {
|
|||
|
this._undoButton.disabled = !value;
|
|||
|
});
|
|||
|
|
|||
|
// VideoView.canMovePieces
|
|||
|
this.canMovePieces = true;
|
|||
|
|
|||
|
// Create gameboard cells
|
|||
|
for (var i = 0; i < this._rows; i++) {
|
|||
|
var row = document.createElement('tr');
|
|||
|
for (var j = 0; j < this._columns; j++) {
|
|||
|
row.appendChild(document.createElement('td'));
|
|||
|
}
|
|||
|
this._board.appendChild(row);
|
|||
|
}
|
|||
|
|
|||
|
// Create puzzle pieces
|
|||
|
for (var i = 0; i < this._rows; i++) {
|
|||
|
this._currPosition.push([]);
|
|||
|
this._origPosition.push([]);
|
|||
|
for (var j = 0; j < this._columns; j++) {
|
|||
|
var piece = document.createElement('canvas');
|
|||
|
piece.dataset.correctRow = i;
|
|||
|
piece.dataset.correctColumn = j;
|
|||
|
if (i === this._rows - 1 && j === this._columns - 1) {
|
|||
|
piece.id = 'slidepuzzle-empty-piece';
|
|||
|
}
|
|||
|
this._currPosition[i].push(piece);
|
|||
|
this._origPosition[i].push(piece);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
this._placePieces();
|
|||
|
this.startUpdates();
|
|||
|
|
|||
|
function makePieceClickHandler(cellIndex) {
|
|||
|
var row = Math.floor(cellIndex / self._columns);
|
|||
|
var column = cellIndex % self._columns;
|
|||
|
var listener = function() {
|
|||
|
self._pieceClicked(row, column);
|
|||
|
}
|
|||
|
return listener;
|
|||
|
}
|
|||
|
|
|||
|
var cells = this._board.getElementsByTagName("td");
|
|||
|
for (var i = 0; i < cells.length; i++) {
|
|||
|
cells[i].addEventListener('click', makePieceClickHandler(i));
|
|||
|
}
|
|||
|
|
|||
|
this._videoSelect.addEventListener("change", function() {
|
|||
|
if (self._videoList) {
|
|||
|
console.log("selected video: " + self._videoSelect.value);
|
|||
|
var name = self._videoSelect.value;
|
|||
|
self.loadVideo(name);
|
|||
|
}
|
|||
|
});
|
|||
|
this._loadVideoList();
|
|||
|
|
|||
|
this._undoButton.addEventListener('click', function() {
|
|||
|
self._dispatchEvent('undo');
|
|||
|
});
|
|||
|
|
|||
|
this._newGameButton.addEventListener('click', function() {
|
|||
|
self._dispatchEvent('newGame');
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype._loadVideoList = function() {
|
|||
|
// Create an XMLHttpRequest to this._videoListURL.
|
|||
|
// Parse the JSON and store the resulting object as this._videoList.
|
|||
|
var self = this;
|
|||
|
this._videoSelect.textContent = '';
|
|||
|
this._videoList = {};
|
|||
|
var req = new XMLHttpRequest();
|
|||
|
//req.responseType = "json";
|
|||
|
req.overrideMimeType("application/json");
|
|||
|
req.onload = function() {
|
|||
|
console.log("loaded video list");
|
|||
|
var videos = JSON.parse(req.responseText);
|
|||
|
|
|||
|
for (var i = 0; i < videos.length; i++) {
|
|||
|
var opt = document.createElement('option');
|
|||
|
//opt.value = videos[i].url;
|
|||
|
opt.text = videos[i].name;
|
|||
|
self._videoSelect.appendChild(opt);
|
|||
|
|
|||
|
self._videoList[videos[i].name] = videos[i];
|
|||
|
}
|
|||
|
|
|||
|
var changeEvent = document.createEvent("Event");
|
|||
|
changeEvent.initEvent("change", true, true);
|
|||
|
self._videoSelect.dispatchEvent(changeEvent);
|
|||
|
};
|
|||
|
req.onerror = function() {
|
|||
|
console.log("error loading video list");
|
|||
|
};
|
|||
|
req.open("GET", this._videoListURL);
|
|||
|
req.send();
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype.addEventListener = function(type, listener) {
|
|||
|
this._listeners[type].push(listener);
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype.removeEventListener = function(type, listener) {
|
|||
|
for (var i = 0; i < this._listeners[type].length; i++) {
|
|||
|
if (this._listeners[type][i] === listener) {
|
|||
|
this._listeners[type].splice(i, 1);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype._dispatchEvent = function(type, detail) {
|
|||
|
if (detail === undefined) {
|
|||
|
detail = null;
|
|||
|
console.log("detail is undefined");
|
|||
|
}
|
|||
|
var event = document.createEvent('CustomEvent');
|
|||
|
event.initCustomEvent(type, false, false, detail);
|
|||
|
for ( var i = 0; i < this._listeners[type].length; i++ ) {
|
|||
|
this._listeners[type][i](event);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype._pieceClicked = function(row, column) {
|
|||
|
var self = this;
|
|||
|
|
|||
|
if (!self._moving) {
|
|||
|
var piece = self._currPosition[row][column];
|
|||
|
var detail = {
|
|||
|
row: row,
|
|||
|
column: column,
|
|||
|
correctRow: parseInt(piece.dataset.correctRow, 10),
|
|||
|
correctColumn: parseInt(piece.dataset.correctColumn, 10),
|
|||
|
};
|
|||
|
|
|||
|
self._dispatchEvent('pieceClicked', detail);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype.loadVideo = function(name) {
|
|||
|
// We can't reference `this` normally in the closure unless we use bind, which we won't.
|
|||
|
var self = this;
|
|||
|
function setBoardSize() {
|
|||
|
// The board will stretch to match the container.
|
|||
|
self._container.style.height = self._video.videoHeight + "px";
|
|||
|
self._container.style.width = self._video.videoWidth + "px";
|
|||
|
self._board.style.height = self._container.style.height;
|
|||
|
self._board.style.width = self._container.style.width;
|
|||
|
|
|||
|
for (var row = 0; row < self._rows; row++) {
|
|||
|
for (var col = 0; col < self._columns; col++) {
|
|||
|
var piece = self._currPosition[row][col];
|
|||
|
piece.width = self._video.videoWidth / self._columns;
|
|||
|
piece.height = self._video.videoHeight / self._rows;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
this._video.onloadeddata = setBoardSize;
|
|||
|
this._video.src = this._videoList[name].url;
|
|||
|
var copyright = document.createElement('a');
|
|||
|
copyright.href = this._videoList[name].infoUrl;
|
|||
|
copyright.text = this._videoList[name].copyright;
|
|||
|
this._copyright.textContent = '';
|
|||
|
this._copyright.appendChild(copyright);
|
|||
|
};
|
|||
|
|
|||
|
// setBoard arranges the pieces to the given position.
|
|||
|
//
|
|||
|
// boardPosition is a 2-dimensional array indexed by row, then column.
|
|||
|
// The values in boardPosition are numbers representing the pieces'
|
|||
|
// order in the solved puzzle, with the highest number being the empty space.
|
|||
|
// E.g. a solved 4×4 puzzle is:
|
|||
|
// [[0,1,2,3],
|
|||
|
// [4,5,6,7],
|
|||
|
// [8,9,10,11],
|
|||
|
// [12,13,14,15]]
|
|||
|
// boardPosition must have the proper dimensions and valid piece values,
|
|||
|
// or bad things will happen.
|
|||
|
// TODO rewrite setBoard to not use _origPosition
|
|||
|
SLIDE_PUZZLE.VideoView.prototype.setBoard = function(boardPosition) {
|
|||
|
for (var row = 0; row < this._rows; row++) {
|
|||
|
for (var col = 0; col < this._columns; col++) {
|
|||
|
var pieceNum = boardPosition[row][col];
|
|||
|
var origRow = Math.floor(pieceNum / this._rows);
|
|||
|
var origCol = pieceNum % this._columns;
|
|||
|
|
|||
|
this._currPosition[row][col] = this._origPosition[origRow][origCol];
|
|||
|
}
|
|||
|
}
|
|||
|
this._placePieces();
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype.movePiece = function(row, column) {
|
|||
|
var self = this;
|
|||
|
if (!self._moving && self.canMovePieces) {
|
|||
|
self._moving = true;
|
|||
|
|
|||
|
var empty = this._getEmptyPosition();
|
|||
|
var boardRect = this._board.getBoundingClientRect()
|
|||
|
var movingPiece = this._currPosition[row][column];
|
|||
|
var leftDistance = (empty.column - column) * boardRect.width / this._columns;
|
|||
|
var topDistance = (empty.row - row) * boardRect.height / this._rows;
|
|||
|
|
|||
|
movingPiece.style.left = '0px';
|
|||
|
movingPiece.style.top = '0px';
|
|||
|
|
|||
|
function animateMove() {
|
|||
|
var moveFactor = 6; // Must be an integer
|
|||
|
var left = parseInt(movingPiece.style.left, 10);
|
|||
|
var top = parseInt(movingPiece.style.top, 10);
|
|||
|
if (Math.abs(left) < Math.abs(leftDistance) && Math.abs(leftDistance - left) > Math.abs(leftDistance / moveFactor)) {
|
|||
|
left += leftDistance / moveFactor;
|
|||
|
movingPiece.style.left = left + "px";
|
|||
|
window.setTimeout(animateMove, self.FRAME_DELAY);
|
|||
|
} else if (Math.abs(top) < Math.abs(topDistance) && Math.abs(topDistance - top) > Math.abs(topDistance / moveFactor)) {
|
|||
|
top += topDistance / moveFactor;
|
|||
|
movingPiece.style.top = top + "px";
|
|||
|
window.setTimeout(animateMove, self.FRAME_DELAY);
|
|||
|
} else {
|
|||
|
self._currPosition[row][column] = self._currPosition[empty.row][empty.column];
|
|||
|
self._currPosition[empty.row][empty.column] = movingPiece;
|
|||
|
movingPiece.style.left = '0px';
|
|||
|
movingPiece.style.top = '0px';
|
|||
|
self._placePieces();
|
|||
|
self._moving = false;
|
|||
|
self._dispatchEvent('doneMoving');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
animateMove();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype.startUpdates = function() {
|
|||
|
var self = this;
|
|||
|
if (this._updateInterval === null) {
|
|||
|
this._updateInterval = window.setInterval(function() {
|
|||
|
self._updatePieces();
|
|||
|
}, this.FRAME_DELAY);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype.stopUpdates = function() {
|
|||
|
if (this._updateInterval !== null) {
|
|||
|
window.clearInterval(this._updateInterval);
|
|||
|
this._updateInterval = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype._getEmptyPosition = function() {
|
|||
|
var pos = {};
|
|||
|
for (var row = 0; row < this._rows; row++) {
|
|||
|
for (var col = 0; col < this._columns; col++) {
|
|||
|
if (this._currPosition[row][col].id === 'slidepuzzle-empty-piece') return {row:row,column:col};
|
|||
|
}
|
|||
|
}
|
|||
|
throw new Error('no empty position found');
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype._updatePieces = function() {
|
|||
|
// If the pieces' videos end up out of sync, we can optimize
|
|||
|
// this method by first taking a still of the video and then slicing it.
|
|||
|
for (var row = 0; row < this._rows; row++) {
|
|||
|
for (var col = 0; col < this._columns; col++) {
|
|||
|
/*
|
|||
|
var piece = this._origPosition[row][col];
|
|||
|
var ctx = piece.getContext('2d');
|
|||
|
var w = piece.width;
|
|||
|
var h = piece.height;
|
|||
|
var sourceX = col * w;
|
|||
|
var sourceY = row * h;
|
|||
|
ctx.drawImage(this._video, sourceX, sourceY, w, h, 0, 0, w, h);
|
|||
|
*/
|
|||
|
var piece = this._currPosition[row][col];
|
|||
|
var origRow = parseInt(piece.dataset.correctRow, 10);
|
|||
|
var origCol = parseInt(piece.dataset.correctColumn, 10);
|
|||
|
var ctx = piece.getContext('2d');
|
|||
|
var w = piece.width;
|
|||
|
var h = piece.height;
|
|||
|
var sourceX = origCol * w;
|
|||
|
var sourceY = origRow * h;
|
|||
|
ctx.drawImage(this._video, sourceX, sourceY, w, h, 0, 0, w, h);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
SLIDE_PUZZLE.VideoView.prototype._placePieces = function() {
|
|||
|
for (var row = 0; row < this._rows; row++) {
|
|||
|
for (var col = 0; col < this._columns; col++) {
|
|||
|
var cell = this._board.rows[row].cells[col];
|
|||
|
cell.innerHTML = '';
|
|||
|
var piece = this._currPosition[row][col];
|
|||
|
cell.appendChild(piece);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|