こちらは JP_Stripes Advent Calendar 2022 の12月18日分のエントリーです。
はじめに
まずはぜひデモ動画からご覧ください。
キレイなキューブが60fpsでヌルヌル動きます。マウスでも操作が可能です。ソースコードは GitHub に公開しています。
Stripe Appsとは
Stripeダッシュボードの中で表示されるカスタムUIの拡張機能がStripeアプリです。Stripe APIとWebhookを使用することで、データを可視化したり、独自のサービスと連携したりすることが可能です。
アプリはReactで作られているので、TypeScriptでコードを書きます。
アプリの作り方についてもっと詳しく知りたい方はぜひ @hideokamoto さんの Stripe Apps を25日間紹介し続ける Advent Calendar 2022 を読んでみてください。
WebGLとの連携・仕組み
Stripeアプリはセキュリティのため、サンドボックスされた iframe
で表示されます。DOMには直接アクセスできず、WebGLのコンテキストでGPUからの出力は表示できません。
ただ、GPUの出力をバッファーにキャプチャーすることは可能(リアルタイムでテクスチャ生成など)なので、キャプチャーされた画像データをCPU側のメモリに戻し、以下のdataURLの様にエンコードすることで静止画像として表示することは可能です。
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASAAAAEgCAYAAAAUg66AAAAAAX...
そしてみんな大好きThree.jsがこのレンダリング出力からdataURLのエンコードまでの流れを既に対応しています。とてもありがたいことです。
Stripeアプリの Img
の画像表示するコンポーネントは src
属性にdataURLをそのまま渡せるので、Reactのstateでレンダリング出力のdataURL文字列を保存することで、基本のレンダリングループが簡単に出来上がります。
const ThreeJS = () => {
const [renderOutput, setRenderOutput] = useState('');
useEffect(() => {
renderer.setAnimationLoop((time) => {
// ...update scene here...
renderer.render(scene, camera);
setRenderOutput(renderer.domElement.toDataURL());
});
}, []);
return (
<Img width={width} height={height} src={renderOutput} />
);
}
マウスからの入力
DOMに直接アクセスがないなので、MouseEvent
にローカル座標の offsetX
/ offsetY
が無く、clientX
/ clientY
の絶対座標をコンポーネントのローカル座標にどう変換すればいいかという情報すら取得ができないらしい。
(※ React用の ref
を使ったり、 getBoundingClientRect()
などで結構色々と苦労したのですが、もし誰かが解決方法がご存じでしたら、ぜひ教えてください!)
一応仮の解決方法として、画像のサイズ ( width
と height
は指定してる)は分かるし、MouseEvent
を取得する際はマウスが Img
コンポーネントに確実に入ってる時のみになるので、マウス座標を取得する際はコンポーネントのエリアを自動的に調整し、ユーザーがマウスを動かすことで正確なバウンディングボックスが認識されるようになります。
const mx = evt.clientX;
const my = evt.clientY;
// update sliding bounds window based on input coordinates
if ((mx < bound.x1) || (my < bound.y1)) {
bound.x1 = Math.min(mx, bound.x1);
bound.y1 = Math.min(my, bound.y1);
bound.x2 = bound.x1 + width;
bound.y2 = bound.y1 + height;
} else if ((mx > bound.x2) || (my > bound.y2)) {
bound.x2 = Math.max(mx, bound.x2);
bound.y2 = Math.max(my, bound.y2);
bound.x1 = bound.x2 - width;
bound.y1 = bound.y2 - height;
}
あとはマウスの座標を正規化することで、X/Yの位置はコンポーネントの中で [0.0 .. 1.0] に変換されます。
// convert mouse coords to local normalized space
const mouseState = {
pos: {
x: (mx - bounds.x1) / width,
y: (my - bounds.y1) / height
},
buttons: evt.buttons
};
props.onMouseEvent(mouseState);
なお、絶対座標が正確ではなくても、今回のデモのように相対座標を利用すれば、正常の動きは取れます。
今後の進展
3Dデータの可視化はWebGLがかなり良いなので、Stripe Appsには結構使えそうですね。
現状は本当にシンプルなプロトタイプなもんですが、あとはキーボード入力とか、もっとクオリティが高いのシーンを表示したりしたいと思います!