まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:html5

HTML5 Canvas を使って1年間かけて一周する「年間時計」を作ってみました:
https://dotnsf.github.io/yearclock/
2019112300


表示すると1分で一周する秒針と、1年で一周する年針(?)が表示されます。年針は針というよりはパイチャートの要領で面積で表示されます。ちなみに上図は 11 月後半に表示したものです。もうかなり進んでいますね。。

仕組みはそれほど複雑ではなく、HTML5 Canvas 内に円と弧を描画した上で秒針を setInterval() を使って1秒おきに描画しています。画像の透明度をうまく使って秒針の残像が残る形にしています。パックマン型に弧を描画する技術については先日公開したこちらのエントリを参照ください:
Canvas でパックマン型に塗りつぶした弧を描く


計算上では(うるう年でない年は)毎年 11月25日 の正午が一年間の 90% を経過する瞬間になります。今年も残すはあと1割!

HTML5 の Canvas を使うことで HTML の画面内にコンテキストを利用した図形を比較的自由に描くことができるようになります。この機能の1つである arc() 関数を使うと、下図のような「円の弧」を描画できます(塗りつぶすかどうかは選択できます):
2019112203


この弧を描く際のコードは以下のようなものです:
  :
<script>
  var canvas1 = document.getElementById( 'myCanvas1' );
  if( !canvas1 || !canvas1.getContext ){
    return false;
  }
  var ctx1 = canvas1.getContext( '2d' );

  //. 円の情報
  var r = 80;
  var x0 = 100;
  var y0 = 100;

  var deg = 190;
  ctx1.beginPath();
  ctx1.arc( x0, y0, r, -90 * Math.PI / 180, deg * Math.PI / 180, false );
  ctx1.fillStyle = "rgba( 255, 128, 128, 0.8 )";
  ctx1.fill();
</script>

<body>
<canvas id="myCanvas1"></canvas>
</body>
  :

で、このような図形を描くのが目的であればいいのですが、パックマンのような(円からピザの一部を切り取ったような)画像を描きたい場合はこれでは目的の画像とは異なります。なんとかしてパックマン型に塗りつぶした弧を描く方法はないでしょうか? というのが今回のテーマです。





2019112202


結論としては可能で、「arc() 関数の実行直後に円中心に向かって直線を引く」のがその答えになります:

  :
<script>
  var canvas1 = document.getElementById( 'myCanvas1' );
  if( !canvas1 || !canvas1.getContext ){
    return false;
  }
  var ctx1 = canvas1.getContext( '2d' );

  //. 円の情報
  var r = 80;
  var x0 = 100;
  var y0 = 100;

  var deg = 190;
  ctx1.beginPath();
  ctx1.arc( x0, y0, r, -90 * Math.PI / 180, deg * Math.PI / 180, false );
  ctx1.lineTo( x0, y0 );
  ctx1.fillStyle = "rgba( 255, 128, 128, 0.8 )";
  ctx1.fill();
</script>

<body>
<canvas id="myCanvas1"></canvas>
</body>
  :

これでパックマンが描画できました:
2019112200

 

Web アプリでドラッグ&ドロップ(以下 DnD と表記)すること自体はできるようになりました。HTML5 では DnD できる要素が多く定義されていたり、HTML5 以前にも mouseup イベントや mousedown イベントをハンドリングすることで独自に実装することは可能でした。 

ただモバイル Web で、つまりスマホのウェブブラウザから DnD を行うことはまだ困難が伴います。そもそもスマホの小さい画面で DnD を行うことが使いやすいかどうか、という根本的な問題もあると思っていて、それが理由かどうかは定かではありませんが、HTML5 の DnD API の多くはスマホブラウザからは使えないことが多いようです。

そういった事情を理解した上で、それでも現状でどこまでできるだろうか? という観点で実現方法を考えて実装し、公開してみました:
https://dotnsf.github.io/mobile_dnd_sample/


上記 URL にスマホのブラウザでアクセスすると以下のような画面になります。4角の枠内に5枚の付箋が貼られているイメージです:
2019111701


各付箋は指でドラッグして位置を変えることができるようにしています。下図はピンクの「ハロー」と書かれた付箋をドラッグして位置を変更した後の様子です:
2019111702


