2021年4月28日水曜日

Zoomの没入型ビューはオンライン講義の救世主となるか?

Zoomのバージョン5.6.3から,Immersive View(没入型ビュー)という機能が提供されるようになった.ちょっと違和感があるんじゃないの?(Zoomの新機能は「圧迫面接」?)と,最初に聞いた時点では感じたものだが,百聞は一見にしかず,実際にやってみたら,「これはひょっとしてオンライン講義のひとつのあり方としては便利な形かも……と考えるようになった.

実際に「ぼっち会議」を開催して,試してみた画面を次に示す.

空いている青い椅子で表されている席のところに参加学生のリアルタイム胸像が描かれるらしい.教室をそのままサイバー空間に再現したようにみえないだろうか.VR系のアプリや空間を意識させたオンラインチャットなど,似たようなアイデアのサービスはこれまでもあったが,私にはどれもイマイチで,これじゃない感があった.その点では,今回の没入型ビューは,(最後に述べる問題点を除くと)工夫が凝らされていて極めて自然な表現のようにみえる(まあ,実際に学生たちの像が出てくるとまた違和感も感じるのかもしれないが).

やり方

ところで,この没入型ビュー,ただZoomをアップデートしただけでは利用できない(注:契約形態によってはアップデートしただけでもOKとのことらしい.アップデートして「没入型ビュー」がメニューに出てこない方は,次の設定をお試しください).まず,利用できるようにするための設定を説明する.

まずは,アプリを最新版にアップデートしておこう.そうでないと話にならない.

やらねばならない作業は,WebインタフェースでZoomにログインして,設定を調整することである.設定項目から「Immersive View」を探して,オンにする.これをしないと没入型ビューを試してみることができない.

この設定をしてからアプリを起動し,自らが会議をホストすると,表示メニューに「没入型ビュー」が追加される.


このメニューを選ぶと,背景イメージを選ぶときと同様に,どのシーンに没入するかを選ぶことができる.シーンによって,対応する人数も異なる設定になっている.最大で25人ということなので,大人数講義には向かない.それでも,大きな一歩ではなかろうか.


残された課題

この機能があれば,学生が寝ながら聞いているんじゃないかという心配をする必要はないし,学生からしても部屋が散らかっているのを写してほしくないというプライバシー配慮にも対応できている.学生の反応を見ながら喋れるという話者側のニーズも満たすことができそうだ.

今までのギャラリービューで,バーチャル背景機能をうまく使うことでも,同等のニーズを満たすことはできた.しかし,それを要求するよい「言い訳」がなかった.今回のこの機能を使って教室を再現したいというリクエストは,うまい説明になるだろう.

ところで,冒頭の教室風没入シーンを見ていただきたい(再掲する).ひとつだけ,どうしても納得がいかない点が,少なくとも私の心のわだかまりとしては残っている.

それは,講師(教員,話者)「自体」が!こちらを向いてしまっているということだ.これはどう考えても変だろう?

もちろん,自分の姿がどう配信されているかモニタリングする必要があることは,これまでの経験でいやというほど分かっている.自分の姿は,画面共有しているときみたいに隅っこのほうにワイプで抜くとか,そんな方法のほうが,かえって自然なんじゃなかろうかと考えるが,いかがだろうか.

2021年4月27日火曜日

Zoomの新機能は「圧迫面接」?

Zoomの新バージョン(5.6.3)では,Immersive Viewという機能が実装されたとのニュースを教えていただいた.Immersive(没入的)というこの機能,オンライン会議の参加者が画面に全て埋め込まれたように表示され,擬似的に会議室で会議をしているように表示する,というものだそうだ.

しかし,この機能,どうにも感じる違和感がある.お分かりだろうか.

そう,参加者の向きである.皆が一様に同じ方向「だけ」を向いている会議なんてある?あるいはこれ,「私以外全部向こう側のヒト」なのかな.これなんて圧迫面接……

たぶん向こうの連中は違和感を憶えないんだろう.なんてったって「最後の晩餐」の文化圏だから.しかしこちとらお茶の間コントで育った世代である.やはりちゃぶ台を三方向から囲まないと,会議感は出ないんじゃなかなあ?


2021年4月26日月曜日

