お絵描きアプリと画像の保存処理の実装
こんにちは。内藤です。
HTML5でアプリを作成する場合、画像は予め用意されたpngファイルをそのまま表示することが普通で、アプリ側でエフェクトをかけたりすることはあまりありません。けれども、HTML5のCanvasを使うことでアプリ内で画像を作成する機能は作れるので、ここでは作った画像を画像ファイルとして扱い、保存する部分までの実装方法について説明します。
これを応用すれば、アプリで作成した画像をメールで転送したり、サーバーにアップロードしたり、といったことも可能です。
内部的には、JavaScriptでバイナリを扱うArrayBufferオブジェクトが登場しますので、これの使い方も参考にして下さい。
お絵描きの基本機能
まずは、Monacaから最小限のアプリを作り、JS/CSSコンポーネントよりjQuery(Monaca version)を入れておきます。
次のリストによりお絵描き機能を完成させます。
index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, user-scalable=no">
<script src="components/loader.js"></script>
<link rel="stylesheet" href="components/loader.css">
<link rel="stylesheet" href="style.css">
<script>
window.addEventListener('load',onLoad,false);
function onLoad() {
var width = $("body").width();
var height = $("body").height();
$("#my-canvas").attr("width",width-20);
$("#my-canvas").attr("height",height-60-100);
$(".buttons").height(100);
$(".buttons").css("top",height-100);
$(".mybutton").width( (width-44)/3 );
$(".mybutton").height( 100 );
var myCanvas = $("#my-canvas").get(0);
var myContext = myCanvas.getContext("2d");
myContext.strokeStyle = "red";
myContext.lineWidth = 5;
var startX = 0;
var startY = 0;
var offsetLeft = $("#my-canvas").offset().left;
var offsetTop = $("#my-canvas").offset().top;
$("#my-canvas").on("touchstart",function(event){
event.preventDefault();
var pageX = event.originalEvent.touches[0].pageX;
var pageY = event.originalEvent.touches[0].pageY;
startX = pageX - offsetLeft;
startY = pageY - offsetTop;
});
$("#my-canvas").on("touchmove",function(event){
var pageX = event.originalEvent.touches[0].pageX;
var pageY = event.originalEvent.touches[0].pageY;
var endX = pageX - offsetLeft;
var endY = pageY - offsetTop;
myContext.beginPath();
myContext.moveTo(startX, startY);
myContext.lineTo(endX, endY);
myContext.stroke();
startX = endX;
startY = endY;
});
}
function clearImage() {
var canvas = $("#my-canvas");
var myCanvas = canvas.get(0);
var myContext = myCanvas.getContext("2d");
myContext.clearRect( 0 , 0 , canvas.width(), canvas.height() );
}
</script>
</head>
<body>
<div class="buttons">
<div class="mybutton" onClick="saveImage()">save</div>
<div class="mybutton" onClick="loadImage()">load</div>
<div class="mybutton" onClick="clearImage()">clear</div>
</div>
<canvas id="my-canvas">
</body>
</html>
style.css
html {
height:100%;
}
body {
background: #DDDDDD;
margin:0px;
padding : 0px;
height: 100%;
}
#my-canvas {
margin-top : 30px;
margin-left : 10px;
padding : 0px;
background : #FFFFFF;
border: thin inset #AAAAAA;
}
.buttons {
position:absolute;
top:670px;
left:20px;
}
.mybutton {
position:relative;
float: left;
text-decoration: none;
text-align: center;
font-size: 20px;
display: block;
border: 1px solid #000;
width: 185px;
height:80px;
}
これで、画面をタップすると線が描けるようになります。
お絵描き機能は基本的なことしかやっていないのですが、内容を簡単に説明すると、次のようになります。
まず、#my-canvasでIDを付けたdivタグをキャンバス化します。これは、次のようにコンテキストを取得することで行います。
var myCanvas = $("#my-canvas").get(0);
var myContext = myCanvas.getContext("2d");
これにtouchstartイベントとtouchmoveイベントをひも付け、それぞれ、始点の設定処理と、線を描く処理を実装しています。具体的な作画部分は、次のようになります。
myContext.beginPath();
myContext.moveTo(startX, startY);
myContext.lineTo(endX, endY);
myContext.stroke();
注意事項は、
event.preventDefault();
の部分です。これがないと、イベントが上位層まで伝達されてしまい、キャンバスが安定しません。(タップ操作により独自に拡大表示したりしてしまう)
保存処理の実装
保存するためには、まずcanvasのデータをバイト列にする必要があります。そのためには、canvasにあるtoDataURL()メソッドを使います。このメソッドは、iOSでもAndroidでも利用出来るのですが、残念ながら、Android 2.3系の一部の機種では正常に動作しません。Android 2.3系の一部の機種(HTC Evoなど)では、同様の機能の処理を自分で実装する必要があります(後述)。
次に、toDataURL()で変換した画像ファイルは、基本的にbase64という書式で記述された文字列
になっています。この文字列をそのまま保存することも出来ますが、保存してもこれは「画像ファイル」にはなりません。ただの、文字列が記述されたファイル(テキストファイル)になってしまいます。
そこで、今度はbase64文字列を、バイナリに変換する必要があります。
次のコードをb64utils.jsで保存し、読み込むようにして下さい。
(function(){
// see
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Base64_encoding_and_decoding
function base64DecToArr (sBase64, nBlocksSize) {
var
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen);
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 & & nOutIdx < nOutLen; nMod3++, nOutIdx++) {
taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
}
nUint24 = 0;
}
}
return taBytes.buffer;
}
function b64ToUint6 (nChr) {
return nChr > 64 & & nChr < 91 ?
nChr - 65
: nChr > 96 & & nChr < 123 ?
nChr - 71
: nChr > 47 & & nChr < 58 ?
nChr + 4