html5-video-slidepuzzle/js/SlidePuzzleView.js
2024-06-21 10:14:17 -06:00

370 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
}