はじめに
みなさん、数学ガールは読まれていますか?最新巻1のネタは「ビットとバイナリー」、つまり2進数に関する話題です。
数学ガールの秘密ノート/ビットとバイナリー
第2章は「変幻ピクセル」と題して16x16の画像をビット演算により変換する(フィルタをかける)話になっています。前巻と異なりプログラムは提示されており、すでにRubyで実装してみた例も公開されています。
ここまで前置き。
前回はPython(matplotlib)で実装したわけですがその後Twitterで「JavaScriptで実装してみました!」という方をちらほら見かけ、「インタラクティブええなぁ」と思ったわけで、
React使って実装してみました。
コードはここに置いてあります。
https://github.com/junjis0203/math_girls-secret_notebook_bits_and_binary
フィルタ
書籍では「受信する」・「送信する」という表現でフィルタへの入力・出力が行われており、先に紹介したRuby実装でもFiberを使って実装されています。
JavaScriptでもasync使って頑張れば実装できそうですが、本題から外れるし最終形のフィルタを考慮すると状態管理がめんどくさいので普通に入力配列を渡し出力配列を返すようにしました。
const right = (input) => {
const output = [];
for (let i = 0; i < 16; i++) {
let x = input[i];
x = x >> 1;
output.push(x);
}
return output;
};
Figureコンポーネント
この実装のメインとなる画像の表示。このためにFigureコンポーネントを作成しました(src/pixel-component.js)。使い方はこんな感じ。
const world = <Figure lines={input1} clickable={true} />;
これで以下のように表示されます。ファイルとしてはpixel0.htmlです。
clickableがtrueだとクリックに反応して白黒が反転するようになっているのですが、Edgeは問題ないもののChromeは一番下しか反応しません。line-height
を0にしているのが原因のようですが直し方がわからないのでそのままにしています。
コンポーネントの描画はrender()→lines()→pixels(y, line)という階層構造になっています。元々はFigure > PixelLine > Pixelという階層構造になっていたのですがイベントハンドリング考えると親にさかのぼるのが煩雑なのでFigureの中で全部処理するようにしました。
Store
この実装のメインその2、インタラクティブ性。入力画像をポチポチしたら出力が変わるようにしたいわけです。
Reactの仕組みも流儀もよくわかっていなかったので苦労しましたが最終的に以下のような感じにしました。
const in1 = <Figure lines={input} clickable={true} store={store} dst_tag={'INPUT1'} />;
store.addFilter('INPUT1', right, 'OUTPUT1');
const out1 = <Figure store={store} src_tag={'OUTPUT1'} />;
- in1のFigureはクリックされるとstoreに
'INPUT1'
というタグで画像全体を投げる(Figureクラスのdispatchメソッド) - rightのフィルタは
'INPUT1'
が投げられると動作し結果を'OUTPUT1'
として投げる(Storeクラスのdispatchメソッド) - out1のFigureは
'OUTPUT1'
を受け取り表示する(FigureクラスのonDispatchメソッド)
pixel1.htmlが上記のコードに対応します(実際には次に説明するComposerを使っています)
ちなみに、このaddFilterの実装(フィルタはlistenerの一部である)はなかなか美しいと自画自賛しているのですがいかがでしょうか。
addFilter(src_tag, filter, dst_tag) {
// listen tag and execute filter
const listener = {
onDispatch: (input) => {
const output = filter(input);
this.dispatch(dst_tag, output);
}
};
this.addListener(src_tag, listener);
}
Composer
さてというわけでイベントに反応して出力が変わるようになりましたがもう一度見てみましょう。
const in1 = <Figure lines={input} clickable={true} store={store} dst_tag={'INPUT1'} />;
store.addFilter('INPUT1', right, 'OUTPUT1');
const out1 = <Figure store={store} src_tag={'OUTPUT1'} />;
ダサいですね。全然DRYじゃありません。というわけで、Composerというクラスを作り以下のように書けるようにしました。
const composer = new Composer();
const in1 = composer.makeSource(input1);
const r_block1 = composer.addFilter(in1, right);
const world = r_block1.component;
仕組みは以下のようになっています。
- tagは連番で勝手に割り振る。
- makeSourceやaddFilterは
{component: Reactコンポーネント, tag: 割り振られたタグ}
を返す。他への入力に利用できる。
最終形:縁取りフィルタ
さてちょっと伏線してた最終形。入力画像の縁取りをするというフィルタについてテトラちゃんユーリが考え無理だった後、リサにより答えが示されます。
右上にポツンとあるFは実際には最終段のAND入力の片割れでCSSを個別にいじればどうにかはできるのですが、まあ逆に目立つしいいかなと思ってます。
これを作るためのコードは以下になります。
const out1_1 = composer.addFilter(in1, right);
const out1_2 = composer.addFilter(in1, left);
const out1_3 = composer.addFilter(in1, up);
const out1_4 = composer.addFilter(in1, down);
const out2_1 = composer.addFilter2(out1_1, out1_2, and);
const out2_2 = composer.addFilter2(out1_3, out1_4, and);
const out3 = composer.addFilter2(out2_1, out2_2, and);
const out4 = composer.addFilter(out3, complement);
const out5 = composer.addFilter2(in1, out4, and);
実際に動いている様子はこちら。左端の4つのFも入力なので同期させないといけないのですが分岐をちゃんと実装しようとするとコードが増えてしまうので今回は残バグということにしてます。
おわりに
今回は秘密ノート/ビットとバイナリーで紹介されているフィルタプログラムをReactで実装してみました。ReactNativeは使ったことあるので2Figureコンポーネントはすぐに作れたのですが、入力→フィルタ実行&伝搬の仕組みは思いつくまでしばらくかかりました。暑くてやる気がしなかったというのもありますが(笑)
ところで
<script src="pixel-filter.js"></script>
<script src="pixel-component.js"></script>
<script src="pixel-composer.js"></script>
<script src="pixel-main_common.js"></script>
何時代のJSだよって感じですね。importおよびWebpack使うのをちゃんとやろうと思いましたがめんどくさいので手抜きしました。
宣伝?
さて初めに書いたように数学ガールの秘密ノート最新巻は「ビットとバイナリー」ですが、なんと!すでに次の巻が決まっています。
数学ガールの秘密ノート/学ぶための対話
「数学が苦手な子にどのように教えるべきか」「わからないものをどう学んだらよいか」がテーマという連載当時から絶大な反響を呼んだシリーズが早くも書籍化!数学だけでなく「教えるということ」「学ぶということ」に関心のある方、ない方も必読です!
ちなみに、私にアフィリエイトは一銭も落ちませんので純粋に宣伝です。