【東大「情報」講義用資料】ハミング距離の意義を直観的に理解する

ハミング距離を十分にとることによって,ある程度の誤り訂正ができるということを説明する.まずは次の図を見てほしい.

この図において,伝送したいメッセージはAとBの二種類である.AとBはそれぞれ,「1001011000101」および「1011011010010」というコードで符号化されている.

確認1:AとBのハミング距離は5であることを確かめよ

さて,残念ながら伝送路は雑音が多く,一定の確率でときどき誤りが生じ,ビットが反転してしまうという現象が生じるものとする.たとえば,Aを伝送したときに先頭から9ビットめが反転してしまったものがA’であり,さらに7ビットめも,すなわち7ビットめと9ビットめが反転してしまったものがA’’であるとする.

確認2:AとA’,およびAとA’’のハミング距離がそれぞれ1と2であることを確かめよ

同様に,Bから派生した誤りを含む符号がB’とB’’である.

ところで,上記において,とりあえずAとBしか今は考えていないので,A’やB’が観測されたときに,AまたはBから誤りが生じたものであることは明らかである.図では,AとBからのハミング距離を半径として,同心円が描かれているが,A’やA’’,あるいはB’やB’’といった誤りを含む符号はこの同心円状に配置される.

このとき,3個以上のビット誤りが生じてAとB双方へのハミング距離が3以上になってしまったらどうなるだろうか?その符号の元の符号はAなのかBなのか.

これは判定できない.

確認3:AとBへのハミング距離が3以上であるような符号を考え,その符号の元となった符号はAなのかBなのか,判定できないことを確かめよ

以上の理屈により,t個までの誤りが生じたときに,2t+1個のハミング距離を確保しておけば誤りを修正できる(元の符号を推定できる)ということになるのである.

Googleさん勘弁したってください

Googleから「不正使用されたパスワードを変更してください」っていうメールが飛んできて,そこには「ウェブ上であなたのパスワードの一部が検出されました。そのパスワードを見つけた人は誰でも、あなたのアカウントにアクセスできます。」とまあ,仰々しいメッセージが書かれている.かなわんなあとメールにあったアドレスにアクセスしてみると,「いくつかのパスワードが危ないでぇ?」っていうリストが並んでいた.

しかし,しかしである.

たいがいもう無効になっちゃったアカウントなんだよなあ.放置でおkっていうモノが並んでいて,羊頭狗肉とはまさにこのことか,という印象.

さらには,これはちょっとないんじゃないの?Googleさん.

sbp-crissalっていうのはHerokuで試験的に作ってみたアプリで,今はもう跡形もない.まあ,それはGoogleタンには分からないだろうから,百歩譲って,まあ,理解を示してあげるとして.

localhostて!localhostて!

重要なことなので二度いいました.

世界のGoogleとあろうもんが「192.168.0.1は私のアドレスだから使わないでください」っていう電波ゆんゆん氏と同じレベルの話をしてどーするよ?

開発用にテキトーに付けた使い捨てのアカウントが危ないって,知らねーよそんなん.勘弁したってや.

2021年4月22日木曜日

SPAを作ってみよう

SPAとは何か.SPAは,Single Page Application(シングルページ・アプリケーション)の頭文字をとったものである.シングルページ,つまり,ひとつのページで完結した比較的単純なアプリケーションのことをいう.

一般的なWebアプリケーションは,ネットワークの向こう側にあるサーバとユーザが操作するクライアントが連携し,ときにはページ遷移を伴いセッションを行う.SPAでは基本的にはページ遷移を行わない.

原則として,クライアントで動作するSPAはサーバとは密には連携せず(本プログラムで動画データを適宜ダウンロードしたり,ページ遷移を伴わないAJAX等の技術で逐次通信したりというように,全く切り離して運用するというわけではない),PWA(Progressive Web Application)のようにネットワークに接続されていなくてもある程度の範囲でサーバから独立して動作できるようなものもある.すなわち,一度,サーバからリソースを全てダウンロードして取得してしまえば,クライアント単独で動作させることも可能という形態のWebアプリである.

実装形態は様々だが,基本的に,コンポーネントの論理的な配置を示すHTML,デザインを指定するCSS,アプリケーションの動作,ロジックを記述するJavaScriptの3つがDOM(Document Object Model)を操作することでアプリケーションを実現する.最近ではAltJS(Alternative JavaScript,JavaScript代替品)と呼ばれる各種の言語で実装されることもある.

