CSSチームが運営するHTML5・CSS3情報サイト

HTML5 GOGO

ボーズ・オンラインストア

Mckee

HTML5 Advent Calendar 2011の13日目です。
本来のAdvent Calendarとは、12月1日からクリスマスの25日まで、カードに作られた窓を1日に1つずつ開けていくというものです。一方、技術系のAdvent Calendarは、12月1日から25日までの間、毎日違う人が特定のテーマに沿ってブログ記事を書くというものです。ここでは、「HTML5」がテーマになります。他にも面白い記事が公開される予定ですので興味のある方は是非チェックしてみてください。http://atnd.org/events/21987

JavaScriptなし」で制作した前回に打って変わって、今回はVideoCanvasIndexed Database APIを使い、JavaScript満載で制作しました。

概要

今回はそのサンプルを題材に、VideoCanvas連携させた描画の紹介と、Indexed Database APIを使ったデータのローカル保存についてご紹介します。

※対応ブラウザ:Chrome 12~
※Firefoxはお絵かきアプリに使用している offsetX, offsetYプロパティが未実装。
※「オフラインでも使える!!」のは動画が読み込まれている場合に限ります。
※サンプルコードは紹介用に編集してありますので、
動作しているコードと異なる場合があります。

サンプルアプリケーションについて

動画を好きなシーンで止めて、そこに落書きをして画像として保存できます。保存したデータはリストアップして出来栄えを確認して、個別に削除したり、全て削除したりできます。お気に入りの画像をクリックすると、実寸サイズのページに遷移しますので、「名前をつけて保存」しましょう。

サンプルアプリケーション

機能

左から順番にボタンを使っていくとわかりやすいと思います。

名前 機能
Play 動画再生とcanvasへの描画を開始します。
Pause 動画再生とcanvasへの描画を停止します。
Save data 現在描画中のデータを保存します。
Clear 現在描画中の絵部分をクリア
Truncate table 保存済みデータを全て削除
Delete (保存済みリストの各データに付随して表示) 押された項目を削除
Color palettes 絵の線色を変更
絵が描ける 絵が描けます

VideoとCanvasの連携

動画を Canvas に描画する

HTML

<video id="video">
	<source src="video/blop.mp4" type='video/mp4' width="480" height="320">
	<p>HTML5 Videoに対応したブラウザで御覧ください</p>
</video>
<article id="editer">
	<canvas id="canvas" width="480" height="320"></canvas>
</article>

動画形式

各ブラウザ毎に操作できる動画形式が統一されていません。対応状況は以下の通りです。

ブラウザ 形式
Chrome WebM(webm) , Ogg Theora(ogv)
Firefox WebM(webm) , Ogg Theora(ogv)
Safari H.264(mp4)
Opera WebM(webm) , Ogg Theora(ogv)
IE H.264(mp4)

※参考
Google ChromeがH.264対応をとりやめ、WebM推進を鮮明に

また、未対応の形式が対応可能になるプラグインが色々用意されているようです。
ChromeでWMPを再生可能にするプラグイン「Windows Media Player HTML5 Extension for Chrome」や、FirefoxでH.246形式を再性能にする「HTML5 Extension for Windows Media Player Firefox Plug-in」、IEでWebMを再生可能にするプラグイン「WebM for IE9」などがあるようです。
(どれも未検証)

JavaScript

動画をCanvasに描画するのは意外に簡単で、5行もあれば書けてしまいます。
下記、ごちゃごちゃと書いていますが、要は
videoタグから取得した HTMLVideoElement を canvas.drawImage() に渡してあげるだけです。

$.canvas = {
	"cnvs" : '',
	"ctx" : '',
}; 
$.video = {
	"ms" : 0,
	"startTimer" : function() {
		var interval = 100;
		timer = setTimeout(function() {
			$.video.ms += interval;

			/**
			* @param Object<br />
			* HTMLImageElement | HTMLCanvasElement | HTMLVideoElement
			* @param Number 描画イメージ矩形のx座標
			* @param Number 描画イメージ矩形のY座標
			* @param Number イメージを描画する幅(初期値はイメージ本来の幅)
			* @param Number イメージを描画する高さ(初期値はイメージ本来の高さ)
			*/
			$.canvas.ctx.drawImage(video, 0, 0);
			var data = $.canvas.cnvs.toDataURL();
			var indexData = {
				"ms" : $.video.ms,
				"video" : data
			};
			dataStrage.push(indexData);
			$.video.startTimer();
		}, interval);
	}
};
$(function() {
	var video = $("#video")[0];
	$.canvas.cnvs = $('#canvas')[0];
	$.canvas.ctx = canvas.getContext('2d');
});

動画の再生に合わせて、100ミリ秒ごとに Canvas に再描画しているという処理です。
本当は動画のフレームレートとかに合わせて実行間隔を設定するのが良さそうですが、大雑把に100ミリ秒にしました。
drawImage()の使い方は、まず第一引数に指定できる要素は「 img, canvas, video 」で、第二引数以降で、描画座標を設定します。