また各付箋はダブルタップすると編集モードになり、書かれた文字を編集することができるようになります。「ハロー」を編集して「ハロー!」にしてみました:
2019111703


編集モードで「OK」をタップすると編集後の文字列が反映されます:
2019111704


付箋を削除する場合は、一度ダブルタップして編集モードにして、「DELETE」をタップします:
2019111705


確認後に削除されて元の画面に戻ります。「abc」と書かれていた付箋が削除されました:
2019111706


新しい付箋を追加する場合は「NEW」をタップし、編集モードで内容を入力します:
2019111707


そして「OK」をタップすると新しい付箋が追加されます。この付箋も同様にドラッグして位置を変えたり、再度編集して内容を変更することができます:
2019111708



一連の機能紹介は以上です。一応スマホのウェブブラウザでもドラッグ&ドロップによる UI が実現できました。サンプルでは全て JavaScript を使ってフロントエンドだけで実現していますが、データベースとのバックエンド連携を加えることで永続化なども実現できると思っています。

詳しくは後述のソースコードを参照いただきたいのですが、今回のサンプルでは HTML5 Canvas を使って、Canvas 内に付箋に相当するオブジェクトを描画して実現しています。Canvas 内の Touch イベントを監視してドラッグを処理しています。また編集モード画面は Bootstrap のモーダルダイアログを使っています。


ソースコードはこちらです。実態は index.html ファイル1つで、全ての HTML と JavaScript がこの中に含まれています:
https://github.com/dotnsf/mobile_dnd_sample



カスタマイズネタでテトリスを扱うことがあるのですが、その時のベースに使っているのがこちらです:
https://github.com/dionyziz/canvas-tetris

Dionysis Zindros さんが HTML5 Canvas で作ったテトリスです。Canvas 、JavaScript 、そして CSS でテトリスが再現されていて、MIT ライセンスで公開されています。実際に遊ぶ時にもブラウザで index.html ファイルを開くだけ(index.html ファイルは HTTP サーバー上にアップロードされている必要はなく、ローカルファイルとして開くだけでも可)で遊べます。

一応、遊び方にも触れておきます。index.html ファイルをブラウザで開くと以下のような画面になります。この「Play」ボタンをクリックするとテトリスがスタートします:
2019061703


操作はキーボードを使います。矢印の左右でテトリミノ(ピース)を左右に移動、上矢印で回転、下矢印で強制的に下に移動します:
2019061704



といった背景もあって実際に遊ぶのはもちろん、改良する場合においても技術的&ライセンス的なハードルが低く、デモやハンズオンも含めたセミナーのネタとして重宝しています。


そんなわけで、今日のブログエントリはこのテトリスを Obniz で操作する、というカスタマイズの紹介です。


Obniz (オブナイズ)をご存じない人のために簡単に紹介しておきます。Obniz は 2017 年に Kickstarter でクラウドファンディングが開始された IoT ボードです。Arduino や Raspberry Pi(Zero) と比較しても小型で、単体ではダイヤル式のスイッチと簡単な文字が表示できるディスプレイを入出力装置として持っています。これ以外に標準で無線 LAN および Bluetooth につながることができ(そのための設定をダイヤル式スイッチで指定します)、そして 12 個の GPIO によって各種センサーやモーターを接続することができます。MicroUSB からの給電により、(単体では)最大 1.8A の電流で稼働します。

そして最大の特徴といえるのが JavaScript による API の提供です。HTML や Node.js から利用可能な JavaScript でこれらの入出力装置の状態を監視したり、値を入力/設定/出力することができます。Obniz の JavaScript そのものの紹介が本ブログエントリの目的ではないので、詳しくは公式ドキュメントを参照してください。ググればネット上にサンプルを含めた使い方がたくさん見つかりますが、要するに「ウェブ(ページ)との連携が容易なボード」なのです:
https://obniz.io/doc/root





この Obniz ボードとの連携として、ボードのダイヤルスイッチでテトリミノを動かせるように改良しました。そのコードがこちらです:
https://github.com/dotnsf/canvas-tetris