本稿では,簡単な映像編集アプリを想定して,SPAの開発手順を解説する.手順を追って説明するので,手元にソースコードを編集するエディタとプログラムの動作を確認するためのWebブラウザを用意して,ひとつひとつ確認しながら理解してほしい.

アプリの要求事項と技術的要件

まずは,今回,作り上げるアプリになにを求めるかを明確にしよう.「要求事項」を書き出すという作業である.システム開発は,まずこの段階から始める必要がある.

今回想定するアプリは,シンプルな映像操作アプリである.このアプリに求められる根本的な要件は,次のとおり.

  • いくつかの動画を連続して再生したい
  • その連続させる順序は,ドラッグ・アンド・ドロップの操作で直観的に指定したい

アプリを簡単に実現するために,動画データは作り付けとしよう.アップロードやダウンロードをできるようにしたいところだが,データの管理などサーバとの密な連携が必要になるので後回しとしたい.

続いて技術要件である.単純なSPAとはいえスクラッチから作る(0から作ること)のはいささかしんどい.まずは,どんな技術が使えるかを調べてみるところから始めよう.

調べてみると,コンポーネント(画面の部品)をグリッド配置して,マウス操作できるようにするにはMuuriというJSのライブラリを見つけた.これが使えそうだ(他にもいくつかあるようだが,とりあえずはこれを使ってみることにする).なお,Muuriを使うために,関連ライブラリが必要になるらしいのでそれも用意する.

JavaScriptの各種ライブラリは,都度,ネットからダウンロードして使うようにするやり方も一般的だが,今回はネットに繋がっていないときにでもローカルで開発作業を進められるように,ダウンロードしてローカル側に用意する方法をとった.そのあたりは,皆さんの環境に鑑みて,やりやすい方法を選んでほしい.

さらに,きれいな画面デザインを実現するために,BootstrapというCSSライブラリも用意する.HTML5で表現力が格段に向上したとはいえ,素のデザインはそっけなく,いささか頼りなさすら感じてしまう.本アプリでは,人気がありいろいろなところで使われているBootstrapを使うことにした.JavaScriptのライブラリ同様,Bootstrapもサイトからダウンロードしてローカルに参照することにする.

ステップ1

Muuriの使い方を理解して,白紙のページから粛々と開発していく方法もアリだろう.しかし,そのやり方は,やはり手間がかかる.Muuriをしっかり理解する必要があるし(まあ,これは最終的には必要なことではあるが),ちゃんと設定できないと動作確認すらできない.

そこで,サンプルプログラムを改造していくという方法がオススメしたい.今回の要求事項を満たすためには「Muuri が提供するKanbanという機能がちょうどよさそう」というようにアタリをつけて探してみると,CodePen というサービスに,良さそうなサンプルを見つけた.これを改造していくことを考えよう.

step01.zipをダウンロードして展開しよう(ちゃんと「展開」すること!Windowsの余計な機能に圧縮ファイルをメモリに展開して?そのままフォルダとしてアクセスできるという機能があるようだが,その状態でアクセスすると,CSSなどをうまく参照できず動かないので注意されたい).展開したなかにあるindex_01.htmlをクリックすると,CodePenのサンプル画面のようなSPAが実現されるはずである.アプリの構成は,同HTMLファイルの他,jsディレクトリの下に必要なJavaScriptコードが,cssディレクトリの下にCSSのコードが格納されている.それぞれ,script_01.jsとstyle_01.cssが,我々が修正を加えていくべきコードである.

なお,index_01.htmlをindex.htmlに,script_01.jsとstyle_01.cssをそれぞれscript.jsと style.cssにリネームし,index.htmlからそれぞれのコードを参照している箇所をscript.jsと style.cssに書き換えて作業を続けていくとよい.本稿では,適宜,それぞれのステップでその時点のスナップショットを用意しているが,実際には皆さんが修正を加えつつ,理解しながらコードを書き換えていくことを強くオススメする(次図は,ダウンロードした圧縮ファイルを展開してリネームした後の状態である).

