Loomlet は、結果ではなく関係を記述する。
Loomlet graph は、環境から出力を導くためのシリアライズ可能な定義である。
graph 自体は、決定論的な関係として扱う。つまり、同じ環境と同じ評価規則が与えられれば、同じ出力を生成するべきである。
言い換えると、次の原則が成り立つ。
同じ graph + 同じ environment + 同じ評価規則 = 同じ出力
この原則が Loomlet の設計判断の土台である。
ただし、Loomlet はすべての host behavior を完全に決定論的にすることを目的としない。
物理、デバイス入力、描画、AI サービス、乱数、外部 API など、host 固有または非決定的な処理は、environment input または同期済み result として扱う。
Loomlet の役割は、同期済み environment から、決定論的に記述できる振る舞いを各 runtime が再現できるようにすることである。
Loomlet は振る舞いを次の 3 つの層に分離する。
Graph は、値、イベント、状態、出力の関係を記述する。
Graph はホスト言語のコードではなく、データである。
Graph が JSON として表現されることで、シリアライズ、送信、ランタイム編集、AI 生成、ノードとしての可視化、そして Web、Unity、Godot など複数のホスト環境での実行が可能になる。
Graph は、可能な限り決定論的で、副作用を持たない状態に保つべきである。
Environment は、graph を評価するために外部から与えられる入力を含む。
Environment には次のものが含まれる。
例として、スライダー値、ボタン押下、乗り物の発車イベント、プレイヤー操作、ページ送り、その他 graph に注入される外部シグナルがある。
Environment は graph から分離される。
この分離が重要である。Loomlet は、計算済みのすべての結果を同期する必要はない。
代わりに、Loomlet は environment を同期する。すべてのクライアントが同じ graph を同じ environment で評価すれば、各クライアントは同じ結果を独立に計算できる。
Runtime は、graph を environment に対して評価し、その結果として得られた出力をホストシステムに適用する。
Runtime は次の責務を持つ。
Web、Unity、Godot など複数の runtime が存在し得るが、それらは同じ評価規則に従うべきである。
Loomlet graph は、scene 全体に対して持つことも、個別の scene object に attach することもできる。
Scene-level graph は、scene 全体の状態や object 間の関係を記述する。
例:
Scene-level graph は、object 間の橋渡しを担当する。
たとえば、button object が押されたとき、door object を開くという関係は、button object や door object の内部に直接埋め込むのではなく、scene-level graph に記述できる。
Object-level graph は、個別 object の振る舞いを記述する。
例:
Object-level graph は、基本的に自分自身の振る舞いを記述する。
他 object を直接変更するのではなく、environment event、scene-level graph、または output command を通じて関係を扱う。
同じ graph は、異なる object context と environment で評価することで、多数の object に再利用できる。
たとえば、100 個の bullet object が存在する場合でも、それぞれに同じ bullet motion graph を attach し、spawnTime、initialPosition、direction、speed などの environment だけを変えて評価できる。
このモデルにより、Loomlet core が動的に N 個の object を graph 内で直接管理する必要を減らせる。
Object の生成、削除、識別子、graph attachment は SceneSync または host 側が管理する。
Loomlet は、連続的な結果ではなく原因を同期する。
オブジェクトの位置、回転、アニメーション値、その他の計算済み出力を毎フレーム broadcast する代わりに、Loomlet は environment を同期することを優先する。
連続的な振る舞いは、各クライアントがローカルで計算する。
例:
これにより、通信量を減らし、振る舞いの再生、デバッグ、再現を容易にする。
Loomlet が読むべき入力は、生の local input ではなく、同期対象として確定した environment event である。
たとえば、マルチプレイで一人のプレイヤーがボタンを押した場合、local device はまず local input を検出する。
しかし、door を開く、ride を開始する、score を加算するなど、scene の共有状態に影響する処理は、同期済み environment event に基づいて行うべきである。
推奨される流れは次の通りである。
Environment event には、少なくとも次の情報を含めるべきである。
同期において重要なのは、全クライアントが同じ event set、同じ event order、同じ timestamp、同じ payload を観測できることである。
ただし、local feedback は committed event を待たずに表示してもよい。
たとえば、ボタンが押された瞬間の軽いアニメーション、音、ハイライト、触覚 feedback などは local input に基づいて即時に出せる。
共有状態に影響する committed behavior と、操作感のための local feedback は分けて扱う。
Loomlet runtime は、object-level graph 間で評価順に依存する振る舞いを避けるべきである。
各 evaluation tick において、runtime は次の順序で処理する。
Object-level graph は、同じ tick 内で他の object-level graph が生成した output を観測しないべきである。
観測できるのは、前回 commit 済みの environment / scene state、または現在 tick の同期済み environment snapshot である。
この方式を次のように表せる。
snapshot → evaluate → collect outputs → resolve conflicts → apply
この評価モデルにより、object の評価順が結果に影響することを避ける。
同じ property に対して複数の graph が同時に output を生成すると、結果が曖昧になる。
例:
このような競合は避けるべきである。
基本方針として、Loomlet runtime は single writer rule を採用することが望ましい。
つまり、1 つの property に対して書き込める graph は原則として 1 つにする。
他 object に影響を与えたい場合は、その object の Transform を直接書き換えるのではなく、environment event、command、または scene-level graph を通じて依頼する。
例:
Object の生成や削除は、graph 評価中に即座に反映しない方がよい。
推奨される扱いは次の通りである。
これにより、同じ tick 内で生成順や評価順に依存する挙動を避けられる。
Projectile など、生成された瞬間から進行しているように見せたい object では、spawnTime を environment に含める。
Object-level graph は serverTime - spawnTime を使って現在位置を計算できる。
Loomlet runtime は、host frame ごとに 1 回だけ graph を評価する必要はない。
Host の frame loop と Loomlet の evaluation tick は、異なる周期で動作してよい。
決定論的な振る舞いのために、Loomlet は同期された時刻に基づく固定 timestep で評価されるべきである。
Runtime は、現在の同期時刻に追いつくために、1 つの host frame 内で Loomlet を複数回評価してよい。
Unity の場合、Unity Update と Loomlet tick は次のように分離できる。
推奨される評価モデルは次の通りである。
Loomlet の graph 評価中に host object を直接変更してはならない。
Host の変更は、収集された output command を適用する段階でのみ行うべきである。
Unity の Transform、GameObject、Renderer などの Unity API は、原則として Unity main thread 上で扱う。
Loomlet の評価を worker thread 化する場合でも、worker thread は純粋な graph 評価と output command 生成に限定し、Unity API への反映は main thread で行う。
Environment 同期モデルが成立するためには、Loomlet の評価が決定論的である必要がある。
そのため、Loomlet は次のルールに従うべきである。
この中心原則をより厳密に書くと、次のようになる。
同じ graph + 同じ environment + 同じ評価規則 = 同じ出力
Loomlet の多くのノードは、純粋な関係ノードであるべきである。
state を持つ振る舞いは許可するが、それらは明示的でなければならない。
例として、delay、previous-value、accumulator、low-pass filter、state-machine ノードなどがある。
副作用は output 境界に隔離するべきである。
つまり Loomlet は、次のものを区別するべきである。
Graph は、何が起きるべきかを記述する。
Runtime は、それを host 環境にどう適用するかを決定する。
すべてを Loomlet の決定論的モデルに押し込むべきではない。
一部の振る舞いは、host 固有のシステム、物理エンジン、外部サービス、AI 生成、乱数、デバイス固有データ、その他の非決定的な処理に依存する場合がある。
そのような場合、SceneSync または別の host 同期レイヤーが、結果を直接同期してよい。
実用上、Loomlet と SceneSync は次の 2 種類の同期戦略を併用できる。
Loomlet は、決定論的な関係として記述できる振る舞いに使う。
SceneSync は、外部結果として同期する必要がある振る舞いを扱う。
例:
これらは Loomlet graph 内に隠すのではなく、environment input または同期済み result として扱う。
Loomlet は、Arrowized FRP に着想を得た、シリアライズ可能な振る舞い graph システムである。
Loomlet は関係を JSON として記述し、environment を分離し、その environment をクライアント間で同期し、各 runtime が結果を独立に計算する。
Scene-level graph は object 間の関係を記述する。
Object-level graph は個別 object の振る舞いを記述する。
Runtime は immutable な environment snapshot に対して graph を評価し、output command を収集し、host scene に適用する。
Loomlet の役割は、同期可能な振る舞いを記述することである。
SceneSync の役割は、environment を同期し、object の生成・削除・識別子・graph attachment を管理し、必要に応じて外部結果を同期することである。
Arrowized FRP や Yampa などの背景概念については、Appendix: Influences を参照する。
Loomlet はまだ experimental だが、以下のワークフローは実装済みである。
.loom DSL の parser/compiler@afjk/loomlet としての npm package boundaryただし、API とデータ形式はまだ変更される可能性がある。
Loomlet は、Core、拡張パック、統合プロダクトの 3 層で構成される。
Core はホストに依存しない言語処理系である。
Core に含めるもの:
Core に含めないもの:
Core は、外部世界への副作用を直接実行しない。
Core は @afjk/loomlet package として共有できるように public exports を整理中である。
現在の主な export 対象:
ただし、npm 公開と package 安定化はまだ進行中である。
拡張パックは、ホスト固有の source / sink / adapter を提供する薄いレイヤーである。
例:
拡張パックは、scene.setPosition や dom.setText のような使いやすいノードを提供してよい。ただし、それらは Core そのものではなく、ホスト I/O への変換として扱う。
統合プロダクトは、Core と拡張パックを組み合わせてユーザーが使える形にしたものである。
例:
統合プロダクトは、UI、保存、ネットワーク、デプロイ、ホスト固有の UX を持ってよい。
Loomlet は、.loom テキスト、ノードエディタ、ランタイム実行、ホスト連携を同じ形式で無理に扱わない。
用途ごとに次の表現へ分ける。
DSL Source
↓ parse
Source AST
↓ lower / normalize
Graph AST
↓ compile
Runtime Graph
↓ adapt
Target Graph
Node Editor は Graph AST を表示・編集し、座標や選択状態などの UI 情報は EditorModel / Node Editor ViewModel として別に持つ。
| 表現 | 主な役割 | 人間 | AI | ランタイム | ノードエディタ |
|---|---|---|---|---|---|
| DSL Source | .loom テキスト。人間・AI・Git が扱う正本 |
◎ | ◎ | × | △ |
| Source AST | DSL の構文情報。コメント、raw literal、source range を保持 | × | △ | × | △ |
| Graph AST | ノード、ポート、エッジ、params、source map を持つ編集向け中間表現 | ○ | ◎ | △ | ◎ |
| Runtime Graph | 実行に必要な最小グラフ。評価器が読む形式 | △ | ○ | ◎ | △ |
| Target Graph | Scene Sync / Unity / Web など各ホスト向けに変換された形式 | △ | ○ | host 側 | △ |
| EditorModel / Node Editor ViewModel | ノード位置、選択、zoom、pan など UI 状態 | △ | × | × | ◎ |
DSL Source は正本である。人間と AI が読み書きしやすく、Git diff でも扱いやすい。
Source AST は、DSL を安全に編集するための構文表現である。コメント、元の数値表記、名前付き引数、source range などを保持する。
Graph AST は、ノードエディタと AI が構造を理解するための中間表現である。ノード、ポート、エッジ、params、source map を持つ。
Runtime Graph は、実行に必要な情報だけを持つ最小表現である。Loomlet runtime は基本的にこれを評価する。
Target Graph は、Scene Sync、Unity、Web runtime など、実行先の世界に合わせた形式である。
EditorModel / Node Editor ViewModel は、表示上の状態である。ノード座標、選択状態、zoom、pan などはプログラムの意味とは別なので分離する。
現在の実装では、ノードエディタ向けの共有表現として EditorModel を使用する。
EditorModel は Runtime Graph とは異なり、ノードの編集に必要な情報を含む。
ただし、ノード位置・label・comment などの実行に不要な情報は hidden editor metadata として保存し、Runtime Graph の意味には含めない。
ホストとは、Loomlet Core を載せて動かす外側の実行環境である。
例:
Core はホスト固有の副作用を直接実行しない。
Core は次のような境界 API を通じてホストとやり取りする。
setInput(channel, value)
emitEvent(channel, event)
getOutput(channel)
onOutput(channel, callback)
ホストは、現在値やセンサー値を setInput で Core に渡す。
例:
ホストは、一回性のイベントを emitEvent で Core に渡す。
例:
Core は、外の世界に反映したい結果を output として生成する。
例:
{
"channel": "scene.position",
"objectId": "cube",
"value": [1, 0, 0]
}
実際の副作用は Core ではなく、拡張パックまたは host adapter が実行する。
例:
loomlet-scenesync は scene.position output を Scene Sync の scene-delta に変換するloomlet-unity は同じ output を Unity の Transform.position に反映するloomlet-web は DOM output を HTMLElement に反映するこのモデルにより、同じ Loomlet graph を複数のホストへ移植しやすくする。
.loom の DSL Source を正本とする。
ノードエディタは、DSL から生成された Graph AST を表示・編集する UI である。ノードエディタ独自の保存形式を正本にはしない。
最初から完全な双方向変換を目指さず、編集可能範囲を段階的に広げる。
Level 1: 数値リテラル編集
Level 2: 文字列 / boolean / objectId 編集
Level 3: ノード名 / 変数名変更
Level 4: sink ノード追加
Level 5: compute ノード追加
Level 6: edge 再接続
Level 7: 任意の Graph AST から DSL を再生成
当初は Level 1〜2 から始める方針だったが、現在の Editor Studio では、param 編集、node 追加/削除/rename、connection 編集、canonical DSL 再生成、Graph → DSL auto sync まで実験的に実装している。
ただし、任意の DSL 構文を完全に保持したまま双方向編集することは、まだ保証しない。
Graph 側の編集は、必要に応じて canonical DSL として再生成される。
source map を使った最小差分 patch(Level 1〜6 相当)は設計思想として残すが、現在の実装では canonical regeneration を主経路としている。
例:
x = math.sine(t, freq: 0.2, amplitude: 2, offset: 0)
freq をノードエディタで 0.5 に変更した場合、現在の実装では canonical DSL を再生成する(source map patch ではなく)。
x = math.sine(t, freq: 0.5, amplitude: 2, offset: 0)
また、DSL 変更時には layout preservation により、可能な範囲でノード位置を保持する。
この方針により、DSL の可読性、Git diff、AI 編集、ノードエディタの操作性を両立する。
Editor Studio は、ノード位置・label・comment など、実行意味に影響しない編集情報を hidden editor metadata として .loom ファイル内に保存する。
この metadata は Runtime Graph の意味には含めない。
現在の方針:
metadata の詳細形式は今後変更される可能性があるため、固定しすぎない。
Loomlet は、ブラウザで動くステートレスなデータフロー実行エンジンです。JSON でグラフを定義し、毎フレーム値を計算・更新します。
英語での一行説明:
A stateless dataflow engine for the browser. Build reactive visual, audio, and 3D content by composing pure functions.
日本語での説明:
ブラウザで動くステートレスなデータフロー実行エンジン。純粋な関数の合成により、リアクティブな視覚・音響・3D コンテンツを構築します。
Loomlet の核は、以下の仕組みです:
engine.getValue() で任意のノードの出力値を取得し、画面や音響に反映状態(過去を引きずる値)を持たず、現在時刻と入力だけから出力が決まる純粋なデータフローを基盤とします。
意図:
「状態部品」という限定された種類の部品にだけ状態を持たせ、グラフ上で目に見える形で管理します。
意図:
外部への副作用(位置や色を変える、音を鳴らす、メッセージを送るなど)は「シンク」専用部品でのみ行います。中間の計算は副作用を持ちません。
意図:
内部はテキストの専用記法(DSL)で持ち、UI はそれを視覚化します。両方向に変換可能で、人間も AI も自由に行き来できます。
注意: 当初(第ゼロ段階)は DSL とビジュアル UI を実装せず JSON 直書きから始めたが、現在は DSL parser/compiler と Editor Studio が実装済みである。
意図:
すべての値は次のどちらかに分類されます。
連続値(Behavior)
常に何らかの値が流れている「川」のようなもの。時間的に途切れません。例:
当初(第ゼロ段階)は連続値のみを扱っていたが、現在はイベント型も実装済みである。
イベント(Event)
時々瞬間的に発生する「雷」のようなもの。時間的に離散的です。例:
イベントは第一段階(実装済み)で導入された。
部品は次の 5 つのカテゴリに分かれます。
入力なしで値を生み出すノード。
例: clock(時刻)、constant(定数)
外界から値を受け取るノード。ユーザーの操作やネットワーク通信などが対象。
例: pointerPosition(マウス位置)、pointerClick(クリック)、webhook(HTTP リクエスト)
バージョン 0.3.0 では smoothLerp、lowpass、delay1、integrate を実装済み。
純粋な計算で値を変換する。状態を持ちません。
例: add(足し算)、multiply(掛け算)、sine(正弦波)、map(配列変換)
内部に状態を持つ唯一のカテゴリ。過去の値を記憶し、それに基づいて出力を決定します。
例: smoothLerp(easing follow)、lowpass(平滑化)、delay1(1フレーム遅延)、integrate(積分)
状態部品は実装済みである(smoothLerp、lowpass、delay1、integrate)。
外部への副作用を持つノード。値を受け取り、画面・音響・ネットワークなどに影響を与えます。
例: setPosition(位置変更)、setText(テキスト変更)、setStyle(スタイル変更)
DOM シンク部品および SceneSync / Three.js アダプタ経由のシンク部品は実装済みである。
TYPE_MISMATCH エラーevent<vec2> → event<void>)も TYPE_MISMATCH エラー例外: sample ノードの value 入力ポート(Behavior 型)には Event 型の上流から接続することが許される唯一のケース。これは Behavior 値をイベントトリガでサンプリングするための設計である。
clock、constant、sine、add、multiplyengine.getValue() で外部から値を取得loom.js)として配布accum、smooth など)setPosition など)engine.dispatchEvent(ref, payload) APIpointerClick、pointerPosition、keyDown、keyUpfilter、sample、mergesetText、setStyle、setAttr、setClass、setCssVar、setTransform2D、log各ノード型は、内部的に以下のメタデータを持つオブジェクトとして定義されます。
{
category: "source" | "transform" | "state" | "sink" | "input",
inputs: [
{ name: "t", type: "number", default: 0 },
...
],
outputs: [
{ name: "out", type: "number" }
],
params: [
{ name: "freq", type: "number", default: 1 },
...
],
evaluate: (inputs, params, ctx) => outputs
}
このメタデータは以下の目的で利用されます。
すべてのノードは、入力ポートとパラメータについて次の優先順位で値を決定します。
params の同名フィールドの値を使うこのルールにより、すべてのノードの設定値は「グラフ上で動的に変えたければエッジで接続、静的に固定したければ params で指定」という形で統一的に扱えます。
カテゴリ: ソース部品
入力: なし
出力:
t(経過秒、エンジン起動時を 0 とする)
number(double)パラメータ: なし
説明:
エンジン起動からの経過時間(秒)を常に出力します。
例:
{
"id": "timer",
"type": "clock"
}
カテゴリ: ソース部品
入力: なし
出力:
out(定数値)
number(デフォルト 0)パラメータ:
value(出力する定数値、型:number、デフォルト 0)説明:
パラメータで指定した定数値を常に出力します。変わりません。
例:
{
"id": "freq_source",
"type": "constant",
"params": {
"value": 2.5
}
}
カテゴリ: 変換部品
入力:
t(時刻、未接続時は 0)
numberfreq(周波数、Hz、未接続時は params.freq、デフォルト 1)
numberamplitude(振幅、未接続時は params.amplitude、デフォルト 1)
numberphase(位相、ラジアン、未接続時は params.phase、デフォルト 0)
numberoffset(オフセット、未接続時は params.offset、デフォルト 0)
number出力:
out(正弦波出力)
numbersin(t * freq * 2π + phase) * amplitude + offsetパラメータ:
freq(周波数、Hz、型:number、デフォルト 1)amplitude(振幅、型:number、デフォルト 1)phase(位相、ラジアン、型:number、デフォルト 0)offset(オフセット、型:number、デフォルト 0)説明:
時刻 t を入力として、正弦波を計算します。周波数、振幅、位相、オフセットでカスタマイズ可能です。
例:
{
"id": "oscillator",
"type": "sine",
"params": {
"freq": 2.0,
"amplitude": 1.5,
"phase": 0,
"offset": 0
}
}
カテゴリ: 変換部品
入力:
a(加数、未接続時は params.a、デフォルト 0)
numberb(加数、未接続時は params.b、デフォルト 0)
number出力:
out(合計)
numbera + bパラメータ:
a(入力 a が未接続のときのデフォルト値、型:number、デフォルト 0)b(入力 b が未接続のときのデフォルト値、型:number、デフォルト 0)説明:
2 つの数値を足し合わせます。入力が未接続の場合、対応する params の値が使われます。
例:
{
"id": "summer",
"type": "add",
"params": {
"b": 5
}
}
カテゴリ: 変換部品
入力:
a(被乗数、未接続時は params.a、デフォルト 1)
numberb(乗数、未接続時は params.b、デフォルト 1)
number出力:
out(積)
numbera * bパラメータ:
a(入力 a が未接続のときのデフォルト値、型:number、デフォルト 1)b(入力 b が未接続のときのデフォルト値、型:number、デフォルト 1)説明:
2 つの数値を掛け合わせます。入力が未接続の場合、対応する params の値が使われます。
例:
{
"id": "scaler",
"type": "multiply",
"params": {
"b": 0.5
}
}
カテゴリ: 変換部品
入力:
a(被減数、未接続時は params.a、デフォルト 0)
numberb(減数、未接続時は params.b、デフォルト 0)
number出力:
out(差)
numbera - bパラメータ:
a(デフォルト 0)b(デフォルト 0)説明:
2 つの数値の差を計算します。a から b を引きます。
カテゴリ: 変換部品
入力:
a(被除数、未接続時は params.a、デフォルト 0)
numberb(除数、未接続時は params.b、デフォルト 1)
number出力:
out(商)
numberb === 0 ? 0 : a / bパラメータ:
a(デフォルト 0)b(デフォルト 1)説明:
2 つの数値の商を計算します。除数が 0 の場合は 0 を返します。
カテゴリ: 変換部品
入力:
a(被除数、未接続時は params.a、デフォルト 0)
numberb(除数、未接続時は params.b、デフォルト 1)
number出力:
out(剰余)
numberb === 0 ? 0 : ((a % b) + b) % bパラメータ:
a(デフォルト 0)b(デフォルト 1)説明:
a を b で割った剰余を返します。負数に対応し、常に非負の値を返します(数学的な正のモジュロ)。除数が 0 の場合は 0 を返します。
カテゴリ: 変換部品
入力:
a(未接続時は params.a、デフォルト 0)
number出力:
out(符号反転値)
number-aパラメータ:
a(デフォルト 0)説明:
数値の符号を反転させます。正を負に、負を正に変えます。
カテゴリ: 変換部品
入力:
a(未接続時は params.a、デフォルト 0)
number出力:
out(絶対値)
numberMath.abs(a)パラメータ:
a(デフォルト 0)説明:
数値の絶対値を返します。負の値は正に、正の値はそのまま返します。
カテゴリ: 変換部品
入力:
value(未接続時は params.value、デフォルト 0)
numbermin(最小値、未接続時は params.min、デフォルト 0)
numbermax(最大値、未接続時は params.max、デフォルト 1)
number出力:
out(クランプされた値)
numbermin > max ? min : Math.max(min, Math.min(max, value))パラメータ:
value(デフォルト 0)min(デフォルト 0)max(デフォルト 1)説明:
入力値を min と max の範囲に挟みます。min > max の場合は min を返します。
カテゴリ: 変換部品
入力:
a(開始値、未接続時は params.a、デフォルト 0)
numberb(終了値、未接続時は params.b、デフォルト 1)
numbert(補間パラメータ、未接続時は params.t、デフォルト 0)
number出力:
out(補間値)
numbera + (b - a) * tパラメータ:
a(デフォルト 0)b(デフォルト 1)t(デフォルト 0)説明:
2 つの値 a と b を t で線形補間します。t=0 で a、t=1 で b が得られます。t が 0~1 の範囲外でも外挿(クランプされない)します。
カテゴリ: 変換部品
入力:
x(入力値、未接続時は params.x、デフォルト 0)
numberedge0(下辺、未接続時は params.edge0、デフォルト 0)
numberedge1(上辺、未接続時は params.edge1、デフォルト 1)
number出力:
out(スムーズステップ値)
numberパラメータ:
x(デフォルト 0)edge0(デフォルト 0)edge1(デフォルト 1)説明:
GLSL の smoothstep に準拠した関数です。x が edge0 より小さい場合は 0、edge1 より大きい場合は 1 を返します。その間では、エルミート補間で滑らかに遷移する値を返します。edge0 === edge1 の場合は、x < edge0 なら 0、そうでなければ 1 を返します。
カテゴリ: 変換部品
入力:
value(入力値、未接続時は params.value、デフォルト 0)
numberinMin(入力範囲の最小、未接続時は params.inMin、デフォルト 0)
numberinMax(入力範囲の最大、未接続時は params.inMax、デフォルト 1)
numberoutMin(出力範囲の最小、未接続時は params.outMin、デフォルト 0)
numberoutMax(出力範囲の最大、未接続時は params.outMax、デフォルト 1)
number出力:
out(リマップ値)
numberパラメータ:
value(デフォルト 0)inMin(デフォルト 0)inMax(デフォルト 1)outMin(デフォルト 0)outMax(デフォルト 1)clamp(boolean、デフォルト false):入力値が範囲外の場合、true なら出力をクランプ、false なら外挿する説明:
入力範囲 [inMin, inMax] から出力範囲 [outMin, outMax] へ値をリマップします。TouchDesigner の Math CHOP の Range/Map 機能に相当します。inMin === inMax の場合は outMin を返します。clamp パラメータは 入力ポートではなくパラメータのみ です。
カテゴリ: 変換部品
入力:
t(時刻、未接続時は params から解決、デフォルト 0)
numberfreq(周波数(Hz)、未接続時は params.freq、デフォルト 1)
numberamplitude(振幅、未接続時は params.amplitude、デフォルト 1)
numberphase(位相(ラジアン)、未接続時は params.phase、デフォルト 0)
numberoffset(オフセット、未接続時は params.offset、デフォルト 0)
number出力:
out(コサイン出力)
numberMath.cos(t * freq * 2 * Math.PI + phase) * amplitude + offsetパラメータ:
freq(デフォルト 1)amplitude(デフォルト 1)phase(デフォルト 0)offset(デフォルト 0)説明:
コサイン波を出力します。sine と同じシグネチャですが、Math.cos を使用します。Lissajous 曲線など、複数の異なる周波数のトリゴノメトリック関数を組み合わせるのに利用します。
カテゴリ: 状態部品
入力:
value(目標値、未接続時は params.value、未指定時は params.initial)
number出力:
out
numberパラメータ:
value(デフォルト 0)rate(デフォルト 5)initial(デフォルト 0)説明:
時間ベースの指数追従。dt を使って prevOut から目標値へ滑らかに収束する。評価式は prevOut + (value - prevOut) * (1 - exp(-rate * dt))。
カテゴリ: 状態部品
入力:
value(入力値、未接続時は params.value、未指定時は params.initial)
number出力:
out
numberパラメータ:
value(デフォルト 0)tau(デフォルト 0.2 秒)initial(デフォルト 0)説明:
時定数ベースの一次ローパスフィルタ。評価式は prevOut + (value - prevOut) * dt / (tau + dt)。tau=0 の場合は即時追従になる。
カテゴリ: 状態部品
入力:
value(入力値、未接続時は params.value、未指定時は params.initial)
number出力:
out
numberパラメータ:
value(デフォルト 0)initial(デフォルト 0)説明:
1 フレーム前の入力値を返す。出力 out は現在の prevOut、次フレームに保存される内部状態は現在フレームの入力値。実装上は evaluate の戻り値で _newState を明示する唯一の標準ノード。
カテゴリ: 状態部品
入力:
value(積分対象。単位は per-second)
number出力:
out
numberパラメータ:
value(デフォルト 0)initial(デフォルト 0)min(デフォルト null)max(デフォルト null)説明:
prevOut + value * dt を評価し、必要なら min / max でクランプする。ゲージ、累積、減衰量の管理などに使う。
カテゴリ: 入力部品
入力: なし
出力:
event(クリックイベント)
event<vec2>(ペイロードはクリック位置 {x, y})パラメータ:
target(オプション、文字列):CSS セレクタ。指定するとそのセレクタに合致する DOM 要素上のクリックのみを発生させる。未指定なら window のクリックすべて。説明:
ブラウザの pointer down イベントを購読し、クリックがあったフレームに event ポートからイベントを発生させる。engine.dispatchEvent を経由する経路(後述)か、ノード型が start() 内で自動購読する形のいずれかで実装される(実装は第一段階プロトタイプで決定)。
ペイロード座標系:
event<vec2> の {x, y} は viewport 基準(clientX / clientY 相当)の座標。ピクセル単位。target がウィンドウ全体の場合(target 省略時または "window"):ブラウザビューポート左上が原点。target が DOM 要素の場合:座標は依然として viewport 基準のまま発火する(要素相対への変換は呼び出し側が getBoundingClientRect() を使って行う)。将来 Unity 等の非 DOM 環境で実装する場合は、当該プラットフォームの「画面座標系」相当(左上原点、ピクセル単位)にマップする。
カテゴリ: 入力部品
入力: なし
出力:
pos(現在のポインタ位置)
vec2({x, y})パラメータ: なし
説明:
現在のマウス/タッチ位置を Behavior 型として常時出力する。値は最後に観測された位置で、初回観測前は {x: 0, y: 0}。
座標系:
pos は pointerClick と同じ viewport 基準の {x, y}。ピクセル単位。{x: 0, y: 0}。非 DOM 環境での解釈は pointerClick に準ずる。
カテゴリ: 入力部品
入力: なし
出力:
event(キー押下イベント)
event<string>(ペイロードは KeyboardEvent.key)パラメータ:
key(オプション、文字列):指定するとそのキーのみフィルタする説明:
keydown を購読し、該当フレームに event ポートからイベントを発生させる。
カテゴリ: 入力部品
入力: なし
出力:
event(キー離鍵イベント)
event<string>(ペイロードは KeyboardEvent.key)パラメータ:
key(オプション、文字列):指定するとそのキーのみフィルタする説明:
keyup を購読し、該当フレームに event ポートからイベントを発生させる。
カテゴリ: 変換部品
入力:
event(任意の Event 型)出力:
event(入力と同じ Event 型)パラメータ:
predicate(必須、文字列):制限式 DSL で記述された条件式。制限式 DSL の文法:
predicate は以下の文法に限定される。new Function や eval での評価は禁止。
0、1.5、-3.14'Enter'、'a'true、falsevalue:イベントペイロード本体key:ペイロードが文字列の場合のみ参照可能。それ以外は undefined 扱いvalue.x、value.y:ペイロードがオブジェクトの場合のフィールドアクセス(1 段のみ、ネスト不可)==、!=、<、<=、>、>=&&、||、!+、-、*、/(数値同士のみ)( ) によるグループ化! > 算術 > 比較 > && > ||(C 系言語準拠)禁止事項:
Math.abs(value) など)は使用不可value.toString() など)は使用不可=、+= 等)は使用不可value.a.b)不可例:
value > 0key == 'Enter'value.x > 100 && value.y < 200!(key == 'Escape')実装方針:
predicate は load() 時にパースして抽象構文木(AST)に変換し、評価は AST のインタプリタで行う。これにより JavaScript 環境と他環境(C#、Unity 等)で同一の評価結果を保証する。パース失敗時は INVALID_GRAPH エラーを投げ、details: { reason: "filter.predicate", nodeId, error } を含める。
実装メモ:
バージョン 0.2.0 では new Function ベースの評価を廃止し、制限式 DSL のパーサ・インタプリタで predicate を評価する。これにより、複数環境での一貫性が保証される。
カテゴリ: 変換部品
入力:
trigger(型:event<void>):サンプリングのきっかけvalue(型:任意の Behavior):サンプリング対象の連続値出力:
event(型:event<入力 value の値型>):trigger が発火した瞬間の value をペイロードに持つイベントパラメータ: なし
説明:
trigger イベントが発火したフレームに、その時点の value の値をペイロードとして含むイベントを出力する。クリックした瞬間のマウス位置を取得する、といった用途に使う。
カテゴリ: 変換部品
入力:
a(型:event<T>)b(型:event<T>):a と同じペイロード型出力:
event(型:event<T>)パラメータ: なし
説明:
複数の Event ストリームを1本にまとめる。同一フレームに両方発生した場合、出力配列は a の全ペイロード、その後に b の全ペイロード、という順序で連結される。下流ノードはこの順序を前提にしてよい。
カテゴリ: 変換部品
入力:
value(型:number、デフォルト:0)threshold(型:number、デフォルト:0)出力:
out(型:boolean)パラメータ:
value(型:number、デフォルト:0)threshold(型:number、デフォルト:0)説明:
value > threshold を評価し、真偽値を返す。しきい値判定をシンプルに記述するためのノード。
カテゴリ: 変換部品
入力:
value(型:number、デフォルト:0)threshold(型:number、デフォルト:0)出力:
out(型:boolean)パラメータ:
value(型:number、デフォルト:0)threshold(型:number、デフォルト:0)説明:
value < threshold を評価し、真偽値を返す。greaterThan と対で使える比較ノード。
カテゴリ: シンク部品
入力:
value(型:any、デフォルト:"")出力: なし
パラメータ:
target(型:string、デフォルト:""):CSS セレクタ説明:
DOM 要素のテキスト内容を更新する。document.querySelector(target) で要素を取得し、その textContent に value を文字列化して設定します。要素が見つからない場合、何もしない(エラーにしない)。
カテゴリ: シンク部品
入力:
value(型:any、デフォルト:"")出力: なし
パラメータ:
target(型:string、デフォルト:""):CSS セレクタproperty(型:string、デフォルト:""):スタイルプロパティ名unit(型:string、デフォルト:""):単位(例:”px”、”em”、”“)説明:
DOM 要素の CSS スタイルを更新する。el.style[property] = String(value) + unit として設定されます。例えば value=50、property="width"、unit="px" なら、el.style.width = "50px" となります。要素が見つからない場合、何もしない(エラーにしない)。
カテゴリ: シンク部品
入力:
enabled(型:boolean、デフォルト:true)出力: なし(シンクノードは副作用専用のため出力を持たない)
パラメータ:
target(型:string、デフォルト:""):CSS セレクタclassName(型:string、デフォルト:""):付け外しするクラス名説明:
element.classList.toggle(className, Boolean(enabled)) を実行してクラスを付与/削除する。対象要素がない場合、className が空の場合は何もしない。
カテゴリ: シンク部品
入力:
value(型:any、デフォルト:0)出力: なし(シンクノードは副作用専用のため出力を持たない)
パラメータ:
target(型:string、デフォルト:""):CSS セレクタname(型:string、デフォルト:""):CSS カスタムプロパティ名unit(型:string、デフォルト:""):単位文字列説明:
CSS custom property を更新する。name が -- で始まらない場合は自動で -- を補う。value が null / undefined の場合や対象要素がない場合は何もしない。
カテゴリ: シンク部品
入力:
x(型:number、デフォルト:0)y(型:number、デフォルト:0)scale(型:number、デフォルト:1)rotate(型:number、デフォルト:0)出力: なし(シンクノードは副作用専用のため出力を持たない)
パラメータ:
target(型:string、デフォルト:""):CSS セレクタunit(型:string、デフォルト:"px"):translate の単位rotateUnit(型:string、デフォルト:"deg"):rotate の単位説明:
style.transform を translate(...) scale(...) rotate(...) 形式でまとめて設定する。setStyle でも transform は設定できるが、setTransform2D は 2D 変形を分かりやすく扱うための専用シンク。対象要素がない場合は何もしない。
カテゴリ: シンク部品
入力:
value(型:any、デフォルト:"")出力: なし
パラメータ:
target(型:string、デフォルト:""):CSS セレクタname(型:string、デフォルト:""):属性名説明:
DOM 要素の HTML 属性を更新する。el.setAttribute(name, String(value)) として設定されます。例えば name="data-count" なら data-count 属性が更新されます。要素が見つからない場合、何もしない(エラーにしない)。
カテゴリ: シンク部品
入力:
value(型:any、デフォルト:undefined)出力: なし
パラメータ:
label(型:string、デフォルト:""):ラベル説明:
ブラウザコンソールにメッセージを出力する。console.log(label || "log", value) として実行されます。デバッグ用。
注: 以下のノードは、src/loom-three.js のアダプタを通じて登録されます。コアには含まれず、registerThreeNodes(Loom, objects) 呼び出しで利用可能になります。
カテゴリ: シンク部品
入力:
x(型:number、デフォルト:0、kind: behavior)y(型:number、デフォルト:0、kind: behavior)z(型:number、デフォルト:0、kind: behavior)出力: なし
パラメータ:
target(型:string、デフォルト:""):操作対象の Three.js Object3D のキー説明:
Three.js Object3D の位置を設定します。registerThreeNodes(Loom, { objectKey: mesh }) で登録されたオブジェクトに対して、target: "objectKey" でアクセスできます。position.set(x, y, z) が呼ばれます。
カテゴリ: シンク部品
入力:
x(型:number、デフォルト:0、kind: behavior):X軸周りの回転(ラジアン、オイラー角)y(型:number、デフォルト:0、kind: behavior):Y軸周りの回転(ラジアン、オイラー角)z(型:number、デフォルト:0、kind: behavior):Z軸周りの回転(ラジアン、オイラー角)出力: なし
パラメータ:
target(型:string、デフォルト:""):操作対象の Three.js Object3D のキー説明:
Three.js Object3D の回転をオイラー角(ラジアン)で設定します。rotation.set(x, y, z) が呼ばれます。
カテゴリ: シンク部品
入力:
x(型:number、デフォルト:1、kind: behavior)y(型:number、デフォルト:1、kind: behavior)z(型:number、デフォルト:1、kind: behavior)出力: なし
パラメータ:
target(型:string、デフォルト:""):操作対象の Three.js Object3D のキー説明:
Three.js Object3D のスケールを設定します。scale.set(x, y, z) が呼ばれます。
カテゴリ: シンク部品
入力:
r(型:number、デフォルト:1、kind: behavior):赤成分(0..1)g(型:number、デフォルト:1、kind: behavior):緑成分(0..1)b(型:number、デフォルト:1、kind: behavior):青成分(0..1)出力: なし
パラメータ:
target(型:string、デフォルト:""):操作対象の Three.js Object3D のキー説明:
Three.js Object3D のマテリアルの色を RGB(0..1 範囲)で設定します。material.color.setRGB(r, g, b) が呼ばれます。
マテリアル配列対応: material が配列の場合、第一実装では 最初の要素のみ を更新します。material[0].color.setRGB(r, g, b) が対象。
カテゴリ: シンク部品
入力:
visible(型:any、デフォルト:true、kind: behavior):表示状態の真偽値出力: なし
パラメータ:
target(型:string、デフォルト:""):操作対象の Three.js Object3D のキー説明:
Three.js Object3D の表示・非表示を切り替えます。visible = !!inputs.visible として設定されます。
注: 以下のノードは、src/loom-scenesync.js のアダプタを通じて登録されます。コアには含まれず、new LoomSceneSync(...) で利用可能になります。
カテゴリ: ソース部品
入力: なし
出力:
t(サーバ同期済み時刻、秒)
numberパラメータ:
adapterId(型:string、自動注入):アダプタインスタンス ID説明:
全クライアント同期済みのサーバ時刻を出力します。コンストラクタで渡された getServerTime() の戻り値を返します。
カテゴリ: シンク部品
入力:
x、y、z(型:number、デフォルト:0)出力: なし
パラメータ:
target(型:string):オブジェクト IDadapterId(型:string、自動注入):アダプタインスタンス ID説明:
オブジェクトの位置を設定します。resolveTarget(target) で取得したオブジェクトの position.set(x, y, z) を呼びます。
カテゴリ: シンク部品
入力:
x、y、z(型:number、デフォルト:0、オイラー角ラジアン)出力: なし
パラメータ:
target(型:string):オブジェクト IDadapterId(型:string、自動注入):アダプタインスタンス ID説明:
オブジェクトの回転をオイラー角(ラジアン)で設定します。
カテゴリ: シンク部品
入力:
x、y、z(型:number、デフォルト:1)出力: なし
パラメータ:
target(型:string):オブジェクト IDadapterId(型:string、自動注入):アダプタインスタンス ID説明:
オブジェクトのスケールを設定します。
カテゴリ: シンク部品
入力:
r、g、b(型:number、デフォルト:1、0..1 範囲)出力: なし
パラメータ:
target(型:string):オブジェクト IDadapterId(型:string、自動注入):アダプタインスタンス ID説明:
オブジェクトのマテリアル色を RGB で設定します。material が配列の場合は最初の要素のみ更新します。
カテゴリ: シンク部品
入力:
visible(型:boolean、デフォルト:true)出力: なし
パラメータ:
target(型:string):オブジェクト IDadapterId(型:string、自動注入):アダプタインスタンス ID説明:
オブジェクトの表示・非表示を設定します。
グラフは、nodes と edges を持つオブジェクトで表現されます。loom および meta はオプションです。
{
"loom": "0.0.1",
"meta": {
"name": "sample-graph",
"author": "afjk",
"description": "Sin 波を出力するサンプル"
},
"nodes": [ ... ],
"edges": [ ... ]
}
loom(オプション、文字列):グラフが対象とする Loomlet 仕様のバージョン。後方互換性チェックなどに使用meta(オプション、オブジェクト):人間や AI 向けの自由なメタデータ。エンジンの評価には影響しないnodes(必須、配列):ノード定義の配列edges(必須、配列):エッジ定義の配列各ノードは以下の構造を持ちます:
{
"id": "unique_node_id",
"type": "node_type_name",
"params": { ... }
}
id(必須):ノードを一意に識別する文字列。グラフ内で重複してはいけません。type(必須):ノードの型("clock"、"constant"、"sine"、"add"、"multiply" など)params(オプション):ノードのパラメータ。型によって異なります。省略可能で、その場合はデフォルト値が使われます。各エッジは、値の流れを表現します:
{
"from": "sourceNodeId.portName",
"to": "targetNodeId.portName"
}
from(必須):送信側のノードの出力ポート("nodeId.outputPortName" 形式)to(必須):受信側のノードの入力ポート("nodeId.inputPortName" 形式)時刻を取得し、その時刻に対する正弦波を計算するグラフです:
{
"nodes": [
{
"id": "timer",
"type": "clock"
},
{
"id": "wave",
"type": "sine",
"params": {
"freq": 1.0,
"amplitude": 1.0,
"phase": 0,
"offset": 0
}
}
],
"edges": [
{
"from": "timer.t",
"to": "wave.t"
}
]
}
このグラフでは:
clock ノードが時刻 t を生成sine ノードに入力sine ノードが正弦波 out を出力外部から engine.getValue("wave.out") を呼ぶと、現在の正弦波の値が得られます。
定数 10 と時刻を足し、さらに 2 倍にするグラフ:
{
"nodes": [
{
"id": "const10",
"type": "constant",
"params": {
"value": 10
}
},
{
"id": "timer",
"type": "clock"
},
{
"id": "adder",
"type": "add"
},
{
"id": "doubler",
"type": "multiply",
"params": {
"b": 2
}
}
],
"edges": [
{
"from": "const10.out",
"to": "adder.a"
},
{
"from": "timer.t",
"to": "adder.b"
},
{
"from": "adder.out",
"to": "doubler.a"
}
]
}
このグラフの最終結果は、engine.getValue("doubler.out") で取得できます。
Loomlet の評価モデルは「指定された時刻のグラフ状態を計算する」ことを中核とします。エンジン本体は時刻を内部で進めるのではなく、外部から evaluateAt(time) を呼ぶことで、その時刻におけるすべてのノード出力を確定させます。start() / stop() は requestAnimationFrame を使って evaluateAt を毎フレーム呼ぶ便利ラッパーであり、テストや決定論的再生では evaluateAt を直接呼ぶ運用が想定されています。
new Loom(graph)const engine = new Loom(graph);
引数:
graph(オブジェクト):グラフ定義(nodes と edges を持つオブジェクト)説明:
グラフ定義を受け取り、エンジンを初期化します。この時点では評価ループは開始されていません。
グラフにサイクル(循環参照)が存在する場合、このコンストラクタはエラーを投げます。
例:
const graph = { nodes: [...], edges: [...] };
const engine = new Loom(graph);
engine.evaluateAt(time)engine.evaluateAt(time);
引数:
time(数値):評価する時刻(秒、エンジン起動を 0 とする想定)説明:
指定された時刻におけるグラフ全体を一度評価し、すべてのノードの出力値を内部に保存します。呼び出し後、getValue() でその時刻における任意のノード出力を取得できます。
evaluateAt は Loomlet の中核 API であり、start() / stop() はこれを requestAnimationFrame で繰り返し呼ぶ便利ラッパーです。テストや、外部の時刻ソース(サーバ時刻、録画再生など)に同期したい場合は、evaluateAt を直接呼ぶ運用が想定されています。
呼び出し時、保留中のグラフ(load() で渡されたもの)があれば、評価開始前に切り替えが行われます。
例:
const engine = new Loom(graph);
engine.evaluateAt(0);
console.log(engine.getValue("wave.out"));
engine.evaluateAt(0.5);
console.log(engine.getValue("wave.out"));
engine.dispatchEvent(ref, payload)engine.dispatchEvent("click1.event", { x: 100, y: 200 });
引数:
ref(文字列):"nodeId.portName" 形式で、入力ノードの Event 型出力ポートを指定payload(任意):Event のペイロード。event<void> の場合は省略可能説明:
外部からの非同期イベント(DOM のクリック等)をエンジンに注入する。呼び出し時点でキューに積まれ、次の evaluateAt 呼び出しの中で該当ノードのその出力ポートからイベントが発生し、下流ノードへ伝播する。
入力ノードの実装は通常、コンストラクタや start() 内で DOM イベントリスナを登録し、その中で dispatchEvent を呼ぶ形になる。
engine.getValue(ref)const value = engine.getValue("nodeId.portName");
引数:
ref(文字列):"nodeId.portName" 形式で、ノードの出力ポートを指定戻り値:
number)説明:
グラフ上の任意のノードの現在の出力値を取得します。
指定されたノードやポートが存在しない場合、またはまだ評価されていない場合の動作は未定義です。
Behavior ポート参照時の戻り値:
evaluateAt() で評価された最新値。一度も評価されていないポートは undefined。Event ポート参照時の戻り値:
getValue("nodeId.eventPort") で Event 型ポートを参照した場合の戻り値は以下のとおり:
[](undefined ではない)。これは内部の Event 伝播メカニズムにおいて、同一 tick 内で複数の payload が同一ポートを通過し得るため、配列形式で統一する。下流の Event ノード(filter、merge、sample)はこの配列を要素ごとに処理する。
注意:
evaluateAt() を呼んだ直後にのみ Event の発火状態が反映される。start() ループ外で getValue() を呼んでも、最後の evaluateAt() 時点の Event 配列が返る。次の evaluateAt() 呼び出しで Event 配列はクリアされる。
補足: 出力ポート名はノード型によって異なります。clock の出力ポートは t、pointerPosition の出力ポートは pos、それ以外のノード(状態ノードを含む)のデフォルト出力ポートは out です。
例:
const current_time = engine.getValue("timer.t");
const wave_value = engine.getValue("wave.out");
const keyEvents = engine.getValue("kd.event"); // 配列 or 空配列
engine.load(graph)engine.load(graph);
引数:
graph(オブジェクト):新しいグラフ定義説明:
グラフを差し替えます。動作は以下の三段階です。
evaluateAt 呼び出し開始時に切り替え:次に evaluateAt(time) が呼ばれた瞬間、保留中のグラフが新しい現行グラフになり、評価はそのグラフに対して行われます。id の state ノードが新グラフにも残っていれば、その prevOut は引き継がれます。消えた id の state は破棄されます。新規 id の state は params.initial から始まります。評価ループ実行中に呼ばれた場合も、フレーム途中でグラフがすり替わることはありません。同一フレーム内で load() 直後に getValue() を呼ぶと、古いグラフの値が返ります。
例:
engine.load(newGraph);
console.log(engine.getValue("wave.out")); // 古いグラフの値
engine.evaluateAt(1.0); // ここで切り替え
console.log(engine.getValue("wave.out")); // 新しいグラフの値
engine.start()engine.start();
説明:
リアルタイム評価ループを開始します。これは便利機能であり、内部では requestAnimationFrame を使い、毎フレーム evaluateAt(t) を呼び出しているだけです(t はエンジン起動からの経過秒)。
開始時には dt 計算用の前回タイムスタンプをリセットするため、再開直後の最初のフレームは dt=0 になります。state ノードの内部状態そのものは保持されます。
リアルタイム実行を必要としない場合(テスト、サーバサイド、手動制御)は、evaluateAt() を直接呼び出すことを推奨します。
呼び出し後、engine.stop() が呼ばれるまで評価が続きます。
例:
engine.start();
engine.stop()engine.stop();
説明:
start() で開始したリアルタイム評価ループを停止します。state ノードの内部状態は保持されるため、再度 start() した場合は続きから動作します。
呼び出し後も getValue() で最後の値を取得することはできます。手動で evaluateAt() を呼ぶこともできます。
例:
engine.stop();
Loom.registerNodeType(name, definition)(静的メソッド)Loom.registerNodeType('setPosition', {
category: 'sink',
inputs: [...],
outputs: [],
params: [...],
evaluate: (inputs, params, ctx) => ({})
});
引数:
name(文字列):登録するノード型の名前definition(オブジェクト):ノード型のメタデータ定義。「5. ノード仕様」で説明した構造と同じ説明:
外部からノード型を追加する。アダプタライブラリ向けの拡張ポイント。
同じ name で既に登録済みの場合、LoomError (code: 'DUPLICATE_NODE_TYPE')をスロー。
例:
import { Loom } from './src/loom.js';
import { registerThreeNodes } from './src/loom-three.js';
registerThreeNodes(Loom, objectsMap); // 内部で Loom.registerNodeType を呼ぶ
const engine = new Loom(graph);
nodes 配列を検証(重複した ID がないか確認)edges 配列を検証(存在しないノードやポートへのエッジがないか確認)毎フレーム、以下の情報をコンテキストとして保持します:
time:エンジン起動からの経過秒数(秒単位、小数含む)dt:前フレームからの経過秒数。初回は 0、上限は 0.1prevOut:state ノードのみ。前フレームに保存された内部状態。未初期化時は params.initialこのコンテキストは、ノード評価時に参照されます(例:clock ノードが time を出力し、state ノードが dt と prevOut を参照する)。
毎フレーム、以下の処理が行われます:
engine.load() で渡されたもの)があれば、現行グラフに切り替えtime を evaluateAt(time) の引数で更新し、前回フレームとの差から dt を計算する(最大 0.1 秒にクランプ)dispatchEvent で積まれた保留イベントを、対応する入力ノードの出力に反映するevaluate(inputs, params, { time, dt, prevOut, engine, ... }) を呼ぶ。戻り値に _newState があればそれを、なければ out を次フレーム用の内部状態として保存するinputs.value に NaN / Infinity が入った場合は 0 として扱うprevOut や計算結果が NaN / Infinity になった場合は params.initial に戻すdt はエンジン側でクランプし、各 state ノードでは個別にクランプしない入力エッジが接続されていないポートの値は undefined として扱われます。各ノード型は、未接続ポートに対して以下のデフォルト値で処理します。
sine.t:未接続時は 0sine.freq:未接続時は params.freq(デフォルト 1)sine.amplitude:未接続時は params.amplitude(デフォルト 1)sine.phase:未接続時は params.phase(デフォルト 0)sine.offset:未接続時は params.offset(デフォルト 0)add.a:未接続時は params.a(デフォルト 0)add.b:未接続時は params.b(デフォルト 0)multiply.a:未接続時は params.a(デフォルト 1)multiply.b:未接続時は params.b(デフォルト 1)これにより、エンジンは未接続ポートがあっても安全に評価を継続できます。
ES Module(ESM)形式の単一 JavaScript ファイルとして配布されます。
<script type="module">
import { Loom } from './src/loom.js';
const engine = new Loom(graph);
engine.start();
</script>
src/loom.jsリポジトリのルートではなく、src/ ディレクトリに配置されます。
ビルドツール(webpack、esbuild など)を使いません。人が読める単一ファイルのまま配布されます。
ゼロです。Three.js や d3.js などの外部ライブラリに依存しません。
これにより、ファイルサイズが最小化され、読み込みが高速化されます。
コアエンジン(src/loom.js)は依存ゼロを保つが、特定ライブラリとの連携は別ファイルのアダプタとして提供する。
src/loom-three.js:Three.js Object3D 連携。registerThreeNodes(Loom, objects) で利用。アダプタは Loom コアに依存し、コアの Loom.registerNodeType(name, definition) 経由でノード型を登録する。
Loomlet がスローするエラーは、以下の構造を持つ Error オブジェクトです。
{
name: "LoomError",
message: "...",
code: "ERROR_CODE",
details: { ... }
}
code フィールドにより、利用側はエラーの種類をプログラム的に判別できます。
| code | 発生タイミング | details の内容 |
説明 |
|---|---|---|---|
DUPLICATE_NODE_ID |
コンストラクタ、load() |
{ nodeId } |
同じ ID のノードが複数存在する |
UNKNOWN_NODE_TYPE |
コンストラクタ、load() |
{ nodeId, type } |
未知のノード型が指定された |
UNKNOWN_NODE |
コンストラクタ、load() |
{ nodeId } |
エッジが存在しないノードを参照している |
UNKNOWN_PORT |
コンストラクタ、load() |
{ nodeId, port, side } |
エッジが存在しないポートを参照している(side は "input" または "output") |
DUPLICATE_INPUT_EDGE |
コンストラクタ、load() |
{ nodeId, port } |
同じ入力ポートに複数のエッジが接続されている |
CYCLE |
コンストラクタ、load() |
{ nodeIds } |
グラフにサイクルが存在する。第ゼロ段階では一律エラー扱い |
TYPE_MISMATCH |
コンストラクタ、load() |
{ from, to, fromType, toType } |
エッジが Behavior と Event を混ぜている、またはペイロード型が一致しない |
DUPLICATE_NODE_TYPE |
Loom.registerNodeType() |
{ name } |
同じ名前のノード型が既に登録されている |
INVALID_GRAPH |
コンストラクタ、load() |
{ reason } |
上記以外の構造的問題(nodes が配列でない等) |
new Loom(graph):コンストラクタ呼び出し時にグラフを検証し、問題があれば即座にスローするengine.load(graph):呼び出し時にグラフを検証し、問題があれば即座にスローする。スローした場合、保留中のグラフ切り替えは発生しないengine.evaluateAt(time):実行時のエラーは原則発生しない(未接続ポートはデフォルト値で処理されるため)Loomlet は単一の JSON グラフ表現を真の単一ソースとし、複数の評価環境(JavaScript / C# / その他)で同一の入力に対し同一の出力を返すことを保証する。
ステートレスなノードのみで構成されたグラフは、以下を入力として与えれば全環境で同一結果を返す:
evaluateAt(time) に渡す timedispatchEvent 呼び出し列(順序とペイロード)ステートを持つノード(将来追加される accum、smooth 等)はノード自身に状態保持責任があり、状態の初期値・更新規則は当該ノード仕様で定義する。
数値演算は IEEE 754 倍精度(double / float64)を使用する。sin、cos、sqrt 等の超越関数の最終ビットは環境依存となる場合があるが、視覚的・聴覚的同期に問題のないレベルとする(厳密一致が必要な用途では deterministic math ライブラリの導入を将来検討)。
filter.predicate で使用される制限式 DSL(5.10 節)は、全環境で同一の AST に変換され、同一のインタプリタ規則で評価される。各環境のホスト言語の構文や評価規則に依存しない。
入力ノード(pointerClick、pointerPosition、keyDown、keyUp)は環境依存の API を利用するため、環境ごとに同等の機能を提供するアダプタが必要となる:
addEventListenerInputSystem または Input.GetKey各環境で発火タイミング・ペイロード形式は本仕様(5.6〜5.9)に準拠する。
複数クライアントで結果を揃えるには、共有された時刻ソースが必要となる。これは serverClock ノード(SceneSync アダプタの src/loom-scenesync.js で実装済み)で実現する。evaluateAt(time) の time を全クライアントで揃えれば、ステートレスグラフの結果は揃う。
engine.dispatchEvent(ref, payload) API の実装pointerClick、pointerPosition、keyDown、keyUpfilter、sample、mergegetValue() 戻り値の配列化vec2unpack / vec2pack ノードの追加(vec2 とスカラの相互変換)。これにより pointerPosition をスカラ系ノード(map, lerp 等)と組み合わせ可能になる。
vec2unpack:入力 vec2 を x, y という2つの number 出力に分解vec2pack:2つの number 入力を vec2 にパックaccum、smooth、delay など)の実装src/loom-three.js(setPosition、setRotation、setScale、setColor、setVisible シンクノード)現在の Loomlet CLI では、Scene Sync link code の redeem、session 保存、room/object の確認、.loom から Scene Sync behavior graph へのcompile/run/dev workflow を実験的に実装している。
src/loom-scenesync.js の追加(Loomlet リポジトリ側のアダプタ層)serverClock ノード追加(クロスクライアント時刻同期)sceneSetPosition、sceneSetRotation、sceneSetScale、sceneSetColor、sceneSetVisibleredeem / objects / probe / graph-compile / graph-run / dev watchsample-cube に対する behavior graph 適用Loomlet の主方向は、Scene Sync 上のオブジェクトに時間変化する振る舞いを与える behavior layer である。
engine.load() によるランタイム差し替え対応)。Transform / Renderer に直接書き込む。accum、smooth 等)と Sink ノードの一般化仕様書のバージョン: 0.2.0(クロスプラットフォーム仕様確定版)
JavaScript 版 Loomlet と同じ JSON グラフ形式を、Unity C# ランタイムでも評価できる。
実装は unity/com.afjk.loom/ に配置された Unity Package として提供される。
evaluateAt(time) に相当する EvaluateAt(double time) を毎フレーム呼び出すLoad(graph) は JS 版同様、次の EvaluateAt() まで切り替えを保留するfilter.predicate の式 DSL は JS / C# 両方で評価される== != < <= > >=)、論理(&& || !)、算術(+ - * /)、括弧sceneSetRotation はラジアン入力を受け付ける(JS 版と合わせるため)Transform.localEulerAngles は度数法のため、内部で radians * (180 / π) 変換を行う以下は現バージョンでは Unity 側に実装しない:
Loomlet の通常ノードはステートレスな純粋関数であり、evaluate(inputs, params) の結果は入力のみで決まる。これは graph を「時刻 t の関数 f(t)」として扱える純度の高い性質を生むが、一方で smoothing / delay / integrate / easing follow のような「前フレーム値を必要とする挙動」は表現できない。
これらを graph 外の JS に逃がすと、Loomlet graph から挙動が見えなくなり、Loomlet の「graph に挙動を閉じ込める」という思想からむしろ外れてしまう。そこで Loomlet は state を禁止するのでも無制限に許すのでもなく、明示的に隔離されたカテゴリとして導入する。
category: "state" のノードだけである。engine.load() 時、同じ id の state ノードは state を引き継ぐ。params.initial から始まる。state を使う。dt は秒単位で、フレーム間のタイムスタンプ差から算出される。dt の上限は 0.1 秒にクランプされる(タブ非アクティブ時の暴走を防ぐため)。evaluate(inputs, params, { prevOut, dt }) の形で呼ばれる。prevOut として渡される。engine.load(newGraph) では、同じ id の state ノードは prevOut を保持し、異なる id は params.initial から開始する。prevOut を更新せず、必要に応じて initial にリセットする。Loomlet の標準的な同期モデルは「全クライアントが同じ graph JSON を受け取り、それぞれ独自に評価する」というものである。state ノードはこのモデル上、各クライアントで独立に進行する。
params.initial と同じ入力履歴(クロック、pointerClick イベントなど)が両端で揃っていれば、state ノードの値は収束する。integrate で大きなカウントを保持し続ける等)は、必要なら別途 broadcast 機構で値を共有する設計を将来検討する。| Node | 用途 | 主なパラメータ | 式 |
|---|---|---|---|
smoothLerp |
目標値への指数的収束(easing follow) | rate (1/sec), initial |
out = prevOut + (value - prevOut) * (1 - exp(-rate * dt)) |
lowpass |
ノイズ除去・平滑化 | tau (sec), initial |
out = prevOut + (value - prevOut) * (dt / (tau + dt)) |
delay1 |
1 フレーム前の入力を出力 | initial |
out = prevOut、内部状態として現在の入力を次フレームへ |
integrate |
入力の時間積分 | min, max, initial |
out = clamp(prevOut + value * dt, min, max) |
DSL を「書ける言語」から「編集・生成・変換できる言語」へ拡張するため、Loomlet は Source AST を中間表現として公開する。
| 関数 | 役割 |
|---|---|
parseDSLToAST(source) |
DSL を Source AST に変換。throw せず errors を返す。 |
compileToGraph(ast) |
Source AST を graph JSON に lower する。throw せず errors を返す。 |
formatDSL(ast, options?) |
Source AST を整形済み DSL に変換。決定的。 |
type SourceAST = Program;
interface Program { type: "Program"; body: Statement[]; span: Span; }
type Statement = AssignmentStatement | RenderStatement | CommentStatement;
interface AssignmentStatement { type: "AssignmentStatement"; target: Identifier; value: Expression; span: Span; leadingComments?: Comment[]; trailingComment?: Comment; }
interface RenderStatement { type: "RenderStatement"; call: CallExpression; span: Span; leadingComments?: Comment[]; trailingComment?: Comment; }
interface CommentStatement { type: "CommentStatement"; comment: Comment; span: Span; }
type Expression = CallExpression | PipeExpression | Identifier | NumberLiteral | StringLiteral | BooleanLiteral | NullLiteral | ArrayLiteral | ObjectLiteral;
interface CallExpression { type: "CallExpression"; callee: Identifier; args: Argument[]; span: Span; }
interface PipeExpression { type: "PipeExpression"; left: Expression; right: CallExpression; span: Span; }
type Argument = PositionalArg | NamedArg;
interface PositionalArg { type: "PositionalArg"; value: Expression; span: Span; }
interface NamedArg { type: "NamedArg"; name: Identifier; value: Expression; span: Span; }
interface Identifier { type: "Identifier"; name: string; span: Span; }
interface NumberLiteral { type: "NumberLiteral"; value: number; raw: string; span: Span; }
interface StringLiteral { type: "StringLiteral"; value: string; raw: string; span: Span; }
interface BooleanLiteral { type: "BooleanLiteral"; value: boolean; span: Span; }
interface NullLiteral { type: "NullLiteral"; span: Span; }
interface ArrayLiteral { type: "ArrayLiteral"; elements: Expression[]; span: Span; }
interface ObjectLiteral { type: "ObjectLiteral"; entries: ObjectEntry[]; span: Span; }
interface ObjectEntry { type: "ObjectEntry"; key: Identifier | StringLiteral; value: Expression; span: Span; }
interface Comment { type: "Comment"; text: string; variant: "line"; span: Span; }
interface Span { start: { line: number; column: number; offset: number }; end: { line: number; column: number; offset: number }; }
parseDSLToAST → formatDSL → parseDSLToAST で得られる AST は、初回 AST と span を除いて等価。コメント・引数順・キー順・リテラル raw 表現・パイプ式の構造が保持される。
本章で定義した Source AST 型は後方互換を意識したバージョニングの対象。_internal プレフィックスの type は予告なく変更され得る(本バージョンでは未使用)。
現在の parser 実装は最初の 1 件の ParseError を errors 配列で返す。将来、error recovery 実装により複数件収集を予定。
graphToAST(graph): CanonicalASTpatchDSL(originalSource, newGraph): stringastVersion)CallExpression.multiline, Statement.blankLinesBefore, PipeExpression.lineBreakBefore)Loomlet は次の 3 つの truth を分離して扱う。
これらを橋渡しする関数として parseDSLToAST / compileToGraph / graphToEditorModel / editorModelToGraph / applyEditorOperation を提供する。Source AST と Editor Model は構造が異なる(配列ベース vs Map ベース)ため、相互変換は GraphJSON を中継して行うのが基本。
EditorNode: { id, type, category, params, position }EditorEdge: { id, fromNodeId, fromPort, toNodeId, toPort }EditorModel: { nodesById, edgesById, order }EditorOperation: addNode / removeNode / updateParam / moveNode / addEdge / removeEdgegraphToEditorModel(graph): GraphJSON を nodesById / edgesById / order に変換。meta.position がないノードには layoutFallback を適用する。editorModelToGraph(em, originalGraph = null): order で node 配列を再構築し、position を meta.position に保存。edge は edge id 昇順で決定論的に出力し、originalGraph.render を継承できる。applyEditorOperation(em, op): EditorModel をイミュータブル更新する。removeNode は関連 edge も削除し、removeNode / removeEdge は存在しない id で no-op。layoutFallback(nodes): category 別の決定論的グリッド配置を行う。layoutFallback 仕様input: x=0transform: x=300state: x=600sink: x=900position を持つノードは再配置しない。nodesById / edgesById を CRDT マップとしてそのまま扱う方針。Loomlet supports single-expression function literals: fn(x) => math.multiply(x, 2).
Most Loomlet calls allow only the first argument to be positional. Later arguments must be named.
Common binary operator nodes are an exception and may use two positional arguments:
math.add(1, 2)
math.subtract(10, 3)
math.multiply(x, 2)
math.divide(x, 2)
math.mod(x, 3)
More descriptive or multi-argument calls still require named arguments:
logic.greaterThan(x, other: 2)
math.map(x, inMin: 0, inMax: 1, outMin: 0, outMax: 100)
Functions can be assigned to variables: double = fn(x) => math.multiply(x, 2).
Functions can capture values from outer scope: base = 10 and addBase = fn(x) => math.add(x, base).
Functions can be passed to list nodes: list.map(numbers, fn: double).
Current limitations: