G9.js: 自動的に相互作用するグラフィックス
(omrelli.ug)- g9.jsは、数値のキー・バリューデータとレンダー関数を結び付け、ユーザーがグラフィック要素をドラッグすると新しいデータ値を見つけてシーン全体を再描画するインタラクティブ・グラフィックスライブラリ
- 中核APIは
g9(initialData, render[, onChange])の1つだけで、initialDataと描画コンテキストをrender(data, ctx)に渡し、点、線、円、画像などを描画 - ドラッグのような操作が発生すると、g9は可能なデータ値空間を 最適化 し、意図した変化に最も近い値を見つけ、必要に応じて
onChangeで新しいデータを渡す - ギャラリーでは、基本的な点のミラーリングから、Bézier曲線、3点で定義される円、キューブ、ドラゴン曲線、画像、時計、LOGO Turtle、Grid Warp、Crank and Pistonまで、例を段階的に紹介
affects、ctx.pure、getData、setData、isManipulating、Math.max/Math.minといったパターンで、操作可能な変数範囲、再レンダリング、アニメーション中のユーザー入力処理を構成できる
g9.jsの動作フロー
- g9.jsは単一の関数
g9(initialData, render[, onChange])を公開している - まず数値を持つ キー・バリューデータ を作る
- 例では
foo: 10のようなinitialDataオブジェクトを使う - 関連ドキュメントは initialData in the docs
- 例では
- データと描画コンテキストが
render(data, ctx)に渡され、レンダー関数がデータ値に基づいて何を描くかを決める- 例では
ctx.point(data.foo, 17)で点を描く - 関連ドキュメントは render in the docs
- 例では
- ユーザーが要素を新しい位置へドラッグすると、g9は可能なデータ値空間から 最も近い変化 を生み出す値を探す
- 円を左にドラッグすると、例では
{foo: 8}のような新しいデータになることがある
- 円を左にドラッグすると、例では
- 新しいデータが決まると、整合性を保つためにシーン全体を再レンダリングする
onChangeコールバックを提供している場合、g9は新しいデータを渡し、ページの他の部分も更新できるようにする
- API全体は Docs で確認できる
基本図形とデータ制約
- Basic は2つの点だけを使う最小構成の例
- レンダー関数は、片方の点を
(x, y)に、もう片方の点を(y, x)に描く - 片方の点をドラッグすると、もう片方の点が互いにミラーリングされるように動く
- レンダー関数は、片方の点を
- Rings は
radiusとangleのデータを三角関数に入れ、点を2つの円状に配置する- 内側の点は赤色で描画される
- コメントアウトされた
ctx.line(...)を有効にすると、2つの円の点の間に線を追加できる
- The affects option は、g9の基本図形が受け取る
affectsオプションを示すaffectsは、その図形がデータのどの部分を変更できるかを指定する- 3本の線はそれぞれ異なる
affectsの値を使う - ある線は
l2_start_x,l2_start_yだけを変更し、別の線はl3_start_x,l3_end_yを変更する
- Using Max and Min to limit a point's range of motion は
Math.max(Math.min(...))で点の可動範囲を制限する- 中央の点の位置は
x1 + 20とx3 - 20の間に制限される - 別の点は
data.x2の値をそのまま表示する
- 中央の点の位置は
曲線、幾何、3Dの例
- Bézier Curve は、Wikipediaの quadratic Bézier curve アニメーションをインタラクティブ版にした例
start、middle、end座標とtの値をデータとして使うtween=ラベルを表示し、複数段階の線分で滑らかな曲線を構成する- 中間補助線と曲線の線分には
affects: ['t']が指定されている
- Three points define a circle は3点で円を定義する
- 3点の間の線分、中点、垂直方向の線を計算する
- 計算された中心点に点を打ち、1つ目の点までの距離を半径とする緑色の円を描く
- cube は立方体の頂点と辺を作り、3D回転・移動・投影を経て2Dの線として描く
rx,ry,rz関数で各軸回転を計算するproject関数は300 * p[0] / p[2],300 * p[1] / p[2]の方式で投影する- 辺には
affects: ['ax', 'ay']を使い、ドラッグで回転値を変更できる
フラクタル、手続き的描画、画像
- dragon は再帰関数
dragon(...)でドラゴン曲線を描く- データには
squareness,fromX,fromY,toX,toYが含まれる - 線のオプションには
affects: ['squareness']を使い、曲線の形を操作できる - 始点と終点は半径
5の点で表示される
- データには
- tree は再帰的に木を描く
- データは
deltaAngle,attenuation,startLength - 枝の終点と線には
affects: ['deltaAngle', 'attenuation']を使う n > 3なら線の色を茶色に、それ以外は緑色で描くonChangeコールバックは変更されたデータをコンソールに出力する
- データは
- LOGO Turtle simulator は
make_turtle(ctx)でシンプルな Turtle インターフェースを作り、線を繰り返し描く- データは
rとtheta forwardは現在の向きに進みながら線を描き、dturnは度単位の回転をラジアンに変換する
- データは
- Grid Warp は格子点を歪ませる変換を適用する
- データは
spinとscale - 各点の距離
rと角度thetaを計算し、その後theta + data.spin * rで回転量を変える -10から10までのi,jの組み合わせで点を描く
- データは
- image は
ctx.imageで画像を描く- 画像URL、
x、y、幅200、高さ200を指定する
- 画像URL、
機械装置と時間ベースの相互作用
- Kevin Clock は
timeデータを使って時計状の図形を描く- 分針は
data.time / 60、秒針はdata.timeを使ってMath.cos,Math.sinで終点を計算する setIntervalは50msごとにtimeを0.01ずつ増加させるgraphics.isManipulatingが真でないときだけgetData()とsetData(data)で時間を更新する
- 分針は
- Crank and Piston はクランクとピストンの構造を描く
- データは
timeの1つだけ - ハウジングの色は
Math.cos(data.time)から作ったHSL値に応じて変化する - クランク、シャフト、ピストンを線、四角形、円で描く
- アニメーションブロックはコメントアウトされており、有効にすると10msごとに
timeを0.05ずつ増加させるようになっている
- データは
- tongs は回転と反復線分でトング形状を構成する
- データは
a,b rotate(x, y, a)で座標を回転し、4回の反復で2列の線分を描くonChangeコールバックは変更データをコンソールに出力する
- データは
1件のコメント
Hacker News のコメント
再帰ツリーの 5〜6 階層目にレンダリングされたノードをつかんで動かすと、残りのツリーが反応するのが魔法のように見える
任意のリーフノードの位置からツリーの初期パラメータを明示的に推論する関数がなくても動作する
これがどう実装されているのか、点の位置がそこそこ滑らかな関数で結ばれていない場合にどれくらいうまく動くのか気になる
あるいは、既製の最適化器をそのまま放り込んでも可能だと思う
逆運動学の問題をモデル化して解く方法はたくさんある
その中でも最も柔軟な方法は、たいてい反復的な最適化で近似解を探す。順運動学の方程式を逆に解くのは難しく、解空間が空である可能性もあるから
多くの手法の核心は、順運動学の方程式をテイラー級数展開でモデル化することにあり、元のシステムより逆向きに解きやすい場合がある
最初はクールでクリーンな単純 API だと思ったし、同じことをするなら d3 より好みだった
でも「誰かがグラフィックと相互作用するとき、例えば要素を新しい位置へドラッグしようとすると、g9 は可能なデータ値の空間で最適化し、その変化に最も近い値を見つける」を読んだ後では、これはかなり魔法のような DOM 操作に感じる
チャートを操作してデータを変える必要があるアプリケーションは、たいていかなり特殊に見えるが、楽しさという用途は普遍的だ
そういうユースケースが来たらとても楽しそう
ページの他の例も実際に触ってみたのか気になる
ドラッグイベントの最初の数フレームで速度とベクトルを平均し、さらに多くのフレームで平均を更新し続けたうえで、予測された終点へすべてを滑らかな ease-in-out で動かし始めれば、「可能な値の空間で最適化」にかなり近くなる
以前 HN でこれが議論されたとき、興味深いコメントがあった: https://news.ycombinator.com/item?id=12572412
どう動くのか説明を見たい。自分には黒魔術のように感じる
例えば 2 つの点があって、1 つは (0, 10)、もう 1 つは (10, 0) にあるとして、後者の点を (20, 0) にドラッグしたら、システムは最初の点も同じ距離だけ反映されるべきだとどうやって見つけるのだろう?
繰り返すけど、これは魔法だし、作った人は本当にすごい
この関数をデータ変数について偏微分し、関数を最小化して新しいデータ値を見つけ、その新しい最適なデータ値ですべてを再レンダリングする
少し混乱する理由は、ここで普通「データ」と呼ばれているものが、数学的最適化ではパラメータ、統計/機械学習では重みと呼ばれるようなものだからだ
機械学習では固定されているものを「データ」と呼ぶだろうから、ここではドラッグされた要素の位置がデータに当たる
かなり格好よく見える
ひとつ惜しいのは、マーカーや選択表示がないので、説明を先に読まないとオブジェクトをドラッグできることがあまり明確ではない点
オブジェクト位置を再調整するやり方も少し混乱する
普通ならポインタハンドラを定義して入力に応じて変換するはずだが、理解が合っているなら、エンジンがそれを自動化しようとして最善の推測をしているということなのだろう
数年前にこれを見て興奮し、ブラウザ上で PyTorch っぽいライブラリと一緒に動くように Pyodide へ移植した: https://srush.github.io/g9py/
JavaScript より少しカクつくが、そもそも動くこと自体が驚きだ
これは魔女術のように感じる。ものすごく印象的だ
2D CAD の制約ベースのスケッチツールと共通点があるように見える
どこに使うべきかはよく分からないが、本当に賢く作られている
問題の捉え方があまりに違うので、感覚をつかむには何かに実際に使ってみる必要がありそう
教育現場で三角関数や他の数学概念を伝えるのに非常に役立つ可能性がある
数式を視覚的な空間に対応させるのに苦労する視覚型学習者は多いが、少しインタラクションを入れると理解を固める大きな助けになる