ところで,表示したSPAの,各コンポーネントをドラッグ・アンド・ドロップ操作で動かして,順番を換えられることを確認しよう.Itemだけじゃないことに気付いただろうか?それぞれのカラムも動かして位置を変えることができるようになっている.

ステップ2

先のページを改造して,SPAとして仕上げる.ここで,最終的なゴールのイメージを確認しておこう.

最終的な画面のイメージは,次のようなものである.左側のカラムでサムネイルで表示されているアイテムを操作して,順序を決定,右側の大きなカラムで表示されている動画は,左側に並んだ順番に連続して再生される,というイメージである.

それでは,さっそく改造を施していくことにする.

まずは,CSSを書き換える.カラムの幅の調整である.カラムのデザインは,board-columnというクラスのデザインとして指定されている.

    ...

.board-column {

  position: absolute;

  left: 0;

  top: 0;

  padding: 0 10px;

  width: calc(100% / 5);

  z-index: 1;

}

.board-column.working {

  width: calc(100% * 4 / 5);

}

    ...

widthを,ウィンドウ全体の幅の1/5に調整するように指定しよう.また,右側のカラムには,サブクラスとしてworkingという名前が付いている.そこで,そちらのカラムは4/5になるようにしよう.これで,全体を5つに分けて,1:4の大きさで左右を分けることができるようになった.

もう一つ,細かいことだが,.board-column-headerの text-transform: uppercase;を削除しておこう.

    ...

.board-column-header {

  position: relative;

  height: 50px;

  line-height: 50px;

  overflow: hidden;

  padding: 0 20px;

  text-align: center;

  background: #333;

  color: #fff;

  border-radius: 5px 5px 0 0;

  font-weight: bold;

  letter-spacing: 0.5px;

  text-transformuppercase;

}

    ...

続いて,HTMLの書き換えである.まずは,<div  class="board-column done”>〜</div>をまるごと削除する.

    ...

      </div>

    </div>

  </div>

  <div class="board-column done">

    <div class="board-column-container">

      <div class="board-column-header">Done</div>

      <div class="board-column-content-wrapper">

        <div class="board-column-content">

          <div class="board-item"><div class="board-item-content"><span>Item #</span>11</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>12</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>13</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>14</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>15</div></div>

        </div>

      </div>

    </div>

  </div>

</div>


<script type='text/javascript' src='./js/web-animations.min.js'></script>

    ...

次に,<div  class="board-column working”>〜</div>の中身を削除する.

    ...

    </div>

  </div>

  <div class="board-column working">

    <div class="board-column-container">

      <div class="board-column-header">Working</div>

      <div class="board-column-content-wrapper">

        <div class="board-column-content">

          <div class="board-item"><div class="board-item-content"><span>Item #</span>6</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>7</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>8</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>9</div></div>

          <div class="board-item"><div class="board-item-content"><span>Item #</span>10</div></div>

        </div>

      </div>

    </div>

  </div>

  <div class="board-column done">

    <div class="board-column-container">

    ...

あとは,細かな修正ではあるが,board-column-headerの記述内容を Todo → Order,Working → Resultに書き換えておこう.

この操作で,画面は次のようになるはずだ.

ドラッグ・アンド・ドロップの操作はそのまま維持されていることも確認しておきたい.ここまでのソースコードのスナップショットをstep02.zipとして置いておくので参考にされたい.

ステップ3

さて,いよいよ動画を扱っていくのだが,動画ファイルそのものと,サムネイル画像のデータをダウンロードしておこう.dataFiles.zipををダウンロードし,index.htmlの置いてあるディレクトリに,data/src01.mp4 … となるように展開してほしい.正しい置き方は,次のようなものである(dataディレクトリの中には,全ての動画,画像データが並ぶようにすること).

サムネイルはタイトルと画像が表示されるようにしたい.そのためにはBootstrapのcardレイアウトが使えそうだ.ひとつのカードはヘッダ(card-header)とボディ(card-body)からなる(図).

Bootstrapを使うために,HTMLに次の行(太字部分)を追加する.

<!DOCTYPE html>

<html lang="en" >

<head>

  <meta charset="UTF-8">

  <title>SPA example</title>

  <link rel="stylesheet" href="./css/style.css">

  <link rel="stylesheet" href="./css/bootstrap.min.css">