canvasに絵を描く

これはよくあるお絵かきアプリです。カラーパレットで線色を変更できます。

HTML

<article id="editer">
	<canvas id="canvas" width="480" height="320"></canvas>
</article>
<div id="colors">
	<ul>
		<li class="color" style="background:#FFFFFF"></li>
		<li class="color" style="background:#DCDDDD"></li>
		<li class="color" style="background:#9EA1A3"></li>
		<li class="color" style="background:#2B2B2B"></li>
		<li class="color" style="background:#D9333F"></li>
		<li class="color" style="background:#762F07"></li>
		<li class="color" style="background:#F5B199"></li>
		<li class="color" style="background:#FFDB4F"></li>
		<li class="color" style="background:#2F5D50"></li>
		<li class="color" style="background:#89C3EB"></li>
		<li class="color" style="background:#706CAA"></li>
	</ul>
</div>

JavaScript

やっているのは大きく下記の2つのことだけです。
マウスボタンを押した座標から、マウスが移動した座標へと線を引くことと、
パレットに設定されている色を、線色に設定することです。

$.canvas.ctx.globalCompositeOperation = "source-over";

$.canvas.ctx.lineWidth = 5;
$.canvas.ctx.strokeStyle = '#9eala3';

var down = false;
$(canvas).mousedown(function (e) {
	down = true;
	$.canvas.ctx.beginPath();
	$.canvas.ctx.moveTo(e.offsetX, e.offsetY);
});
$(canvas).mousemove(function (e) {
	if (!down) {
		return;
	}
	$.canvas.ctx.lineTo(e.offsetX, e.offsetY);
	$.canvas.ctx.stroke();
});
$(window).mouseup(function (e) {
	if (!down) {
		return;
	}
	$.canvas.ctx.lineTo(e.offsetX, e.offsetY);
	$.canvas.ctx.stroke();
	$.canvas.ctx.closePath();
	down = false;
});
var colors = $('#colors');
$.each(colors, function() {
	$('.color', this).click(function() {
		var color = $(this).attr('style').replace(/^background:/, '');
		$.canvas.ctx.strokeStyle = color;
	});
});

Canvasへの線描画は次の流れで行います。
1. パス開始を宣言: ctx.beginPath();
2. 描画開始位置を指定: ctx.moveTo(Number, Number);
3. 描画終端位置を指定: ctx.lineTo(Number, Number);
※2, 3を繰り返せる
4. パス終了を宣言: ctx.closePath();
5. 描画: ctx.stroke();

globalCompositeOperation は描画するオブジェクトの重なりに関する指定です。
※デフォルトは”source-over“です。

▼参考
globalCompositeOperation プロパティ – Canvasリファレンス – HTML5.JP

描画データをIndexed Database APIで操作する

SQLインジェクション対策として、関数オブジェクトは格納できません
またトランザクション処理をサポートしています。
現在実装が進んでいるものは非同期処理のみで、同期処理はまだ実装されていません

基本的な使い方

成功・失敗時に実行したい処理を事前に登録しておき、データベースと接続する度に判定を行い、その可否で登録しておいた処理が実行されるようなコールバック形式をとります。

初期化

"init" : function() {
	//ベンダープレフィックス対応 FF, Chrome
	$.indexeddb.IDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.moz_indexedDB;
	
	/**
	 * @param String DB Name
	 * @return Object IDBRequest
	 */
	$.indexeddb.req = $.indexeddb.IDB.open("videoLibrary");
	
	//ベンダープレフィックス対応 FF, Chrome
	$.indexeddb.IDBTransaction = window.IDBTransacrion || window.webkitIDBTransaction;
	
	//成功時コールバック
	$.indexeddb.req.onsuccess = function(evt) {
		$.indexeddb.db = evt.target.result;
		$.indexeddb.createObjecStore();
	}
	
	//失敗時コールバック
	$.indexeddb.req.onerror = function(err) {
		alert(err.code + ":" + err.message);
	}
},

オブジェクトストア作成

"createObjecStore" : function() {
	var version = "1.1";
	if($.indexeddb.db.version != version) {
		var verReq = $.indexeddb.db.setVersion(version);
		verReq.onsuccess = function(evt) {
			try {
				/**
				 * 注意
				 * <pre>setVersion.onsuccess内でしか成功しない。</pre>
				 * @param String objectStoreの名前
				 * @param String | null キーの値、nullの場合はout-of-line key(保存するオブジェクト内の該当プロパティがキーとなる)
				 * @param autoincrementフラグ
				 * 
				 */
				var store = $.indexeddb.db.createObjectStore("video", {"keyPath" : "ms"}, false);

				/**
				 * @param インデックス名
				 * @param 対応するプロパティ
				 * @param ユニーク制約
				 */
				store.createIndex("ms", "ms", true);

			} catch(e) {
				console.log(e);
			}
		};
		verReq.onerror = function(e) {
			console.log(e + 'error');
		};
	}
},

