"砂"に描いた絵や文字って何故か、喜びだったり哀愁のような人の思いを感じさせます。
ということで、"砂"に文字や絵を描けるWebサービスを作ってみました。
砂の凹凸はWebGLを使って3Dで表現しています。
タッチイベントにも対応しているので、スマホでアクセスすれば、指を使って砂に絵を描くことができます。
また描いた絵は、そのまま画像付きでTwitterに投稿できるようにしています。
Pittura
※元々heroku上で動かしていましたが、Github Pagesに載せ替えて動かしています。
アップロードやツイート等の機能はすべて外しています。
https://negi141.github.io/pittura-demo/
▽技術的に詰まった箇所や、ややこしい箇所を書いていきます。
目次:
実行環境
- サーバー:Heroku
- 言語/フレームワーク:Ruby On Rails
- DB:MySQL
- グラフィックス:WebGL(three.js)
iOS、AndroidのWebGL対応状況
iOSならiOS8以上、Androidなら基本的にはAndroid4以上のChromeであれば、WebGLが動作するようです。
(参考: https://tips.spacely.co.jp/spvr_devices/)
2016年12月の記事ですが、国内のスマホ全体では98%以上が対応とありますので、現時点では、ほとんどのスマホでWebGLは使えると考えて良さそうです。
念のため、Detector.jsを使って、ブラウザ・端末がWebGLに対応しているかの判定を行うのがよいです。
Canvas上のタッチイベント処理について
作成したCanvas要素のマウス・タッチイベントのハンドリングは、以下のような流れで実装しました。
- Canvasのタッチ・マウスイベント処理をバインドする
- スクロール位置を加えたCanvas要素の座標を取得
- ページ内のタッチ/マウス座標を取得 (タッチの場合はイベントオブジェクトtargetTouchesがあるかどうかで判定)
- Canvas内のタッチ/クリック座標を計算
- イベントのタイプにより、処理を行う (タッチ系のタイプはtouchstart, touchend, touchcancel, touchmove)
- 最後にe.preventDefault()で、イベントをキャンセルする (スマホでスクロールしないようにするため)
// Canvasのタッチ・マウスイベント処理をバインド
$("canvas").bind('click mousedown mouseup mousemove mouseleave mouseout touchstart touchmove touchend touchcancel', onEvent);
// スクロール位置も加えたCanvas要素の座標
var getElementPosition = function (elem){
var position = elem.getBoundingClientRect();
return {
left: window.scrollX + position.left,
top: window.scrollY + position.top
}
}
// イベント処理
var onEvent = function (e) {
// スクロール位置も加えたCanvas要素の座標
var canvasPos = getElementPosition(e.target)
// ページ内のタッチ/マウス座標
var pageMouse = { x: 0, y: 0 };
if (e.originalEvent && e.originalEvent.targetTouches && e.originalEvent.targetTouches.length != 0) {
pageMouse.x = e.originalEvent.targetTouches[0].pageX;
pageMouse.y = e.originalEvent.targetTouches[0].pageY;
} else {
pageMouse.x = e.pageX;
pageMouse.y = e.pageY;
}
// Canvas内のタッチ/クリック座標
var mouse = { x: 0, y: 0 };
mouse.x = Math.round(pageMouse.x - canvasPos.left);
mouse.y = Math.round(pageMouse.y - canvasPos.top);
switch (e.type) {
case 'mousedown':
case 'touchstart':
painting = true;
/*~~押下開始時の処理~~*/
break;
case 'mouseup':
case 'mouseout':
case 'mouseleave':
case 'touchend':
case 'touchcancel':
painting = false;
/*~~押下終了時の処理~~*/
break;
case 'mousemove':
case 'touchmove':
break;
}
if (painting) {
/*~~押下+移動時の処理~~*/
}
e.preventDefault();
};
凹凸の表現について
今回作成したサービスの肝の部分ですが、砂の凹凸の表現は、バンプマッピングをリアルタイムで更新して行っています。
砂部分がタッチされたら、画面上には表示していないCanvasにブラシで描いたような太い線を描画していき、それをバンプマップとして読み込ませています。
実際の砂に線を描くと、中央部分は凹み、周辺は少し盛り上がります。
そこで、バンプマップは最初はグレーrgba(200, 200, 200)にしておき、タッチした中央は凹ませるために黒く塗り、周辺は盛るために白く塗ります。
また盛った部分はピクセル事にノイズをつけて、それっぽくしています。
波の表現について
今回、砂の文字を消すために"波"で消せるようにしています。
波の元となる映像は別途撮影なりしておき、
その合成は、かなり力技でやっています。
65枚の波の画像を事前に(Canvasの初回生成時に)テクスチャデータとして読み込んでおき、
それをボタンが押されたタイミングで、0.07秒刻みで切り替えることでアニメーションさせています。
// 事前に読み込んでおく
waveTexture = [];
for(var i=1 ; i<=65 ; i++){
var t = new THREE.ImageUtils.loadTexture('/images/tex_wave/_' + i + '.jpg');
waveTexture.push(t);
}
//~(中略)~
// テクスチャ―を切り替える
waveMaterial.map = waveTexture[step];
波は、以下の設定で加算合成させて、それっぽく見えるようにしています。
waveMaterial = new THREE.MeshBasicMaterial({transparent: true, depthTest: false });
waveMaterial.blending = THREE.CustomBlending;
waveMaterial.blendEquation = THREE.AddEquation;
waveMaterial.blendSrc = THREE.DstColorFactor;
waveMaterial.blendDst = THREE.SrcColorFactor;
画像付きでツイートするコード
// preserveDrawingBuffeをtrueにする
var context = renderer.domElement.getContext("experimental-webgl", {preserveDrawingBuffer: true});
// base64でデータ取得
var dataUrl = renderer.domElement.toDataURL().replace(/^.*,/, '');
// hiddenフィールドに格納
$("#data_url").val(dataUrl);
// hiddenの格納が完了しないまま、submitされてしまうことがあるので、
// 確実にデータが入っているのを確認してからsubmitしています
setTimeout(function(){
if ($("#data_url").val() !== "") $("#data_post").submit();
}, 100);
# base64デコードしてファイル作成
File.open(fname, 'wb') do|f|
f.write(Base64.decode64(params['data_url']))
end
# Twitter Gemを使用
client = Twitter::REST::Client.new do |config|
config.consumer_key = Rails.application.secrets.twitter_api_key
config.consumer_secret = Rails.application.secrets.twitter_api_secret
config.access_token = current_user.token
config.access_token_secret = current_user.secret
end
# CommentにURLやハッシュタグを追加
str_out = params['comment'] + " www.~~ #~~"
# 作成した画像付きで投稿
client.update_with_media(str_out, open(fname))
以上です!
もし要望・バグ報告ありましたらTwitter @negi3d 宛まで m(_ _)m