</head>

    ...

続いて,Itemの中身をカードで表したアイテムに入れ替えよう(下記の太字部分).

    ...

  <div class="board-column todo">

    <div class="board-column-container">

      <div class="board-column-header">Order</div>

      <div class="board-column-content-wrapper">

        <div class="board-column-content">


          <div class="board-item" id="src01" data-id=1>

            <div class="board-item-content">

              <div class="card">

                <div class="card-header">Movie #1</div>

                <div class="card-body" style="padding: 0px;">

                  <img src="./data/src01.jpg" width="100%">

                </div>

              </div>

            </div>

          </div>

          <div class="board-item" id="src02" data-id=2>

            <div class="board-item-content">

              <div class="card">

                <div class="card-header">Movie #2</div>

                <div class="card-body" style="padding: 0px;">

                  <img src="./data/src02.jpg" width="100%">

                </div>

              </div>

            </div>

          </div>

          <div class="board-item" id="src03" data-id=3>

            <div class="board-item-content">

              <div class="card">

                <div class="card-header">Movie #3</div>

                <div class="card-body" style="padding: 0px;">

                  <img src="./data/src03.jpg" width="100%">

                </div>

              </div>

            </div>

          </div>

          <div class="board-item" id="src04" data-id=4>

            <div class="board-item-content">

              <div class="card">

                <div class="card-header">Movie #4</div>

                <div class="card-body" style="padding: 0px;">

                  <img src="./data/src04.jpg" width="100%">

                </div>

              </div>

            </div>

          </div>


        </div>

      </div>

    </div>

  </div>

    ...

idでファイル名の拡張子以外の部分を指定していたり,data-idという属性を付けていたりしているところは,このあとの作業への伏線である(実はdata-idは最後まで使っていない.ソート機能を実装するときにこれが使えるので,将来への布石である).

ここまでの修正で,サムネイルを表示させることができるようになった(図).

ちょっとだけ修正を加えよう.サムネイルの周りにある余白の白い部分は不要だろう.CSSを修正し,このパディングを削除する.修正すべき点は,board-item-contentクラスにある.paddingの指定を削除しよう.

    ...

.board-item-content {

  position: relative;

  padding: 20px;

  background: #fff;

  border-radius: 4px;

  font-size: 17px;

  cursor: pointer;

  -webkit-box-shadow: 0px 1px 3px 0 rgba(0,0,0,0.2);

  box-shadow: 0px 1px 3px 0 rgba(0,0,0,0.2);

}

    ...

これで,画面は次のようになったはず.とりあえずはこれでヨシということにしよう.

サムネイルをドラッグ・アンド・ドロップすることで順番を指定できるようになった.ここまでのスナップショットをstep03.zipとして置いておく.

ステップ4

さあ,いよいよ右側の動画表示に着手である.右側のカラムに,ビデオ表示の要素を追加する(次のコードの太字部分).moviePaneというIDが付けられている点に注意しておこう.これは,あとでJavaScriptのコードから参照される重要な情報である.

    ...

  <div class="board-column working">

    <div class="board-column-container">

      <div class="board-column-header">Result</div>

      <div class="board-column-content-wrapper">

        <div class="board-column-content">

          <div>

            <video id="moviePane" src="./data/src01.mp4"

                   width="100%" controls="true">

          </div>

        </div>

      </div>

    </div>

  </div>

    ...

この追加で,画面は次のようになるはずだ.

だいぶ形になってきた.ここまでで実現できたことをまとめておこう.

  • 動画の順序をD&DでコントロールするUIを実現できた
  • コントロール用のカラムと,動画を表示するカラムのデザインを実現できた
  • 動画(ひとつだけ)を表示することができた

次に考えるべきことは,一番最初(トップ)のアイテムが変化したときに,ビデオを入れ替えるようにしたい,それにはどうするか,ということだろう.トップの位置をどうやって取得するか,右側画面で動画を入れ替えるにはどうするか,といったあたりがポイントになりそうだ.

JavaScriptを読む

動画の操作を実現するまえに,Muuriのサンプルが提供するJavaScriptのコードをざっと眺めておきたい.全部を理解する必要はないが,重要なポイントは押さえておきたい.

