Reactで画像やテキストにWebGLエフェクトをかけるライブラリ作った

f:id:amagitakayosi:20200120205554g:plain

amagiです。先日React用コンポーネントライブラリのREACT-VFXをリリースしました。 REACT-VFXを使うと、画像や動画、テキストにWebGLでエフェクトをかけることが出来ます。

例えば、画像をグリッチしたり:

f:id:amagitakayosi:20200122230646g:plain
VFXImgのサンプル

GIFアニメをエモい感じにしたり……

f:id:amagitakayosi:20200122230824g:plainf:id:amagitakayosi:20200122230837g:plain
VFXImgでGIFアニメを表示したサンプル

プレーンテキストにもエフェクトをかけたり出来ます!!

f:id:amagitakayosi:20200122230935g:plain
VFXSpanのサンプル

この記事では、REACT-VFXを作った経緯や、動作の仕組みを解説します🦄

REACT-VFXを作った経緯

今後の仕事のために自分のポートフォリオを作り直そうと思い、インスピレーションを得るためにAWWWARDSを見ていた所、こんな感じでWebGLエフェクトが多様されてるのを発見した。

f:id:amagitakayosi:20200122233856g:plain
https://www.sachatourtoulou.com/work

f:id:amagitakayosi:20200122234122g:plain
https://collection-wakawaka.world/objects

かっこ良すぎる…………!僕もこういうWebGLエフェクトを使いたい!

ポートフォリオはReactベースで作ろうとしてたんだけど、ReactでWebGLを扱うのは結構難しい。 react-three-fiberはReact向けWebGLライブラリの中で最も筋が良いライブラリだ(個人の感想)。 react-three-fiberを使うと、JSXでThree.jsのシーンやオブジェクト、イベントなどを記述できる。 WebGLなサイトをちょちょっと作るのにすごく便利なんだけど、今回やりたい事に対してはオーバーキルだし、canvasの制御やシーン生成を手でやりたいって時には向いていない。

僕がやりたいのは、単にDOM要素をWebGLの空間に配置して、シェーダーでエフェクトをかけて描画する、って事だけだ。 という訳で、自分でライブラリを作る事にした。

使い方

インストールは npm i で行います。

npm i -S react-vfx

REACT-VFXVFXProviderVFXElements で構成されています。

  • VFXProvider: canvasWebGLコンテキスト、ページ全体の状態を管理
  • VFXElements: DOM要素をラップして、エフェクトのためのシェーダーを管理

まず、Reactアプリケーション 全体を <VFXProvider> で囲みます。

VFXProviderの使い方

次に、VFXElementsを配置していきます。 <img><video> の代わりに<VFXImg><VFXVideo> で置き換えてください。

VFXImgの使い方

これで完了!

エフェクトの種類は shader プロパティで変更できます。 この例では、画像にグリッチ風エフェクトがかかっているはずです。

シェーダーは自分で書くことも出来ます。 shader プロパティにGLSLシェーダーを渡してください。

自作シェーダーを使うサンプル

このコードはこんな感じに描画されます:

自作シェーダーの出力結果

より詳しい機能については、Webサイトの Usage セクションをご覧ください。

https://amagi.dev/react-vfx/#usage

仕組み

REACT-VFXはWebサイト全体を巨大な <canvas> 要素で覆うことで動作しています。 VFXElementsの位置やサイズをトラッキングし、WebGLのテクスチャとして読み込んで、毎フレーム描画しているだけで、特に変わった事はしていません。

f:id:amagitakayosi:20200122231746p:plain
VFXProviderとVFXElementの仕組み

とはいえ、GIFアニメ対応やテキストの画像変換周りでちょっと苦労したので、その辺を解説します。

GIFアニメ対応

WebGLは画像や動画をテクスチャとしてロードできますが、GIFアニメには対応していません。 (2年前に試した時は、chromeではGIFアニメをロードできてたんだけどな……)

そのため、今回GIFアニメに対応するんは、まずGIFアニメをパースして画像に分割し、順番にテクスチャにロードしていく、ということをする必要がありました。

GIFアニメのパースにはSuperGIFが良く使われるけど動作が重すぎるので、今回は matt-way/gifuct-js を使う事にした。 PromiseベースのAPIだしサクサク動いて便利なんだけど、bower時代の産物なのでforkする必要があった。

gifuct-jsのデモ

gifuct-jsによってフレームごとの画像データが手に入ったので、次はこれをWebGLでロード出来るようにする。 gifuct-jsはフレームごとの長さも返してくれるので、現在時刻と再生時間、フレームの長さを比較して順番に描画する事で、簡易的なGIFプレイヤーを実装できる。

f:id:amagitakayosi:20200123153453p:plain
gifuct-jsによるGIFプレイヤーの実装例

あとはこのcanvasWebGLに渡すだけで、GIFアニメをWebGL上に描画できた。

テキストを画像に変換する

WebGLでテキストを扱いたい場合、事前にテキストを画像に変換しておくか、Three.jsのドキュメントで解説されているようなトリックを使う必要があった。

https://threejs.org/docs/#manual/en/introduction/Creating-text

これらのトリックを使うのも大変だし、デザイン変更の度にPhotoshop等で画像を書き出すのも面倒……という事で、もっと手軽にDOM要素を画像に変換できないか調べた。

まず、html2canvasが使えるか試してみた。

html2canvas - Screenshots with JavaScript

html2canvasはWebページのスクリーンショットをブラウザ上で生成するライブラリだ。一応いい感じにDOM要素の画像を生成できたんだけど、html2canvasは毎回ページ全体をクローンするため、不要なリクエストが走りまくって動作がメチャクチャ重くなってしまい、今回の用途には合わないと判断した。

結局、html2canvasと似た動作をするdom2canvasというスクリプトを自作する事にした。

SVGforeignObjectという要素をつかうと、DOM要素をSVGに埋め込む事ができる。 これを利用すると、次の要領でDOM要素をcanvasに描画できる。

DOM要素 -> HTML文字列 -> SVG文字列 -> Canvas要素に描画 -> WebGL texture

いろいろ制限はあるけど(クロスオリジンなリソースを読み込めない等)、今回はプレーンテクストだけ変換できればいいので、これで充分だ。

こうして、渡された文字列を画像に変換してエフェクトをかけるコンポーネント VFXSpan ができた。 VFXSpanは文字列が更新されたら自動的に再描画するので、例えばtextareaと組み合わせたりもできる。

textareaに連動するVFXSpan

foreignObject周りはこの辺の記事を参考に実装しました🙏

今後の課題

今後は以下の機能を追加したいと考えています。

  • カスタムuniform変数のサポート
  • backbufferサポート
  • エフェクトプリセット追加

REACT-VFXはまだまだ粗もあるけど、ちょっとしたエフェクトをWebサイトに追加したい場合にはかなり便利に使えるはずです。 ぜひ試してみてください!

感想や機能リクエストはGitHubTwitterで気軽にどうぞ!

よろしく〜👻👻👻