setversion()

※setversion()の結果が成功した時にしか、実行できない処理があります。
▼createObjectStore()
オブジェクトストアの作成

▼deleteObjectStore()
オブジェクトストアの削除

▼createIndex()
インデックスの作成

▼deleteIndex()
インデックスの削除

createObjetStore()

引数にそれぞれ、(オブジェクトストア名、キー、autoincremantフラグ)を設定します。
この時に、キーの指定があれば in-line key、無ければ out-of-line key となり、それぞれ下記のような特徴がある。

in-line key
データ内のプロパティキーとします。データ追加時には、キーとしたプロパティに値が存在する必要があります。
autoIncrementtrue の場合、キーとしたプロパティは存在しなくても問題ありません

out-of-line key
データとは別にキーを持ちます。データ追加時には、データの他にキーを渡す必要があります。
autoIncrementtrue の場合、別途キーを渡す必要はありません

保存

※関数オブジェクトは保存できません。

"putData" : function(data) {
	if($.isEmptyObject($.indexeddb.db)) {
		return false;
	}
	
	//トランザクション開始とオブジェクトストアの取得
	var store = $.indexeddb.db.transaction([], $.indexeddb.IDBTransaction.READ_WRITE).objectStore("video");
	
	var result = store.put(data);
	result.onsuccess = function() {
		return true;
	};
	result.onerror = function(e) {
		console.log(e);
		console.log('error');
	};
	
},

取得

キーの指定で特定のデータを取得できます。

"getData" : function(key) {
	if($.isEmptyObject($.indexeddb.db)) {
		return false;
	}
	var store = $.indexeddb.db.transaction([], $.indexeddb.IDBTransaction.READ_WRITE).objectStore("video");
	var req = store.get(key);
	req.onsuccess = function(evt) {
		var value = evt.target.result;
	}
},

全件取得するには、接続が成功している間は、繰り替えし処理を実行してデータを取得していきます。

"getDataMulti" : function() {
	if($.isEmptyObject($.indexeddb.db)) {
		return false;
	}
	var store = $.indexeddb.db.transaction([]).objectStore("video");
	var req = store.openCursor();
	var storedData = [];
	
	req.onsuccess = function(evt) {
		var cursor = evt.target.result;
		
		if(cursor) {
			storedData.push(cursor.value);
		} else {
			return false;
		}
		cursor.continue();
	}
	return storedData;
},

削除

ポイントは2点
・トランザクションのモードを「 READ_WRITE 」にしておく
削除する keyオブジェクトストア作成時に指定した key を設定する

"deleteData" : function(key) {
	if($.isEmptyObject($.indexeddb.db)) {
		return false;
	}
	var store = $.indexeddb.db.transaction([], $.indexeddb.IDBTransaction.READ_WRITE).objectStore("video");
	
	var result = store.delete(key);
	result.onsuccess = function() {
		return true;
	};
	result.onerror = function(e) {
		console.log(e);
		console.log('error');
	};
},

オブジェクトストアの削除が限られた場所でしか実行できないので、いつでも全データを削除できるように、データを1つずつ削除していくメソッドを用意しました。

"truncate" : function() {
	var store = $.indexeddb.db.transaction([], $.indexeddb.IDBTransaction.READ_WRITE).objectStore("video");
	var req = store.openCursor();
	
	var listBlock = $('#list');
			
	req.onsuccess = function(evt) {
		var cursor = evt.target.result;
		
		if(cursor) {
			var v = cursor.value;
			
			var delReq = store.delete(v.ms);
			delReq.onsuccess = function() {}
			delReq.onerror = function() {
				console.log(v.ms);
				console.log('Delete failed');
			}
		} else {
			console.log('no data');
			return false;
		}
		cursor.continue();
	}
}

DB自体を削除するメソッドはうまくいきませんでした。

感想

今回のサンプルは、当初、今とは少し違うアプリケーションになる予定でした。
VideoからCanvasに描画して、それにさらに絵を描いたデータをIndexedDBに保存する部分までは同じなのですが、IndexedDBに保存したデータを、動画の描画間隔と同じ間隔で、別のCanvasに再描画して動画のように見せるという簡易動画編集をやりたかったのです。技術力が足りずに最後の調整が間に合いませんでした。
調整が間に合わなかったのは残念でしたが、仕様が固まっていくにつれ、それらを使ったサイトやサービスが増えていくのは非常に楽しいです。私も今後より一層精進してまいります。

追記1

技術評論社さまのHTML5 Advent Calendar 2011の電子書籍に当記事も掲載されました。
ユーザー登録が必要になりますが、無料でiPadなどで読むことが出来ます。
こちら

追記2

その後、完全版を作りました。
動画に落書きできる!!簡易動画編集アプリ

Subscribe:
pagetop