Webフロントエンジニアが一般参加型コンテンツをつくった話

この記事はカヤックアドベントカレンダー2016の18日目の記事です。

こんばんわ!
今年の春より社会人になりました、HTMLファイ部のゆみこふ( @yumikokh )です。
普段の業務ではクライアントワークチームでWebを作っております。

が!

あるリアルイベントで、一般参加型コンテンツの制作を担当させていただきました。

今回は、 @ki_230 先輩の記事

ウェブページをできる限りネイティブアプリっぽく魅せるテクニックまとめ 〜アップルにリジェクトされつづけるなら、ウェブアプリとストアをつくって自前で配信してしまおうという企み〜 - KAYAC engineers’ blog

に便乗しまして、そのときの知見について書こうと思います。

もくじ

  1. 一般参加型コンテンツ?
  2. システム構成
  3. ブラウザ感を隠す小技集
  4. 音のだしわけ
  5. おまけ
  6. さいごに

一般参加型コンテンツ?

『一般参加型コンテンツ』と言われてもピンと来ないと思います。

ここでは一般の来業者の方に実際に参加して楽しんでいただく、アーケードゲームのようなものを指しています。 (なにかイケてる呼び方があればこっそり教えてください)

システム構成

制作したのは、画面上のマーカーをタッチしてポイントをゲット、
みんなでスコアを競うというシンプルなゲームです。
(諸事業によりコンテンツの詳しい内容は割合します)

60インチのタッチディスプレイを使用したため、センシングに関して実装の必要がありませんでした。
コンテンツはopenFrameworksでもにょもにょ…ではなく、
いつもどおりHTMLとCSSとJavascriptで書いて
Chromeブラウザをフルスクリーンにして見せているところがポイントです。

音の出力はモニタ側(HDMI接続)と別スピーカー(オーディオ端子接続)の2箇所です。
得点したときのピコーンという効果音はモニタ側からステレオ出力、
拍手のようなオーディエンスの音を別スピーカーからモノラル出力します。

ブラウザ感を隠す小技集

ひとつひとつはとても単純ですが
「なーんだただのChromeじゃんっ」と思われないためには大切なことです。
(当然ながら@ki_230先輩の記事と重複するところがあります)

スワイプを防ぎましょう

document.body.addEventListener('touchmove', (ev) => {
    ev.preventDefault();
});

タッチでテキストや画像の見た目を操作できないようにしましょう

* {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // タップ時のハイライトを透明に
  -webkit-touch-callout: none; // 長押し時のメニューを消す
  -webkit-user-select: none; // テキストなどの選択を防ぐ
}

img {
    pointer-events: none; // 画像ドラッグを防ぐため ※重要
}

右クリックメニューを防ぎましょう

document.oncontextmenu = function() {
    return false;
}

カーソルを消しましょう

trans_cursor.cur という透明画像を用意してカーソルとして割り当てます。 f:id:ymym0308:20161218234752p:plain← 👀
デバッグ時にはカーソル欲しくなるのでキーボードでクラスを切り替えられるようにしておきます。

.trans-cursor {
    cursor: url(../images/trans_cursor.cur), auto;
}

音のだしわけ

Audio Midi 設定

まずは既存ソフトを使ってちょっと設定を行います。
一般的に音チャンネルを出し分けするのに使われるソフトにSoundflowerが挙げられますが
今回はAudio Midi 設定というMac標準のアプリだけで設定が完結しました。
(オーディオインターフェースも必要なし!)
左下の「+」よりオリジナルセットを作成し、内蔵出力->HDMIの順にチェックをすると
4つの出力チェンネルが作成され、1,2はHDMI、3,4は内蔵出力になっています。

f:id:ymym0308:20161219005111p:plain

(なぜか内蔵出力の色が濃いグレーのままなので適応されてるか不安でしたが大丈夫でした)

実装

Web Audio APIという音声処理用の高機能なインターフェースを使います。
WebAudioAPIについての詳しい説明は こちら

モニタ側はステレオ出力、別スピーカーではモノラル出力の想定です。
音源読み込み時にsubSpearkerFlgをtrueにしたものを別スピーカーで出力するようにします。

↓イメージ図

f:id:ymym0308:20161219003812p:plain

// 初期設定
const ac = new AudioContext; // Audio Contextの作成
const splitter = ac.createChannelSplitter(2); // 異なるチャンネルのソースををモノラルとして扱えるようにする
const merger = ac.createChannelMerger(4); // 異なるモノラル入力をまとめる
if( ac.destination.maxChannelCount != 4 ) throw new Error('Audio Midi設定が適応されていません');
ac.destination.channelCount = 4;

// ここから新しいソースノードを作成する時の処理
// subSpeakerFlgがtrueのとき別スピーカーで出力するように指定
createNewSource(buffer) {
    const source = ac.createBufferSource();
    source.buffer = buffer; // XHRで音源をロードしておく
    source.connect(splitter); // ステレオチャンネルをモノラルで扱えるようにする

    if (!subSpeakerFlg) {
        // モニタ出力の場合
        splitter.connect(merger, 0, 0); // 左チャンネルからの入力をモニタ側の左チャンネルで出力
        splitter.connect(merger, 1, 1); // 右チャンネルからの入力をモニタ側の右チャンネルで出力

    } else {
        // スピーカー出力の場合
        // 左・右チャンネルからの入力を別スピーカー(モノラル)で出力するように指定
        // AudioMidi設定で設定した3チャンネル目で出力するようにする
        splitter.connect(merger, 0, 2);
        splitter.connect(merger, 1, 2);
    }

    merger.connect(ac.destination);
}

おまけ

今回のイベントでは他にもスコアを出すコンテンツがあり、その合計点を競うというものでした。
スコアは1つのサーバーで管理するので、
Node.jsで書かれた送受信用の別アプリを同時に立ち上げておき、OSC通信でやりとりをしています。
別アプリについてはここでは触れないですが、コンテンツ側では以下の単純なコードで送信を行うことができました。(雑な説明

const socket = io.connect(); // セッティング
socket.emit(“point”, finalScore); // 送信

また、当初はWeb技術だけで本物のネイティブアプリを作ることができるElectronを採用する予定でしたが
今回はChromeだけで間に合いました 🍣

さいごに

Webブラウザで実現できることは多岐にわたり、
Webエンジニアであっても新しい言語を覚えることなく、
インターネットの外に飛び出すことができるようになりました!

私の普段の業務では、インターネットを通してユーザーの方に制作したサイトに訪問していただくので
はてぶやツイッターなどからしかユーザーの方の声は聞こえません。
今回のように現場展示のコンテンツの場合、
自分が作ったコンテンツでユーザーの方が楽しんでいただいている様子を直接目にすることができて
通常業務とはまた違った喜びがありますね :)

先輩方のサポートもとても助かりました!ありがとうございました!

このように、カヤックでは新卒1年目であっても
所属の垣根を超えたお仕事に関わることができます。
チャレンジ精神旺盛な仲間を絶賛募集中です!

明日は @konboi さんです。おたのしみに!