ちょうど社内の勉強会で発表する機会があったのと、もともとこんなことやりたいなーと思っていたので調べてみた
GoogleSlideにはユーザーツールと言ってslido
のような見ているユーザーがスライドに対して質問を投稿できる機能がある(発表者がプレゼンターからユーザーツールを起動することで専用のURLが発行される)
このユーザーツールは発表者側、質問者側ともに質問が投稿されたら一覧に出てくるようになっていてインタラクティブに聞いている側とコミュニケーションを取りやすいようになっている
しかし、スライドに直接反映させる(表示させる)には発表者が「表示」ボタンをクリックする必要がある
- 実際のユーザーツールの使用イメージ
「ここでいくつか質問きましたね、1つずつ見てみましょうか」みたいな感じのやりとりが必要な感じを想像させる
そこで、スライドを見ているユーザーのコメントを直接スライド上に流せないかなーと思っていた
サービスで言うとcommetsというサービスがそれかなと思う
ちゃんと調べたわけではないので間違ってたら申し訳ないがおそらくコメントデータなどはサービス側で持つようにして投稿されたデータをブックマークレットで呼び出してスライド上に直接流す感じだと思う
GoogleSlideの場合せっかくユーザーツールがあり質問のデータなどもすでに持っているのでこれを使ってよしなにできないかなと考えた
結論から言うと次の3つを組み合わせて実現できた
- MutationObserver
- BroadcastChannel
- RequestAnimationFrame
手順
- プレゼンターの質問リスト
- MutationObserverで質問リストのDOMを監視、変更があればBroadcastChannelで表示側へ通知
- スライド表示側
- BroadcastChannelでイベントを受け取りスライド上に表示するコールバックを定義
行けそう! ということで1つずつ分解して試してみる
MutationObserverでDOMの変更を検知
下記を参考にした
MutationObserver.observe() - Web API | MDN
観測対象の要素を指定して対象要素以下でDOMの変更があった場合に指定したコールバックを呼ぶ
今回の場合だとプレゼンターのウィンドウでDevToolを開いて中のコンテンツ(質問のリスト)が追加されたらコールバックを呼ぶようにする
observeElement = document.querySelector('.punch-viewer-speaker-questions'); observer = new MutationObserver(function(records) { console.log(records);console.log('callback that runs when observer is triggered'); }); observer.observe(observeElement, {subtree: true, childList: true});
これで対象の要素以下のDOMの変更を検知してコールバックを実行できる
例だとconsole.log
で実際に変更のあったレコードを出力している
試しに適当なコメントを付けてみた
[0].addedNodes[0].children[1].children[2].innerText
とたどるとメッセージのプロパティを発見できた
Broadcast Channel APIでタブ間のデータやりとり
タブ間もしくはウィンドウ間でのメッセージングの方法の1つとしてBroadcast Channel APIというものがある
Broadcast Channel API - Web APIs | MDN
とりあえずBroadcast Channel APIをつかってみる
参考からコードを拝借してタブ間でのデータのやりとりを行ってみる
devtoolsを立ち上げたタブを2つ用意してConsoleにて下記コードを実行してみる
- 送信側
bc = new BroadcastChannel('comment_channel'); bc.postMessage('This is a test message.');
- 受信側
bc = new BroadcastChannel('comment_channel'); bc.onmessage = function (ev) { console.log(ev); }
- 結果
無事タブ間でのメッセージのやりとりを行えることが確認できた
RequestAnimationFrameで文字列を流す
適当なサンプルを書いてローカルでサーバ立てて動作を確認した
- index.html
<html> <head> </head> <body> <input type="text" id="comment"> <button id="bt">Add Comments !!!</button> <div class="box"> <div> <p class="comment">Contents</p> </div> </div> </body> <script src="./app.js"></script> </html>
- app.js
const addComment = (comment) => { const boxElement = document.querySelector(".box"); const element = document.createElement("p"); element.innerText = comment; boxElement.appendChild(element); const moveUnit = 5; const windowWidth = document.body.clientWidth; element.style = `translateX(${windowWidth}px)`; let moveX = windowWidth; const elementWidth = element.clientWidth; const moveAnimation = () => { if (moveX >= -elementWidth) { moveX = moveX - moveUnit; element.style.transform = `translateX(${moveX}px)`; requestAnimationFrame(moveAnimation); } }; moveAnimation(); } const handleClick = (event) => { const commentInput = document.querySelector("#comment"); const comment = commentInput.value; console.log(comment); addComment(comment); }; const button = document.querySelector("#bt"); button.addEventListener("click", handleClick, false);
文字が動いた
組み合わせてブックマークレットを作成する
以上で色々動かしてみた結果を踏まえてユーザーツールのコメントをリアルタイムに流せるようなブックマークレットを作成した
- 読み取り側
const broadcastChannel = new BroadcastChannel('comment_channel'); const extractComment = (mutationRecords) => { const nodes = mutationRecords.filter(record => record.target.className === 'punch-viewer-speaker-questions').map(record => record.addedNodes[0]); console.log(nodes); const comments = Array.from(nodes).map(node => node.children[1].children[2].innerText); console.log(comments); return comments; } const observeElement = document.querySelector('.punch-viewer-speaker-questions'); const observer = new MutationObserver(function(records) { console.log(records);console.log('callback that runs when observer is triggered'); broadcastChannel.postMessage(extractComment(records)); }); observer.observe(observeElement, {subtree: true, childList: true});
- 表示側
const broadcastChannel = new BroadcastChannel('comment_channel'); const addComment = (comment) => { console.log(comment); const element = document.createElement("p"); element.innerText = comment; const boxElement = document.querySelector('.punch-present-iframe').contentWindow.document.querySelector('.punch-viewer-content'); boxElement.appendChild(element); const elementTop = (boxElement.clientHeight - element.clientHeight) * Math.random(); const moveUnit = 5; const windowWidth = document.body.clientWidth; element.style.transform = `translateX(${windowWidth}px)`; element.style['font-size'] = '4em'; element.style.position = 'absolute'; element.style.top = `${elementTop}px`; let moveX = windowWidth; const elementWidth = element.clientWidth; const moveAnimation = () => { if (moveX >= -elementWidth) { moveX = moveX - moveUnit; element.style.transform = `translateX(${moveX}px)`; requestAnimationFrame(moveAnimation); } }; moveAnimation(); } const handleEvent = (event) => { event.data.forEach(comment => addComment(comment)); } broadcastChannel.onmessage = handleEvent;
スライド自体がiframe内に展開されるようでdocument.querySelector
の書き方がいつもと若干異なる書き方になった
だいぶ雑さが出てしまった気がするがとりあえず目的とする動作をさせることはできた
やってみた結果
まとめ
- GoogleSlideのユーザーツールとブックマークレットを利用して質問コメントをスライド発表中に直で流せるようにした
- プレゼンター側、スライド側でそれぞれブックマークレットの実行が必要
- すべてGoogle内のツールのデータで完結できるようにしたため「これつかって良いかな?」みたいな心配が不要
- それぞれ次のような役割をになっている
- MutationObserver
- DOMの変更検知
- BroadcastChannel
- タブ間、Window間でのメッセージング
- RequestAnimationFrame
- 文字列のアニメーション
- MutationObserver
感想
前からやりたいなーこれで少しでも盛り上がったりしないかなーと思っていたが思ったよりさくっとできた
一度これを用いて社内の勉強会で発表してみたが割と好評だった
ただ、自分の処理能力が足りず発表に集中するあまり発表中盤まで流れてくるコメントが目に入ってこないという失態を犯してしまった
いきなり悪意あるコメントが出てくるような場だと厳しいが、近しい仲間内だったり和やかな雰囲気の場合は選択肢の1つではないかと思う
どうしてもその場で遮ってまで声出しづらいなーと思っている人は少なからずいると思うので…
一応コードも置いてあるのでよければ試してみていただければと思います
(何度か動作確認してたら流れるコメントがスライドの範囲外になってしまうパターンが何度かあった…
参考
Canvasだけじゃない!requestAnimationFrameを使ったアニメーション表現 - ICS MEDIA
Window.requestAnimationFrame() - Web APIs | MDN
Broadcast Channel API - Web APIs | MDN