こちらのコードをダウンロード(または git clone)して、Obniz を WiFi に接続後にブラウザで index.html ファイルを開いてください。すると最初に以下のような画面になって Obniz ID の指定を促されるので所有する Obniz の ID を指定します:
2019061702


そして "Play" ボタンをクリックでゲームスタートです。ここでは Obniz のダイヤルスイッチを使って、以下のように操作することができます:
ダイヤルスイッチ操作
左に回す左移動
右に回す右移動
押し込む回転


実際に遊んでいる様子がこちらです(下手すぎるのは撮影しながらだから、です・・・):



この仕組みのすごい所は「ゲーム画面とコントローラー(Obniz)が離れていても実現できる」点です。↑の動画では Obniz のある場所でゲーム画面を開いていますが、たとえばゲーム画面は地球の裏側で見られていたり、コンサート会場の大型スクリーンなどに表示されていてもいいわけです。そんな仕組みを簡単に実現できてしまっている、という点が素晴らしいと感じてます。

ちなみにカスタマイズの肝となっている箇所のコードはこちらです。js/controller.js ファイル内に以下のコードを追加して、Obniz スイッチの変更イベントを obniz.switch.onchange でハンドリングし、その内容に応じて対応するキーが押されたようにイベントを発生させています:
  :
//. obniz
var obniz = new Obniz("XXXX-XXXX"); //. <-- Edit with your own Obniz ID
obniz.onconnect = async function(){
  obniz.display.clear();
  obniz.display.print( "Obniz TETRIS" );

  obniz.switch.onchange = function( state ){
    obniz.display.clear();
    obniz.display.print( state );

    switch( state ){
    case 'left':
      keyPress( 'left' );
      render();
      break;
    case 'right':
      keyPress( 'right' );
      render();
      break;
    case 'push':
      keyPress( 'rotate' );
      render();
      break;
    default:
    }
  }
}

この Obniz の JavaScript SDK は本当に強力で、簡単にこういうことが実現できちゃいます。LED やセンサーを繋いだり、モーターを繋いだりしなくても、手軽にこんな使い方が実現できちゃうという、このポテンシャルの高さが魅力で気に入ってます。



楽器苦手なオッサンエンジニアの Web Audio API 勉強シリーズ(!?)、前回はマイクから入力した音声をオーディオバッファ・インスタンスに変換して再生するコードを紹介しました。今回は前回作成したコードを改良して、音声データを折れ線グラフで可視化することに調整してみました。

作成したコードはこんな感じです。前回のものからの差分をで示しています:
<html>
<head>
<title>Audio Buffer Chart</title>
<script src="https://code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.1.4/Chart.min.js"></script>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

window.onload = function(){
}

var processor = null;
var num = 0;
var duration = 0.0;
var length = 0;
var sampleRate = 0;
var floatData = null;
function handleSuccess( stream ){
  var source = context.createBufferSource();
  var input = context.createMediaStreamSource( stream );
  processor = context.createScriptProcessor( 1024, 1, 1 );
  
  //window.dotnsf_hack_for_mozzila = input;
  input.connect( processor );
  processor.onaudioprocess = function( e ){
    //. 音声データ
    var inputdata = e.inputBuffer.getChannelData(0);
    //console.log( inputdata );

    if( !num ){
      num = e.inputBuffer.numberOfChannels;
      floatData = new Array(num);
      for( var i = 0; i < num; i ++ ){
        floatData[i] = [];
      }
      sampleRate = e.inputBuffer.sampleRate;
    }
    
    var float32Array = e.inputBuffer.getChannelData( 0 );
    if( availableData( float32Array ) ){
      duration += e.inputBuffer.duration;
      length += e.inputBuffer.length;
      for( var i = 0; i < num ; i ++ ){
        float32Array = e.inputBuffer.getChannelData( i );
        Array.prototype.push.apply( floatData[i], float32Array );
      }
    }
  };
  processor.connect( context.destination );
}

function startRec(){
  $('#recBtn').css( 'display', 'none' );
  $('#stopBtn').css( 'display', 'block' );

  navigator.mediaDevices.getUserMedia( { audio: true } ).then( handleSuccess );
}

