hitode909の日記

以前はプログラミング日記でしたが、今は子育て日記です

ScrapboxでVJするためのChrome拡張を作った

1月のKyoto.JSの休憩時間にMIDIコントローラとWeb MIDI APIでVJできるとよさそうって話になったので買ってみて、ちょっとずつ触ってみた。過去に作ったツールをMIDI対応する、とか、MIDIの入力に応じて絵を出す、という使いみちに加えて、既存のページにコードを差し込んでエフェクトをかけるのを作ってみた。

f:id:hitode909:20200208155801j:plain

Scrapboxにエフェクトをかける

Scrapboxのプロジェクトのトップページには四角いカードが並んでいるので、これをパーティクルとみなして毎フレーム見た目を書き換えると愉快なビジュアルを出せると考えた。
MIDIコントローラで操作するために、動きのパターンを座標、大きさ、色、背景、ページ全体の変形、などチャンネルがいくつかに整理して、縦フェーダーでエフェクトの種類がかわり、各チャンネルに対応するつまみでエフェクトの掛かり具合が変わるようにした。

こういう構造があって

const renderers = {
    positions: [],
    sizes: [],
    colors: [],
    bodyTransforms: [],
    bgs: [],
};

positionを書き換える関数、をセットしていく。positionsはstyle.left, topあたりをセットする関数たち、sizesはtransform: scale()を指定する関数たち、みたいな調子。ジャンプする関数なら周波数に応じてtopをマイナスにする、とか、円を書きたいときはsin,cosで回す、という調子。ジャンプ度合いや回転速度はMIDIコントローラの入力で決める。
midi.get(8,0,10)で8チャンネル目の入力(0〜127)を0.0〜10.0に変換してから返す、みたいなメソッドをつくったところスッキリ書けてよかった。

// jump up
renderers.positions.push((allElements, frameCount) => {
    const volumes = volume.getVolumes(allElements.length, 1.0);
    const gain = midi.get(8, 0, 10);
    let i = 0;
    for (const e of allElements) {
        try {
            e.style.position = '';
            e.style.left = '';
            e.style.top = `-${volumes[i] * gain}px`;
        } catch (ignore) { console.log(ignore) }
        i++;
    }
});

// circle
renderers.positions.push((allElements) => {
    const SPEED = midi.get(8, 0.004, -0.004);
    const RADIUS = 35;
    let i = 0;
    let r = 0;
    const t = new Date().getTime();
    for (const e of allElements) {
        try {
            r += 3.14 * 2 / allElements.length;
            e.style.position = 'absolute';
            e.style.left = `${Math.sin(t * SPEED + r) * RADIUS + 45}%`;
            e.style.top = `${Math.cos(t * SPEED + r) * RADIUS + 45}%`;
        } catch (ignore) { console.log(ignore) }
        i++;
    }
});

で、MIDIのフェーダーに応じて関数を呼んでいく処理をrequestAnimationFrameで呼び出す。

    let channel = 0;
    for (const key in renderers) {
        const fn = renderers[key];
        fn[midi.getInt(channel, 0, fn.length - 1)](elements, frameCount);
        channel++;
    }

こういう構造は素朴な割にちゃんと動いたのでよかった。今回の構造を使って、動きの関数を入れ替えればまったく別のコンテンツも作れる。


動かす対象の要素はscrapboxのDOM構造に依存していて、page-list-itemというクラスがついたものかpageクラスのついたものを集めて、それらのうち幅が1ピクセル以上あるものを選んでいる。頻繁に変わるものでもないので1秒おきに実行。
ここのセレクタをdivとかspanとかimgとか、それくらいの抽象度のものに絞ることができれば、Scrapboxに限らずどんなページでも愉快なビジュアルに変換できるはずだけど、ページにあたってるスタイルとの兼ね合いがあるので、そんなにうまくいかないと思われる。各要素の位置関係を見てそれっぽい要素を探索するような実装をすると良いはずなので誰か研究してほしい。

reloadElements = () => {
    const allElements = Array.from(document.querySelectorAll('.page-list-item,.page')).filter(e => e.getBoundingClientRect().width > 0);
    const visibleElements = allElements.filter(e => e.getBoundingClientRect().width > 0);
    elements = visibleElements.length > 0 ? visibleElements : allElements;
}

こういうことは今週すべてイチからやったわけじゃなくて昔からやっていてページをチカチカさせるGreasemonkeyを作ったりしていた。学生の頃には研究室で酒を飲みつつページを破壊するGreasemonkeyを作るという趣味があった。
blog.sushi.money

RGBじゃなくてHSLで色相を変えるといい感じにチカチカして良いですぞ、みたいな知見もブログにまとめている。
blog.sushi.money

Chrome拡張

今回はid:utgwkkのVJとして準備していた。事前に、当日流すかもしれないのでとYouTubeの好きな動画リストをもらったので一通り聞いたところ、キラキラして声の入った四つ打ちの曲が多かったのでこういう感じだろうとビジュアルの用意をしていた。これで青春日本語ロックみたいのが延々と流れると映像とマッチしないので危なかったけどうまくハマった。
歌川くんのScrapboxを動かすことは決めても、その実現方法はいろいろと考えられる。事前に歌川くんにコードを納品してScrapboxから読み込んでもらうのはインターネットを使ったVJコンテンツとしての熱さが足りない。当日まで本人にはどういう用意をしているか説明せずに準備をして、当日になって勝手にページが動き出すとおもしろいにちがいない。と考えて、外からコードを差し込む手段としてGoogle Chrome拡張として実装することにした。
Chrome拡張は以前はよく遊びで作っていたのでひさしぶりにチュートリアルをやってみたところ当時ととくに変わりなかった。manifest.jsonで必要な権限とか起動する対象のページを決めて、contentScriptを使うとページのコンテキストでコードを実行できたり。UserScriptを使えればそれでもよかったけどもう滅びている。

コードはここに置いてます。contentScript.jsに処理のすべてが書かれてて300行くらい。
世界中のみんなが毎日使うようなものでもないのでGoogleのストアには置いてない。chrome:///extensionsから「パッケージ化されていない拡張機能を読み込む」で使える。
github.com

様子

家で準備しているときに、ひょっとしてこれめっちゃいいのでは、という予感はしていた。
MIDIの信号を全部0にすると何も起きないようにしていたので、当日、Scrapboxをぽんと開いたときは、聴衆の反応は、え、ここからどうするの、みたいな反応だったけど、音に合わせてdivをジャンプさせると、みなジャンプするdivにあわせてジャンプしたりと大喜びだった。
円形に並べるとか、各ページの色を変えるとか、ページ自体ぐるぐる回転させるとか、動きのパターンをたくさん作っていって、組み合わせをリアルタイムに選べるようにしておいたので、曲に合わせて切り替えっていって良い映像を出せた。






今回、DJ3人分担当したので1.5時間分のコンテンツを用意していくことになって、なんで自分で締め切りを増やしてるんだろうとか考えだしてしまってそこそこ大変だったのだったのだけど、ありえないほどにめっちゃいいものができたので満足している。
これで心おきなくVJとして引退できる、ということはなく次回も何か用意していくと思う。何のアイデアもなくてもとりあえず締切があればなんか思いついて作っていく(あるいは社会的信用を失う)、というのをずっとやっていると蓄積がでてきて良いと思う。