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として引退できる、ということはなく次回も何か用意していくと思う。何のアイデアもなくてもとりあえず締切があればなんか思いついて作っていく(あるいは社会的信用を失う)、というのをずっとやっていると蓄積がでてきて良いと思う。

机はデスクトップ

デスクトップにファイルが散らかっているのを見ると哀れな気持ちになる。早く楽にしてやるよ…って言いながら全部ゴミ箱に入れたくなる。
机がデスクトップのメタファーだと考えると、机に物を置かないのが重要で、パソコンを置いて、読みかけの本があって、ランプがあって、ヤン・シュヴァンクマイエルの絵葉書を置くくらいにとどめている。
ペンはボールペン1本しか持ってないのでリュックに入れる。字を書くときはリュックからボールペンを取り出す。
これくらいの物しか置いてなければウエットティッシュで拭いて掃除完了できるので便利。

f:id:hitode909:20200207130747j:plain


こうなっていると掃除するのも大変だと思う。ディスプレイを使いたいときは会社で作業するようにしている。

雑でもいいから動くものを作っておく

最新版のVSCodeではworkbench.editor.limit.enabledの設定ができるようになった。同時に開けるタブの数に上限を指定でき、古い順にタブを自動的に閉じてくれる設定。これを見た瞬間にオッと思って、というのも自作して使っていた拡張の機能が公式の機能で置き換えられることを意味している。
code.visualstudio.com

Atomにあったzentabsという拡張を真似した自作して使っていたけどこれが不要になる。使ってる人はアンインストールしてください。
marketplace.visualstudio.com

issueをみてると、zentabsはあり、動いているけど、これはビルトインの機能があるほうがよいですねという話になっていた。
github.com

zentabsは強引な実装をして実現していて、指定したドキュメントを閉じるために、まずフォーカスを移してからcloseActiveEditorするという方法で閉じている。awaitしてる間に別のエディタにフォーカスが移るとまずいのでlockを取ったり、を手で書いている。今なら良いAPIがあるのかもしれないけど当時はドキュメントを指定して閉じるにはこうするしかなかった。
https://github.com/hitode909/vscode-zentabs/blob/master/src/ZenTabs.ts#L29-L30

await vscode.window.showTextDocument(itemToTrim.editor.document, itemToTrim.editor.viewColumn);
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');

雑でもいいから動くものを作っておくのは良い動きができたと思う。サクッと作った割に2314 installされているし、1.5年ほど常用できていたのでコスパ良かった。

夢、お香が炊かれているような香りがして、うろうろすると部屋の隅に大量の線香が立っていて納得する。
匂いの出てくる夢は珍しい気がする。

僕は東京に行っても舌打ちされたりしないので気のせいじゃないの、という話をしたところ、世の中には女性にだけタックルするおっさんとかもいるので、外見を見て判断されているのでは、という話だった。side_tanaさんは自転車から罵倒されるらしい。

テレビ買い替えてから困るのが、テレビの電源を消してもスタンバイ状態になっているためか、光デジタル出力の出力が出っぱなしになっている。本体だがの電源ボタンという概念もあり、それを消すと本当に消える代わりに番組録画ができなくなると思われる。以前はテレビの電源を消したらライン入力の音が出てたのが、いまは光デジタルのケーブルを抜かないと、ライン入力の音が出ない。テレビ用の棒じゃなくて、まともなアンプかなにかがあるとよいのかもしれない、しかしケーブルがごちゃごちゃになりそうなので物は増やしたくない。