Commit old files
This commit is contained in:
commit
95bd0ea6ed
9
README.txt
Normal file
9
README.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
HTML5 video slide puzzle
|
||||||
|
by Brandon Dyck
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
This is a project from a web development course ca. 2016 in which I was assigned to build some application that used a dynamically generated table, an AJAX request, and a couple of HTML 5 features. The textures on the pieces are CANVAS elements filled periodically with rectangles cropped from the current frame of an invisible video. The whole thing is vanilla JavaScript. I was kind of proud of it, but I didn't yet know about requestAnimationFrame, and the framerate is terrible. (Much worse than I remember, actually.)
|
||||||
|
|
||||||
|
The code has no version history because the original repository was apparently lost when Bitbucket dropped Mercurial support in 2020. I found a copy lying around in 2024.
|
||||||
|
|
||||||
|
Do whatever what you want with it, and don't sue me if it doesn't work.
|
44
game.html
Normal file
44
game.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" >
|
||||||
|
<title>Game Board</title>
|
||||||
|
<link rel="stylesheet" href="style/slidepuzzle.css">
|
||||||
|
<script src="js/SlidePuzzleStopwatch.js"></script>
|
||||||
|
<script src="js/SlidePuzzleModel.js"></script>
|
||||||
|
<script src="js/SlidePuzzleView.js"></script>
|
||||||
|
<script src="js/SlidePuzzleController.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
window.puzzleController = new SLIDE_PUZZLE.VideoController(4,4);
|
||||||
|
window.puzzleController.initView();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Video Slide Puzzle v1.0</h1>
|
||||||
|
|
||||||
|
<div id="slidepuzzle-container">
|
||||||
|
<video id="slidepuzzle-video" autoplay loop>
|
||||||
|
Video not working! Blame Microsoft!
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<table id="slidepuzzle-board"></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="slidepuzzle-controls">
|
||||||
|
<p>Elapsed time: <span id="slidepuzzle-timer">0</span></p>
|
||||||
|
<p>
|
||||||
|
<button id="slidepuzzle-undo">Undo</button>
|
||||||
|
<button id="slidepuzzle-newgame">New game</button>
|
||||||
|
</p>
|
||||||
|
<label for="videolist">Select background:</label>
|
||||||
|
<select name="videolist" id='slidepuzzle-videolist'>
|
||||||
|
</select><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="slidepuzzle-copyright"></p>
|
||||||
|
<p id="cell-coords"></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
64
gameboard.html
Normal file
64
gameboard.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" >
|
||||||
|
<title>Game Board</title>
|
||||||
|
<link rel="stylesheet" href="style/gameboard.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Video Slide Puzzle v0.1</h1>
|
||||||
|
<div id="board">
|
||||||
|
<video id="video" src="video/plasmacropped.mp4" autoplay loop>
|
||||||
|
Video not working!
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="empty"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
04:56<br>
|
||||||
|
<button>Undo</button><br>
|
||||||
|
<button>New game</button><br>
|
||||||
|
<label for="videolist">Select background:</label>
|
||||||
|
<select name="videolist">
|
||||||
|
<option>Plasma lamp</option>
|
||||||
|
<option>Buffalo Dance</option>
|
||||||
|
<option>Viennese waltz</option>
|
||||||
|
<option>Bikini Atoll</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="copyright">
|
||||||
|
Video cropped from <a href="https://commons.wikimedia.org/wiki/File:Plasmaballvid1.ogg">Plasmavid1.ogg</a> by <a href="https://commons.wikimedia.org/wiki/User:Geni">Geni</a> / <a href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
image/puzzleland.jpg
Normal file
BIN
image/puzzleland.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
181
index.html
Normal file
181
index.html
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" >
|
||||||
|
<title>Slide Puzzle Description</title>
|
||||||
|
<link rel="stylesheet" href="style/description.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- NOTE: This page does not bother to demonstrate HTML entities, as they are mostly obsoleted by a compose key. ☺ -->
|
||||||
|
|
||||||
|
<h1>Video Slide Puzzle</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
Brandon Dyck<br>
|
||||||
|
CS 2550-X01
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><a href="game.html">Play game</a></p>
|
||||||
|
|
||||||
|
<h2>Game Description</h2>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img class="illustration" src="image/puzzleland.jpg" alt="The 14-15 Puzzle in Puzzleland">
|
||||||
|
<figcaption>
|
||||||
|
From Sam Lloyd's <cite>Cyclopedia of 5000 Puzzles</cite> (1915).<br>
|
||||||
|
Public domain.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Video Slide Puzzle is a Javascript/HTML5 implementation of the classic slide puzzle, or <a href="https://en.wikipedia.org/wiki/15_puzzle"><span style="font-style:italic">15 puzzle</span></a>, with a video, rather than a static image, as the texture for the puzzle pieces. When the game loads, a list of available videos is loaded into the "Select background" option list via AJAX, the puzzle is rendered using the first video in the list, and the player can able to select a different video at any time during gameplay using the "Select background" list.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The puzzle has fifteen pieces and one empty space arranged in a 4×4 grid. When the game loads, and subsequently whenever the player clicks the "New game" button, the pieces are shuffled to a solvable starting position, and the timer is reset to zero and starts counting upward. When the player clicks a puzzle piece adjacent to the empty space, the piece moves into the space, leaving a space in its previous position. Motion of puzzle pieces is animated. Once the pieces are in the correct positions and until a new game is started, the timer stops, the empty space in the puzzle is filled with the missing piece of the video, and clicking on the puzzle will no longer have any effect until a new game is started.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The "Undo" button is disabled when gameplay starts and is enabled as soon as the player moves a puzzle piece. When the player clicks the "Undo" button, the last piece moved moves to its previous position. This can be repeated until all moves have been reverted, at which point the "Undo" button is disabled again until the player makes another move.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You can see a non-functional <a href="gameboard.html" target="gameboard">mockup of the game board</a>. (The mockup does not show a game in progress, as that would require either a great deal of manual video editing or writing the bulk of the game's view code.) There is also a <a href="prototype.html" target="prototype">proof-of-concept</a> of rendering and moving slices of an HTML5 video element.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Model Design</h2>
|
||||||
|
<h3>Data representation</h3>
|
||||||
|
The model represents the game board internally with the following objects:
|
||||||
|
<dl>
|
||||||
|
<dt><span class="var const">Rows</span></dt>
|
||||||
|
<dd><p>A public, constant integer indicating the number of rows in the game board.</p></dd>
|
||||||
|
|
||||||
|
<dt><span class="var const">Columns</span></dt>
|
||||||
|
<dd><p>A public, constant integer indicating the number of columns in the game board.</p></dd>
|
||||||
|
|
||||||
|
<dt><span class="var const">Empty</span></dt>
|
||||||
|
<dd><p>A public, constant integer representing the empty puzzle piece.</p></dd>
|
||||||
|
|
||||||
|
<dt><span class="var">position<span></dt>
|
||||||
|
<dd>
|
||||||
|
<p>
|
||||||
|
A private, zero-indexed sequence of <span class="var const">Rows</span> × <span class="var const">Columns</span> integers from 0 to <span class="var const">Rows</span> × <span class="var const">Columns</span> − 1 representing the initial game board layout. Each index corresponds to a game board position as follows (for a 4×4 grid):
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>0</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>4</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>6</td>
|
||||||
|
<td>7</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>8</td>
|
||||||
|
<td>9</td>
|
||||||
|
<td>10</td>
|
||||||
|
<td>11</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>12</td>
|
||||||
|
<td>13</td>
|
||||||
|
<td>14</td>
|
||||||
|
<td>15</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The value at each index represents the correct position of the puzzle piece currently in that position. For example, if <span class="var">position</span><sub>4</sub> = 12 in a 4×4 puzzle, then the puzzle piece at the first column and second row must be moved to the first column and fourth row in order to solve the puzzle. The exception is that the value <span class="var const">Rows</span> × <span class="var const">Columns</span> − 1 represents the empty space in the board.
|
||||||
|
</p>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt><span class="var">moves</span></dt>
|
||||||
|
<dd>
|
||||||
|
<p>
|
||||||
|
A private integer sequence representing the order in which pieces have been moved into the empty space. This sequence can be used together with <span class="var">position</span> to reconstruct the initial board position.
|
||||||
|
</p>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Functions</h3>
|
||||||
|
The model provides the following public functions:
|
||||||
|
<dl>
|
||||||
|
|
||||||
|
|
||||||
|
<dt>getPosition():integer[]</dt>
|
||||||
|
<dd>
|
||||||
|
<p>Returns a copy of <span class="var">position</span>.</p>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>getPositionOf(<span class="var">piece</span>:integer):integer[]</dt>
|
||||||
|
<dd>
|
||||||
|
<p>Returns the current position of <span class="var">piece</span>.</p>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>getMoves():integer[]</dt>
|
||||||
|
<dd>
|
||||||
|
<p>Returns a copy of <span class="var">moves</span>.</p>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>isSolved():boolean</dt>
|
||||||
|
<dd>
|
||||||
|
<p>Returns a boolean value indicating whether the puzzle is in a solved position.</p>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>move(<span class="var">piece</span>:integer):void</dt>
|
||||||
|
<dd>
|
||||||
|
<p>
|
||||||
|
Moves <span class="var">piece</span> into the empty space. Throws an exception if <span class="var">piece</span> is not an integer, <span class="var">piece</span> < 0, <span class="var">piece</span> ≥ <span class="var const">Rows</span> × <span class="var const">Columns</span>, or <span class="var">piece</span> is not adjacent to the empty space.
|
||||||
|
</p>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>undo():void</dt>
|
||||||
|
<dd>
|
||||||
|
<p>
|
||||||
|
Changes <span class="var">position</span> to its previous state and removes the last element of <span class="var">moves</span>. Throws an exception if <span class="var">moves</span> is empty.
|
||||||
|
</p>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>isSolved:boolean</dt>
|
||||||
|
<dd>
|
||||||
|
<p>
|
||||||
|
A public read-only property indicating whether the puzzle is in its solved position.
|
||||||
|
</p>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Initialization</h3>
|
||||||
|
<p>Initialization of the model does the following:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Initialize <span class="var">position</span> to a length of <span class="var const">Rows</span> × <span class="var const">Columns</span>, with each value equal to its index.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Randomly arrange the puzzle using the Fisher-Yates (Knuth) shuffle.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Check the puzzle for solvability using the formula described in <a href="https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html">Mark Ryan (2004)</a>, returning to Step 2 if the puzzle is not solvable.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Initialize <span class="var">moves</span> to a length of 0.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The Fisher-Yates shuffle delivers an even distribution of permutations in <span class="var">O</span>(<span class="var">n</span>) time. Since exactly one half of all permutations of the board are solvable (<a href="http://kevingong.com/Math/SixteenPuzzle.html#proof">Kevin Gong 2004</a>), we can expect to have to shuffle the board 1.5 times per game, on average. Checking for solvability using a naïve algorithm based on Ryan's formula will run in <span class="var">O</span>(<span class="var">n</span><sup>2</sup>) time, but this is not enough to matter on a game board small enough for human use. Overall, this seems a reasonably fast method of creating solvable games.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
114
js/SlidePuzzleController.js
Normal file
114
js/SlidePuzzleController.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController = function(rows, columns) {
|
||||||
|
this.STOPWATCH_DELAY = 500;
|
||||||
|
|
||||||
|
this._rows = rows;
|
||||||
|
this._columns = columns;
|
||||||
|
|
||||||
|
this._model = null;
|
||||||
|
this._view = null;
|
||||||
|
this._stopwatch = new SLIDE_PUZZLE.Stopwatch(this.STOPWATCH_DELAY);
|
||||||
|
|
||||||
|
this._undoing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController.prototype.initView = function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this._view = new SLIDE_PUZZLE.VideoView(this._rows, this._columns);
|
||||||
|
|
||||||
|
this._view.addEventListener("pieceClicked", this._tryMove.bind(this));
|
||||||
|
this._view.addEventListener('undo', this._undo.bind(this));
|
||||||
|
this._view.addEventListener('newGame', this._newGame.bind(this));
|
||||||
|
this._stopwatch.addEventListener('tick', function() {
|
||||||
|
self._view.timerText = self._stopwatch.elapsedString;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._newGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController.prototype._tryMove = function(event) {
|
||||||
|
var index = this._rowColumnToIndex(event.detail.row, event.detail.column);
|
||||||
|
var piece = this._rowColumnToIndex(event.detail.correctRow, event.detail.correctColumn);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._model.move(piece);
|
||||||
|
if (this._model.isSolved()) {
|
||||||
|
var listener;
|
||||||
|
listener = function() {
|
||||||
|
this._endGame();
|
||||||
|
this._view.removeEventListener('doneMoving', listener);
|
||||||
|
}.bind(this);
|
||||||
|
this._view.addEventListener('doneMoving', listener);
|
||||||
|
}
|
||||||
|
this._view.movePiece(event.detail.row, event.detail.column);
|
||||||
|
this._view.canUndo = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController.prototype._undo = function() {
|
||||||
|
if (!this._undoing && !this._view.moving) {
|
||||||
|
this._undoing = true;
|
||||||
|
try {
|
||||||
|
var piece = this._model.getMoves().pop();
|
||||||
|
var index = this._model.getPositionOf(piece);
|
||||||
|
var rowCol = this._indexToRowColumn(index);
|
||||||
|
|
||||||
|
this._model.undo();
|
||||||
|
this._view.canUndo = this._model.getMoves().length !== 0;
|
||||||
|
this._view.movePiece(rowCol.row, rowCol.column);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("can't undo: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._undoing = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController.prototype._newGame = function() {
|
||||||
|
this._model = new SLIDE_PUZZLE.Model(this._rows, this._columns);
|
||||||
|
|
||||||
|
var modelPos = this._model.getPosition();
|
||||||
|
var viewPos = [];
|
||||||
|
|
||||||
|
for (var row = 0; row < this._rows; row++) {
|
||||||
|
viewPos.push([]);
|
||||||
|
for (var col = 0; col < this._columns; col++) {
|
||||||
|
var modelPosIndex = row * this._columns + col;
|
||||||
|
viewPos[row].push(modelPos[modelPosIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._view.setBoard(viewPos);
|
||||||
|
this._view.showEmptyPiece = false;
|
||||||
|
this._view.canMovePieces = true;
|
||||||
|
this._view.canUndo = false;
|
||||||
|
|
||||||
|
if (this._stopwatch.isRunning) {
|
||||||
|
this._stopwatch.stop();
|
||||||
|
}
|
||||||
|
this._stopwatch.reset();
|
||||||
|
this._view.timerText = this._stopwatch.elapsedString;
|
||||||
|
this._stopwatch.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController.prototype._endGame = function() {
|
||||||
|
// TODO
|
||||||
|
this._stopwatch.stop();
|
||||||
|
this._view.showEmptyPiece = true;
|
||||||
|
this._view.canMovePieces = false;
|
||||||
|
this._view.canUndo = false;
|
||||||
|
console.log("end game");
|
||||||
|
}
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController.prototype._rowColumnToIndex = function(row, column) {
|
||||||
|
return (row * this._columns) + column;
|
||||||
|
}
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.VideoController.prototype._indexToRowColumn = function(index) {
|
||||||
|
var row = Math.floor(index / this._columns);
|
||||||
|
var column = index % this._columns;
|
||||||
|
return {row:row, column:column};
|
||||||
|
}
|
151
js/SlidePuzzleModel.js
Normal file
151
js/SlidePuzzleModel.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};
|
||||||
|
|
||||||
|
// Model constructor builds a solveable, shuffled slide puzzle model.
|
||||||
|
SLIDE_PUZZLE.Model = function(rows, columns) {
|
||||||
|
'use strict';
|
||||||
|
this.ROWS = rows;
|
||||||
|
this.COLUMNS = columns;
|
||||||
|
this.EMPTY = this.ROWS * this.COLUMNS - 1;
|
||||||
|
|
||||||
|
this._position = [];
|
||||||
|
this._moves = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < this.ROWS * this.COLUMNS; i++) {
|
||||||
|
this._position.push(i);
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
this._shuffle();
|
||||||
|
} while (!this._isSolveable());
|
||||||
|
};
|
||||||
|
|
||||||
|
// getPosition returns the current layout of the puzzle pieces.
|
||||||
|
SLIDE_PUZZLE.Model.prototype.getPosition = function() {
|
||||||
|
'use strict';
|
||||||
|
return this._position.slice();
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Model.prototype.getPositionOf = function(piece) {
|
||||||
|
'use strict';
|
||||||
|
for (var i = 0; i < this._position.length; i++) {
|
||||||
|
if (this._position[i] === piece) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMoves returns the history of all moves.
|
||||||
|
SLIDE_PUZZLE.Model.prototype.getMoves = function() {
|
||||||
|
'use strict';
|
||||||
|
return this._moves.slice();
|
||||||
|
};
|
||||||
|
|
||||||
|
// isSolved indicates whether the puzzle is in its solved order.
|
||||||
|
SLIDE_PUZZLE.Model.prototype.isSolved = function() {
|
||||||
|
'use strict';
|
||||||
|
for (var i = 0; i < this._position.length - 1; i++) {
|
||||||
|
if (this._position[i] > this._position[i+1]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// move moves piece into the blank space and logs the move in the move history.
|
||||||
|
// move throws an error if the move is invalid or if the piece doesn't exist.
|
||||||
|
SLIDE_PUZZLE.Model.prototype.move = function(piece) {
|
||||||
|
'use strict';
|
||||||
|
if (!this._isValidMove(piece)) {
|
||||||
|
throw new Error('Invalid move');
|
||||||
|
}
|
||||||
|
var iPiece = this._position.indexOf(piece);
|
||||||
|
var iEmpty = this._position.indexOf(this.EMPTY);
|
||||||
|
this._swap(iPiece, iEmpty);
|
||||||
|
this._moves.push(piece);
|
||||||
|
};
|
||||||
|
|
||||||
|
// undo reverses the last move and removes it from the move history.
|
||||||
|
// undo throws an error if there are no moves in the history.
|
||||||
|
SLIDE_PUZZLE.Model.prototype.undo = function() {
|
||||||
|
'use strict';
|
||||||
|
if (this._moves.length === 0) {
|
||||||
|
throw new Error('No moves to undo');
|
||||||
|
}
|
||||||
|
var piece = this._moves.pop();
|
||||||
|
var iPiece = this._position.indexOf(piece);
|
||||||
|
var iEmpty = this._position.indexOf(this.EMPTY);
|
||||||
|
this._swap(iPiece, iEmpty);
|
||||||
|
};
|
||||||
|
|
||||||
|
// _isValidMove determines whether piece can move, that is, whether it is next to the empty space.
|
||||||
|
SLIDE_PUZZLE.Model.prototype._isValidMove = function(piece) {
|
||||||
|
'use strict';
|
||||||
|
if (!this._isValidPiece(piece)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var iPiece = this._position.indexOf(piece);
|
||||||
|
var iEmpty = this._position.indexOf(this.EMPTY);
|
||||||
|
return Math.abs(iPiece - iEmpty) === this.COLUMNS ||
|
||||||
|
Math.abs(iPiece - iEmpty) === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// _isValidPiece indicates whether piece is present in this puzzle.
|
||||||
|
SLIDE_PUZZLE.Model.prototype._isValidPiece = function(piece) {
|
||||||
|
'use strict';
|
||||||
|
return piece >= 0 && piece < this.EMPTY;
|
||||||
|
};
|
||||||
|
|
||||||
|
// _isSolveable indicates whether the puzzle is solveable, using Mark Ryan's algorithm.
|
||||||
|
// See https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html.
|
||||||
|
SLIDE_PUZZLE.Model.prototype._isSolveable = function() {
|
||||||
|
'use strict';
|
||||||
|
function even(n) {
|
||||||
|
return n % 2 === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count inversions
|
||||||
|
var inversions = 0;
|
||||||
|
for (var i = 0; i < this._position.length; i++) {
|
||||||
|
for (var j = i + 1; j < this._position.length; j++) {
|
||||||
|
if (this._position[j] < this._position[i]) {
|
||||||
|
inversions++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var iEmpty = this._position.indexOf(this.EMPTY);
|
||||||
|
var isEmptyOddFromBottom = !even(this.ROWS - Math.floor(iEmpty / this.ROWS));
|
||||||
|
var width = this.COLUMNS;
|
||||||
|
return (!even(width) && even(inversions)) ||
|
||||||
|
(even(width) && isEmptyOddFromBottom === even(inversions));
|
||||||
|
};
|
||||||
|
|
||||||
|
// _swap swaps the pieces at indices i1 and i2 in this._position.
|
||||||
|
SLIDE_PUZZLE.Model.prototype._swap = function(i1, i2) {
|
||||||
|
'use strict';
|
||||||
|
var tmp = this._position[i1];
|
||||||
|
this._position[i1] = this._position[i2];
|
||||||
|
this._position[i2] = tmp;
|
||||||
|
};
|
||||||
|
|
||||||
|
// _shuffle performs a Fisher-Yates shuffle on this._position.
|
||||||
|
SLIDE_PUZZLE.Model.prototype._shuffle = function() {
|
||||||
|
'use strict';
|
||||||
|
// Returns a random int n where 0 ≤ n < max
|
||||||
|
function randomInt(max) {
|
||||||
|
return Math.floor(Math.random() * max);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: should possible random ints include i?
|
||||||
|
for (var i = this._position.length - 1; i > 0; i--) {
|
||||||
|
this._swap(randomInt(i), i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Model.prototype.toString = function() {
|
||||||
|
'use strict';
|
||||||
|
var lines = [];
|
||||||
|
for (var i = 0; i < this.ROWS; i++) {
|
||||||
|
var rowStart = i * this.COLUMNS;
|
||||||
|
lines.push(this._position.slice(rowStart, rowStart + this.COLUMNS).join('\t'));
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
};
|
118
js/SlidePuzzleStopwatch.js
Normal file
118
js/SlidePuzzleStopwatch.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Stopwatch = function(tickMillis) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this._tickMillis = tickMillis;
|
||||||
|
this._startTime = null;
|
||||||
|
this._intervalID = null;
|
||||||
|
this._priorElapsed = 0;
|
||||||
|
this._currentElapsed = 0;
|
||||||
|
|
||||||
|
this._listeners = [];
|
||||||
|
|
||||||
|
this.__defineGetter__("isRunning", function() {
|
||||||
|
return self._intervalID !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.__defineGetter__("elapsedMillis", function() {
|
||||||
|
return self._priorElapsed + self._currentElapsed;
|
||||||
|
});
|
||||||
|
|
||||||
|
function divideInt(a, b) {
|
||||||
|
return Math.floor(a/b);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__defineGetter__("elapsedString", function() {
|
||||||
|
var totalMillis = self._priorElapsed + self._currentElapsed;
|
||||||
|
|
||||||
|
var seconds = divideInt(totalMillis, 1000) % 60;
|
||||||
|
var minutes = divideInt(totalMillis, 60000) % 60;
|
||||||
|
var hours = divideInt(totalMillis, 3600000);
|
||||||
|
|
||||||
|
var showHours = hours > 0;
|
||||||
|
var showMinutes = minutes > 0 || showHours;
|
||||||
|
var fixMinutes = showHours;
|
||||||
|
var fixSeconds = showMinutes;
|
||||||
|
|
||||||
|
var hoursString;
|
||||||
|
if (showHours) {
|
||||||
|
hoursString = hours + ':';
|
||||||
|
} else {
|
||||||
|
hoursString = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
var minutesString;
|
||||||
|
if (showMinutes) {
|
||||||
|
if (fixMinutes && minutes < 10) {
|
||||||
|
minutesString = '0' + minutes + ':';
|
||||||
|
} else {
|
||||||
|
minutesString = minutes + ':';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
minutesString = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondsString;
|
||||||
|
if (fixSeconds && seconds < 10) {
|
||||||
|
secondsString = '0' + seconds;
|
||||||
|
} else {
|
||||||
|
secondsString = seconds.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hoursString + minutesString + secondsString;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Stopwatch.prototype.start = function() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
self._currentElapsed = window.performance.now() - self._startTime;
|
||||||
|
self._dispatchTickEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._startTime = window.performance.now();
|
||||||
|
this._intervalID = window.setInterval(tick, this._tickMillis);
|
||||||
|
} else {
|
||||||
|
throw new Error("can't start a started stopwatch");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Stopwatch.prototype.stop = function() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
window.clearInterval(this._intervalID);
|
||||||
|
this._intervalID = null;
|
||||||
|
this._startTime = null;
|
||||||
|
|
||||||
|
this._priorElapsed += this._currentElapsed;
|
||||||
|
this._currentElapsed = 0;
|
||||||
|
} else {
|
||||||
|
throw new Error("can't stop a stopped stopwatch");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Stopwatch.prototype.reset = function() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
this._priorElapsed = 0;
|
||||||
|
this._currentElapsed = 0;
|
||||||
|
this._dispatchTickEvent();
|
||||||
|
} else {
|
||||||
|
throw new Error("can't reset a started stopwatch");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Stopwatch.prototype.addEventListener = function(type, listener) {
|
||||||
|
if (type === 'tick') {
|
||||||
|
this._listeners.push(listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SLIDE_PUZZLE.Stopwatch.prototype._dispatchTickEvent = function() {
|
||||||
|
var event = document.createEvent('CustomEvent');
|
||||||
|
event.initCustomEvent('tick', false, false, null);
|
||||||
|
for (var i = 0; i < this._listeners.length; i++) {
|
||||||
|
this._listeners[i](event);
|
||||||
|
}
|
||||||
|
}
|
369
js/SlidePuzzleView.js
Normal file
369
js/SlidePuzzleView.js
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
prototype.html
Normal file
96
prototype.html
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 480px;
|
||||||
|
height: 480px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
#video {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 480px;
|
||||||
|
height: 480px;
|
||||||
|
}
|
||||||
|
#slice {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid lightblue;
|
||||||
|
/* box-sizing: border-box; */
|
||||||
|
}
|
||||||
|
#slicediv {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#slicediv span {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
function sliceVideo() {
|
||||||
|
var video = document.getElementById("video");
|
||||||
|
var canvas = document.getElementById("slice");
|
||||||
|
var context = canvas.getContext("2d");
|
||||||
|
var cw = canvas.width = 120;
|
||||||
|
var ch = canvas.height = 120;
|
||||||
|
var sourceX = 100;
|
||||||
|
var sourceY = 100;
|
||||||
|
var framerate = 15;
|
||||||
|
var delay = 1000 / framerate;
|
||||||
|
context.drawImage(video, sourceX, sourceY, cw, ch, 0, 0, cw, ch);
|
||||||
|
window.setTimeout(sliceVideo, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRight(distance, time) {
|
||||||
|
var slice = document.getElementById("slicediv");
|
||||||
|
console.log("about to move " + distance +"px in " + time + "ms");
|
||||||
|
var framerate = 15;
|
||||||
|
var delay = 1000 / framerate;
|
||||||
|
var frameCount = Math.round(time / delay);
|
||||||
|
var step = distance / frameCount;
|
||||||
|
|
||||||
|
var setMove = function(framesLeft) {
|
||||||
|
console.log(framesLeft + " frames left");
|
||||||
|
console.log(slice.style.left);
|
||||||
|
if (framesLeft > 0) {
|
||||||
|
window.setTimeout(function() {
|
||||||
|
slice.style.left = (slice.offsetLeft + step) + "px";
|
||||||
|
setMove(framesLeft - 1);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMove(frameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
sliceVideo();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<video id="video" src="video/plasmacropped.mp4" autoplay loop>
|
||||||
|
Video not working! Everything is probably Microsoft's fault!
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="slicediv" >
|
||||||
|
<span>Click me!</span>
|
||||||
|
<canvas id="slice" onclick="moveRight(200, 800)"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
56
style/description.css
Normal file
56
style/description.css
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
figure {
|
||||||
|
float: right;
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
padding: 1em;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.illustration {
|
||||||
|
width:20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt::after {
|
||||||
|
content: ":";
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.var {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.var.const {
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 70%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right:auto;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, .subtitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
45
style/slidepuzzle.css
Normal file
45
style/slidepuzzle.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#slidepuzzle-video {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slidepuzzle-board {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0px;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slidepuzzle-board td {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slidepuzzle-empty-piece.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slidepuzzle-container {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slidepuzzle-board, #slidepuzzle-video {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slidepuzzle-board * {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slidepuzzle-board canvas {
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display:block;
|
||||||
|
position: relative;
|
||||||
|
}
|
BIN
video/plasmacropped.mp4
Normal file
BIN
video/plasmacropped.mp4
Normal file
Binary file not shown.
18
video/videolist.json
Normal file
18
video/videolist.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Plasma lamp",
|
||||||
|
"copyright": "Cropped from Plasmavid1.ogg by Geni / CC-BY-SA",
|
||||||
|
"url": "video/plasmacropped.mp4",
|
||||||
|
"infoUrl": "https://commons.wikimedia.org/wiki/File:Plasmaballvid1.ogg"
|
||||||
|
}, {
|
||||||
|
"name": "Sallie Gardner",
|
||||||
|
"copyright": "\"Sally Gardner at Gallop\" by Eadweard Muybridge / Public Domain",
|
||||||
|
"url": "https://archive.org/download/SallieGardnerAtGallop/SallieGardnerAtAGallop.mp4",
|
||||||
|
"infoUrl": "https://archive.org/details/SallieGardnerAtGallop"
|
||||||
|
}, {
|
||||||
|
"name": "Mario (loads slowly!)",
|
||||||
|
"copyright": "Mario1_500_LQ.mp4 by Andrew Gardikis",
|
||||||
|
"url": "https://archive.org/download/Mario1_500/Mario1_500_LQ.mp4",
|
||||||
|
"infoUrl": "https://archive.org/details/Mario1_500"
|
||||||
|
}
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user