var dragContainer = document.querySelector('.drag-container');

var itemContainers = [].slice.call(document.querySelectorAll('.board-column-content'));

var columnGrids = [];

var boardGrid;


// Init the column grids so we can drag those items around.

itemContainers.forEach(function (container) {

  var grid = new Muuri(container, {

    items: '.board-item',

    dragEnabled: true,

    dragSort: function () {

      return columnGrids;

    },

    dragContainer: dragContainer,

    dragAutoScroll: {

      targets: (item) => {

        return [

          { element: window, priority: 0 },

          { element: item.getGrid().getElement().parentNode, priority: 1 },

        ];

      }

    },

  })

  .on('dragInit', function (item) {

    item.getElement().style.width = item.getWidth() + 'px';

    item.getElement().style.height = item.getHeight() + 'px';

  })

  .on('dragReleaseEnd', function (item) {

    item.getElement().style.width = '';

    item.getElement().style.height = '';

    item.getGrid().refreshItems([item]);

  })

  .on('layoutStart', function () {

    boardGrid.refreshItems().layout();

  });


  columnGrids.push(grid);

});


// Init board grid so we can drag those columns around.

boardGrid = new Muuri('.board', {

  dragEnabled: true,

  dragHandle: '.board-column-header'

});

このコードは,CodePenが提供するKanbanサンプルのJavaScriptソースコードである.ここで,注目すべきは.on('dragReleaseEnd' ... )の部分だ.ここがポイントである.このメソッドは2つ引数をとり,最初の引数で対応するイベント,2つめの引数で,そのイベントが発火したときの処理を記述する.

ここの記述は,ドラッグ・アンド・ドロップが終わったときに呼び出される処理を記述しているということになる.したがって,順番の入れ替えによってメインの動画を書き換える処理は,ここに書いていけばよさそうだということが分かる.

動作の確認

試しに,次のようなデバッグプリントを入れてみる(太字部分).

    ...

  .on('dragReleaseEnd'function (item) {

    item.getElement().style.width = '';

    item.getElement().style.height = '';

    item.getGrid().refreshItems([item]);


    console.log('KOKO');

  })

    ...

このデバッグプリントを入れたことで,ドラッグ・アンド・ドロップの操作が終了した時点でコンソールにメッセージが出力されるはずである.

コンソールの出力を確認するには,Webブラウザの開発ツールを表示してそのコンソールで確かめてみればよい.Google ChromeならF12キーを押せば表示される.Safariなら,開発メニューから「Webインスペクタを表示」させればよい.

コンソールを見ながら,並べ替え操作を行うとどうなるか?「KOKO」というメッセージがコンソールに出力されることを確かめてほしい(図).


JavaScriptコードの追加

では,ドラッグ・アンド・ドロップで並べ替えを行なったときに右側に表示されているビデオを入れ替えるコードを考えていこう.動画を入れ替えるには,video要素のsrcを差し替えればよさそうだ.また,入れ替えるタイミングは一番上のサムネイルが入れ替わったときだけでよい.2つめ以降の順序が入れ替わったとしても,当面は右側の表示を変える必要はないからだ.

video要素を取得するには,document.getElementById('moviePane')とすればよい.当該要素にmoviePaneというIDを付けておいたことを思い出そう.その要素に対してソースを入れ替えればよいが,同じか変える必要があるかはファイル名だけで判定する.パス名は不要なので,パス区切りである「/」で分割して,一番最後の要素を取ってくれば適切な比較ができそうだ.そのために,次のようなメソッドisSrcDifferent()を用意する.

var dragContainer = document.querySelector('.drag-container');

var itemContainers = [].slice.call(document.querySelectorAll('.board-column-content'));

var columnGrids = [];

var boardGrid;


isSrcDifferent = function(a, b) {

  x = a.split("/").slice(-1)[0];

  y = b.split("/").slice(-1)[0];

  return (x != y);

}


// Init the column grids so we can drag those items around.

    ...

split("/")を用いてパス区切り文字「/」で区切り配列にし,slice(-1)で最後の要素のみからなる配列を取得,~[0]でその要素を取得するという手順である.引数に与える2つの文字列を比較し,違っていたらtrueを返すという仕様にした.

大域変数columnGridは,カラムごとにアイテムを管理するための配列である.その対象は次のようになっている.

