html5-video-slidepuzzle/js/SlidePuzzleView.js

370 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2024-06-21 16:04:10 +00:00
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);
}
}
}