function stopRec(){
  $('#recBtn').css( 'display', 'block' );
  $('#stopBtn').css( 'display', 'none' );

  if( processor ){
    processor.disconnect();
    processor.onaudioprocess = null;
    processor = null;
    
    var audioBuffer = context.createBuffer( num, length, sampleRate );
    for( var i = 0; i < num; i ++ ){
      audioBuffer.getChannelData( i ).set( floatData[i] );
    }
    
    console.log( audioBuffer ); //. これを再生する
    
    var source = context.createBufferSource();

    source.buffer = audioBuffer;           //. オーディオデータの実体(AudioBuffer インスタンス)
    source.loop = false;                   //. ループ再生するか?
    source.loopStart = 0;                  //. オーディオ開始位置(秒単位)
    source.loopEnd = audioBuffer.duration; //. オーディオ終了位置(秒単位)
    source.playbackRate.value = 1.0;       //. 再生速度&ピッチ

    source.connect( context.destination );

    //. for lagacy browsers
    source.start( 0 );
    source.onended = function( event ){
      //. イベントハンドラ削除
      source.onended = null;
      document.onkeydown = null;
      num = 0;
      duration = 0.0;
      length = 0;

      //. オーディオ終了
      source.stop( 0 );

      console.log( 'audio stopped.' );
    };
    
    //. floatData[] (の先頭の一部)をグラフ描画する
    var dotnum = 1024;
    var ctx = document.getElementById( 'myChart' ).getContext( '2d' );
    var labels = [];
    var datasets = [];
    var colors = [ "rgba( 255, 0, 0, 0.4 )", "rgba( 0, 0, 255, 0.4 )" ];
    for( var i = 0; i < dotnum; i ++ ){
      labels.push( "" + ( i + 1 ) );
    }
    for( var i = 0; i < num; i ++ ){
      datasets.push({
        label: "data " + i,
        data: floatData[i].slice(1024,1024+dotnum),
        backgroundColor: colors[i%2]
      });
    }
    
    var myChart = new Chart( ctx, {
      type: 'line',
      data: {
        labels: labels,
        datasets: datasets
      }
    });
  }
}

function availableData( arr ){
  var b = false;
  for( var i = 0; i < arr.length && !b; i ++ ){
    b = ( arr[i] != 0 );
  }
  
  return b;
}
</script>
</head>
<body>
  <div id="page">
    <div>
      <h2>オーディオバッファ視覚化</h2>
      <input type="button" id="recBtn" value="Rec" onClick="startRec();" style="display:block;"/>
      <input type="button" id="stopBtn" value="Stop" onClick="stopRec();" style="display:none;"/>
    </div>
    <div>
      <canvas id="myChart"></canvas>
    </div>
  </div>
</body>
</html>

本当は再生する音声データ全てを折れ線グラフとして表示できるとよかったのですが、数10万~数100万個の配列データを扱うことになり、処理がかなり重くなる(見栄えもつまりすぎて折れ線に見えなくなる)ことがわかったので、上記では全データの 1024 個目から 500 個だけ取り出して折れ線グラフにするようなコードにしています(後述のように、このくらいだとかろうじて折れ線に見えます)。

変更点としてはまず HTML ボディ内に折れ線グラフを表示するための canvas 要素を記述しています。そしてこの canvas の中に Chart.js を使って折れ線グラフを描いていきます。今回は Chart.js の CDN を使ってロードしています。

今回の例ではマイクから入力した音声データ(波形データ)は floatData[] 配列に格納しています。入力がモノラルの場合は floatData[0] に、ステレオの場合は floatData[0] (左)と floatData[1] (右)に、それぞれ波形データが配列で格納されます。この中身を折れ線表示すればよいことになります。

※マイクからの入力であれば、このように波形データをあらかじめ配列に格納させておくことができますが、オーディオファイルを再生させる場合であればオーディオバッファ・インスタンスを取得してから
  audioBuffer.getChannelData( n );  ※ n: 0 または 1
を実行することで同じ配列を取り出すことができます。


このコードを実行して、マイクから音声入力するとこんな感じで音声が折れ線グラフ表示されます(以下はモノラルの例):
2018100201


最初はチンプンカンプンでしたが、だんだんオーディオバッファの中身がわかってきました。 v(^o^)


このページのトップヘ