したがって,columnGrid[0]はサムネイルの集合を含めたカラムであり,getItems()でアイテムの集合を取得できる.ということは,getItems()[0]とすれば先頭のアイテムを取得できるはず.さらにそれに対してgetElement()メソッドを適用することで,HTML要素を取得できる.

その要素が持つIDをgetAttribute('id')で参照すれば,ファイル名の語幹部分を取得可能なので,文字列操作で新しい動画ファイルの参照先を作ることができる.あとは,先ほど用意したisSrcDiffence()を使って「すでに設定されているソースと異なっていれば入れ替える」という処理を加えればよいはずだ.

その処理を.on('dragReleaseEnd', …)に追記しよう.次の太字で表記した部分である.

    ...

  .on('dragReleaseEnd', function (item) {

    item.getElement().style.width = '';

    item.getElement().style.height = '';

    item.getGrid().refreshItems([item]);


    topElm = columnGrids[0].getItems()[0].getElement();

    newSrc = "./data/" + topElm.getAttribute("id") + ".mp4";

    mov = document.getElementById("moviePane");


    if (isSrcDifferent(mov.src, newSrc)) { mov.src = newSrc; }

  })

    ...


ソースコードの修正が終わったら,ドラッグ・アンド・ドロップで先頭を入れ替えたときに,メインの動画も差し替えられるか,確認してみよう.

ここまでのコードはstep_04.zipに置いてあるので,迷ったらそれを参照すること.

ステップ5

動画を指定した順序で再生することを考える.それには,ビデオ再生ボタンが必要だろう.ビデオの再生をどうやって制御するかも考えなければならない.さらには,指定した並び順をどうやって取得して,連続再生するか,その仕組みも考える必要があろう.

まず,HTMLを修正して再生ボタンを追加する.Bootstrapのボタンクラス(btn)を使おう.動画表示の要素の下に,次の一行(太字部分)を追加する.

    ...

  <div class="board-column result">

    <div class="board-column-container">

      <div class="board-column-header">Result</div>

      <div class="board-column-content-wrapper">

        <div class="board-column-content">

          <div>

            <video id="moviePane" src="./data/src01.mp4"

                   width="100%" controls="true">

          </div>

          <a class="btn btn-primary" onClick="playMovies();">Play</a>

        </div>

      </div>

    </div>

  </div>

    ...

onClick属性で,クリックイベントに対してplayMovies()メソッドを結びつけてやる.クリックされたらそのメソッドが呼ばれるという仕掛けである.

JavaScriptにはそのメソッドを記述する.script.js の末尾に次(太字部分)を追加する.

    ...

// Init board grid so we can drag those columns around.

boardGrid = new Muuri('.board', {

  dragEnabled: true,

  dragHandle: '.board-column-header'

});


playMovies = function() {

  console.log('The play button was pushd!');

}

見てのとおり,コンソールにメッセージを出力するだけのスタブである.まずは,これで動作確認してみよう.

このように,Playボタンを押すごとに,コンソールにメッセージが出力される,すなわちコールバック関数としてこのメソッドplayMovies()が呼び出され,実行されていることが確認できただろう.

ここまでのソースコード,スナップショットはstep05.zipにあるのでご参考まで.

ステップ6

さあ,ゴールは見えてきた.次は,Playボタンを押したらムービーの再生が始まるようにする手順である.

JavaScriptの当該部分を修正する.追記したplayMovies()を修正すればよい.video要素を取得する方法はすでに学んでいる.取得した要素に対してplay()メソッドを呼ぶだけ,ビデオの再生が始まるはずである.以下のとおりコードを修正して,動作確認してほしい.

    ...

// Init board grid so we can drag those columns around.

boardGrid = new Muuri('.board'{

  dragEnabled: true,

  dragHandle: '.board-column-header'

});


playMovies = function() {

  mov = document.getElementById("moviePane");

  mov.play();

}

いちおう,この段階のコードをstep06.zipとして置いておいた.為念.

ステップ7

もうほぼ完成だが,「指定した順序でビデオを再生したい」という機能がまだできていない.並び順を取得して連続再生を可能にするにはどうすべきだろうか.

