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