20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スマホで砂に絵を描けるWebサービスをWebGLで作ってみた

Last updated at Posted at 2018-05-06

"砂"に描いた絵や文字って何故か、喜びだったり哀愁のような人の思いを感じさせます。
ということで、"砂"に文字や絵を描けるWebサービスを作ってみました。
砂の凹凸はWebGLを使って3Dで表現しています。

タッチイベントにも対応しているので、スマホでアクセスすれば、指を使って砂に絵を描くことができます。
また描いた絵は、そのまま画像付きでTwitterに投稿できるようにしています。

Pittura
※元々heroku上で動かしていましたが、Github Pagesに載せ替えて動かしています。
 アップロードやツイート等の機能はすべて外しています。
https://negi141.github.io/pittura-demo/

▽技術的に詰まった箇所や、ややこしい箇所を書いていきます。
目次:

  1. 実行環境
  2. iOS、AndroidのWebGL対応状況
  3. Canvas上のタッチイベント処理について
  4. 凹凸の表現について
  5. 波の表現について
  6. 画像付きでツイートするコード

実行環境

  • サーバー: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要素のマウス・タッチイベントのハンドリングは、以下のような流れで実装しました。

  1. Canvasのタッチ・マウスイベント処理をバインドする
  2. スクロール位置を加えたCanvas要素の座標を取得
  3. ページ内のタッチ/マウス座標を取得 (タッチの場合はイベントオブジェクトtargetTouchesがあるかどうかで判定)
  4. Canvas内のタッチ/クリック座標を計算
  5. イベントのタイプにより、処理を行う (タッチ系のタイプはtouchstart, touchend, touchcancel, touchmove)
  6. 最後にe.preventDefault()で、イベントをキャンセルする (スマホでスクロールしないようにするため)
javascript(jQuery使用)
// 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)にしておき、タッチした中央は凹ませるために黒く塗り、周辺は盛るために白く塗ります。
また盛った部分はピクセル事にノイズをつけて、それっぽくしています。
sand1.png
sand2.png

波の表現について

今回、砂の文字を消すために"波"で消せるようにしています。
波の元となる映像は別途撮影なりしておき、
その合成は、かなり力技でやっています。
65枚の波の画像を事前に(Canvasの初回生成時に)テクスチャデータとして読み込んでおき、
それをボタンが押されたタイミングで、0.07秒刻みで切り替えることでアニメーションさせています。

javascript
// 事前に読み込んでおく
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];

波は、以下の設定で加算合成させて、それっぽく見えるようにしています。

javascript
waveMaterial = new THREE.MeshBasicMaterial({transparent: true, depthTest: false });
waveMaterial.blending = THREE.CustomBlending;
waveMaterial.blendEquation = THREE.AddEquation;
waveMaterial.blendSrc = THREE.DstColorFactor;
waveMaterial.blendDst = THREE.SrcColorFactor;

wav1.jpg wav2.png

画像付きでツイートするコード

javascript
// 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);
ruby
# 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

20
16
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?