まず,JavaScriptソースコードの冒頭あたりで,大域変数movieFilesを導入しておく.再生すべき動画のソース文字列を格納した配列として扱われる変数である.この宣言は無くても動作するようだが,きちんと書いておいたほうが分かりやすいだろう.

var dragContainer = document.querySelector('.drag-container');

var itemContainers = [].slice.call(document.querySelectorAll('.board-column-content'));

var columnGrids = [];

var boardGrid;

var movieFiles;


// Init the column grids so we can drag those items around.

    ...

そして,後ろのほうに書いておいたplayMovies()メソッドを修正する.実際の再生指示は,新たに用意するplayNext()メソッドで実現する.配列movieFilesが空だったら(length == 0だったら)何もせず戻るとか,配列movieFilesの先頭からひとつ取り出して(movieFiles.shift)セットするといった工夫を加えているが,さほど難しいコードではないから理解は容易だろう.

    ...

// Init board grid so we can drag those columns around.

boardGrid = new Muuri('.board'{

  dragEnabled: true,

  dragHandle: '.board-column-header'

});


playNext = function() {

  if (movieFiles.length == 0) return;

  mov = document.getElementById("moviePane");

  mov.src = movieFiles.shift();

  mov.play();

}


playMovies = function() {

  movieFiles = columnGrids[0].getItems()

    .map(function(item) {

      return "./data/" + item.getElement().getAttribute("id") + ".mp4";

     });

  document.getElementById("moviePane")

          .addEventListener('ended', playNext);

  playNext();

}

playMovies()メソッドは少し丁寧な解説が必要かもしれない.

columnGrids[0]には,サムネイルのアイテムが入っている.そしてgetItems()でその配列が取得できる.配列を対象に操作するmap()メソッドは,関数を引数にとり,それぞれの要素についてその関数を適用した結果を配列にして返す.item.getElement().getAttrribute(“id”)で “src01”などの文字列が得られるので,最終的に変数movieFilesには
( “./data/src01.mp4”, “./data/src02.mp4”… ) という文字列の配列が得られることになる.ここまでが,最初の処理の解説である.

次は,動画要素に「イベントリスナー」を追加する処理である.addEventListener()メソッドは,その処理を行う.

ところでイベントリスナーとは何だろうか?これについてはあとで補足するが,‘ended’イベントが動画が終了したときに発生し,それに応じてplayNext()メソッドが呼ばれるように追加するという設定をここでは行なっている.

最後に,playNext()を明示的に呼び出して最初の動画を再生し,playMovies()は終了する.イベントリスナーを追加することによって,終わったら次,終わったら次,というように,配列で指定するリソースを順次設定して再生する,という手順が少し分かりにくくなっているかもしれない.このような非同期プログラミング(プログラムの進行と実際の処理が同期しないようなプログラミングのこと)は慣れるまでは難しいかもしれないが,ぜひ,慣れておくようにしたいところである.

イベント駆動型プログラミング

非同期プログラミングの1つとしてイベント駆動(event driven)プログラミングにも慣れておきたい.実はすでに使っていて,ドラッグ・アンド・ドロップ終了時の処理の記述とか,Playボタンが押されたときのメソッド呼び出しとか,これらは全て「ドラッグ・アンド・ドロップが押されたよ!」というときに発生するイベントや,「Playボタンが押されたよ!」というときに発生するイベントを受けてメソッドや関数を実行するという形態になっていた.

このようなプログラミングパラダイムのことを,イベント駆動型プログラミングという(図).

ここまでのソースコードはstep07.zipとしてまとめておいた.本稿の指示通りに修正していれば,あらためてダウンロードする必要はないはずだ.Playボタンを押して,Orderカラムに並べたサムネイルの順番にビデオが再生されることを確認しておこう.

まとめ

本稿では,簡単な映像編集アプリを題材にして,シングルページアプリ(SPA)を作りながら,その簡単な作り方について解説した.既存の素晴らしいライブラリを流用しているので,とても簡単に作れることを実感できたことだろう.

今回利用したMuuriにはソートを行う機能もあるので,Muuriを公開しているオリジナルのWebサイトを見るなり情報を調べて,ソートする機能を実装してみるのも一興である.タイトルをクリックすると並べ替えをしてくれるなどの実装例が考えられるだろう.