Web Inspector/Chromeのデバッガで動画を再生する
普段、Web Inspectorはデバッガとして使っていると思うが、Web Inspectorは実は様々な物を表示できる。
例えば、コンソールでライフゲームを表示することができる。 Web Inspector Life Gameに、コードを置いた。タイミングによっては上手く動かないので、コンソール上でCmd+Rなどを押してリロードすると上手くいく。
以下に、実装に使ったハックや技術を紹介する
console.logで色付き文字を表示する
まず、console.warnやconsole.infoなどを使うと、黒字以外の文字を表示できることを思い出してほしい。console画面は、HTMLで実装されているのである。
console.log("%s", "aa");
のように、printf形式で文字を表示できる機能があることを知っているだろうか。 この中に、色指定をするためのものがある。%cである。
console.log("%chello", "color: red;");
とすると、
hello
と表示される。
CSSを悪用する
同様に、 console.logの%cには、 font-sizeやline-height、margin, backgroundなどが使用できる。
console.log("%c.", "font-size: 100px; border: 1px solid black;");
とすると、大きな文字と、ボーダーの枠が表示される。では、画像表示はできるだろうか?
console.log("%c.", "font-size: 10em; background-image: url('http://example.com/image.jpg')");
なんとできるのである。あとは、想像の通り、CSSを微調整すれば背景画像だけを表示することができる。
アニメーション
アニメーションに必要なのは、リフレッシュ、つまり次の画像を、前と同じ位置に表示することである。 これはどうするかというと、
console.clear()
を使用する。
しかし、ここで大きな問題がある。 console関数は全て非同期に動作するのだ。 すなわち、
console.clear();console.log('a');
とすると、aが表示される保障はない。
今回のハックでは、console.clear()のタイミングを20ms程度、console.logより前にずらすことで、clear()がlog()より前に呼ばれることをある程度保障している。
アニメーション自体は、canvasを使用した。toDataURL()を使用して得た画像を、background-imageとしてセットしている。 念のため以下に、全コードを載せておく。
<script> var TheGameOfLife = function (options) { this.width = options.width + 1; this.height = options.height + 1; this.firstLives = options.firstLives; this.cells = this.clearCells([]); this.afterUpdates = []; this.randomize(); }; TheGameOfLife.prototype = { clearCells: function (cells) { var i, j; for (i = 0; i <= this.width; i++) { cells[i] = []; for (j = 0; j <= this.height; j++) { cells[i][j] = 0; } } return cells; }, update: function () { var i, j, n; var newCells = this.clearCells([]); for (i = 1; i < this.width; i++) { for (j = 1; j < this.height; j++) { n = 0; n += this.cells[i - 1][j - 1]; n += this.cells[i - 1][j]; n += this.cells[i - 1][j + 1]; n += this.cells[i][j - 1]; n += this.cells[i][j + 1]; n += this.cells[i + 1][j - 1]; n += this.cells[i + 1][j]; n += this.cells[i + 1][j + 1]; if (this.cells[i][j] == 0 ? n == 3 : (n == 2 || n == 3)) { newCells[i][j] = 1; } else { newCells[i][j] = 0; } } } this.cells = newCells; for (i = 0; i < this.afterUpdates.length; i++) { this.afterUpdates[i](); } }, afterUpdate: function (hook) { this.afterUpdates.push(hook); }, randomize: function () { var remains = this.firstLives; var i, j; while (remains--) { i = Math.floor(Math.random() * (this.width - 1) * (this.height - 1)); j = i % (this.height - 1); i = (i - j) / (this.height - 1); i++; j++; this.cells[i][j] = 1; } } }; document.addEventListener("DOMContentLoaded", function () { var width = 16; var height = 16; function reproduce() { var url = canvas.toDataURL(); console.log('Reload to reset: %c.', 'font-size: 0; padding: 150px; background-image: url(' + url + '); background-repeat: no-repeat;'); } function render () { var wScale = canvas.width / width; var hScale = canvas.height / height; context.clearRect(0, 0, canvas.width, canvas.height); var i, j; for (i = 1; i < tgol.width; i++) { for (j = 1; j < tgol.height; j++) { if (tgol.cells[i][j] & 1) { context.fillRect(i * wScale, j * hScale, wScale, hScale); } } } } var canvas = document.getElementById("canvas"); var context = canvas.getContext("2d"); var options = { width: width, height: height, firstLives: 50, canvas: canvas }; var tgol = new TheGameOfLife(options); tgol.afterUpdate(function () { render(); reproduce(); }); render(); setInterval(function () { tgol.update(); }, 200); document.addEventListener("click", function () { tgol.clearCells(tgol.cells); tgol.randomize(); }, false); }, false); setTimeout(function () { setInterval(function (){ console.clear();}, 200); }, 180); </script> <canvas id="canvas" width="300" height="300" style="display: none;"></canvas>