@wcstack/raf 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +176 -0
- package/README.md +177 -0
- package/dist/auto.js +3 -0
- package/dist/auto.min.js +3 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.esm.js +698 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.esm.min.js +2 -0
- package/dist/index.esm.min.js.map +1 -0
- package/package.json +73 -0
package/README.ja.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# @wcstack/raf
|
|
2
|
+
|
|
3
|
+
`@wcstack/raf` は wcstack エコシステムのヘッドレス requestAnimationFrame コンポーネントです。
|
|
4
|
+
|
|
5
|
+
視覚的な UI ウィジェットではありません。
|
|
6
|
+
ブラウザの「描画機会」をリアクティブな状態に変える**非同期プリミティブノード**であり、時間源を周期(`setInterval`)からフレーム(`requestAnimationFrame`)に差し替えた `@wcstack/timer` の兄弟です。
|
|
7
|
+
|
|
8
|
+
`@wcstack/state` と組み合わせると、`<wcs-raf>` はパス契約で直接バインドできます:
|
|
9
|
+
|
|
10
|
+
- **入力面**: `once` / `repeat` / `manual` / `trigger`
|
|
11
|
+
- **出力状態面**: `tick` / `elapsed` / `dt` / `running` / `suspended`
|
|
12
|
+
- **コマンド**: `start` / `stop` / `reset` / `pause` / `resume`
|
|
13
|
+
|
|
14
|
+
ゲームループやアニメーションドライバを、rAF の再登録・dt の簿記・後始末コードなしに、HTML で宣言的に書けるということです。
|
|
15
|
+
|
|
16
|
+
## なぜ存在するか — `wcs-timer` とどう使い分けるか
|
|
17
|
+
|
|
18
|
+
`<wcs-timer interval="16">` でもゲームループは回せますが、`setInterval` はディスプレイのリフレッシュに揃わず、フレーム差分は利用側で自前計測が必要でした。`<wcs-raf>` は実際の描画機会で tick し、差分(`dt`)を一級の出力として配ります。
|
|
19
|
+
|
|
20
|
+
| | `<wcs-timer>` | `<wcs-raf>` |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 時間源 | `setInterval`(選んだ周期) | `requestAnimationFrame`(ディスプレイのフレーム) |
|
|
23
|
+
| `interval` 入力 | あり | **なし** — rAF に周期は無い |
|
|
24
|
+
| `dt` 出力 | なし | **あり**(中断を跨ぐと `0`、下記参照) |
|
|
25
|
+
| 非表示タブ | ~1Hz にスロットル | **完全停止** — `suspended` で顕在化 |
|
|
26
|
+
| 向く用途 | ポーリング・カウントダウン・時計 | ゲームループ・アニメーション・毎フレーム計測 |
|
|
27
|
+
|
|
28
|
+
## インストール
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @wcstack/raf
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## クイックスタート
|
|
35
|
+
|
|
36
|
+
### 1. 宣言的ゲームループ
|
|
37
|
+
|
|
38
|
+
`<wcs-raf>` は DOM に接続されると自動でフレームループを開始します。`tick` / `dt` をバインドするか、event token でフレームを受けて 1 フレーム 1 ステップを回します:
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
42
|
+
<script type="module" src="https://esm.run/@wcstack/raf/auto"></script>
|
|
43
|
+
|
|
44
|
+
<wcs-state>
|
|
45
|
+
<script type="module">
|
|
46
|
+
export default {
|
|
47
|
+
x: 0,
|
|
48
|
+
$eventTokens: ["frame"],
|
|
49
|
+
$on: {
|
|
50
|
+
frame: (state, e) => { state.x += 60 * (e.detail.dt / 1000); }, // 60px/s
|
|
51
|
+
},
|
|
52
|
+
get transform() { return `translateX(${this.x}px)`; },
|
|
53
|
+
};
|
|
54
|
+
</script>
|
|
55
|
+
</wcs-state>
|
|
56
|
+
|
|
57
|
+
<wcs-raf data-wcs="eventToken.tick: frame"></wcs-raf>
|
|
58
|
+
<div class="box" data-wcs="style.transform: transform"></div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`e.detail` は `{ count, elapsed, dt, timestamp }` を運びます — `dt` で積分すればフレームレート非依存の運動になります。
|
|
62
|
+
|
|
63
|
+
### 2. 1 フレームだけ(rAF 一発呼びの宣言化)
|
|
64
|
+
|
|
65
|
+
`once` は次の描画機会にちょうど 1 tick 発火して自動停止します:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<wcs-raf once data-wcs="tick: afterNextPaint"></wcs-raf>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
補足: 自動 start された `once` フレームは接続の約 1 フレーム後に一度だけ発火し、再発火しません。state 自体を非同期ロードする構成(`<wcs-state src="...">` 等)ではバインディングの attach がこの唯一の tick より遅れ、永久に取りこぼす可能性があります。その構成では `manual` にして state 準備後にコマンド / trigger で起動するか、state をインラインにしてください(同一タスク内の attach は必ず間に合います)。
|
|
72
|
+
|
|
73
|
+
### 3. 有限フレーム
|
|
74
|
+
|
|
75
|
+
`repeat="N"` は N フレームで停止します(`running` が `false` になります)。
|
|
76
|
+
|
|
77
|
+
## 属性 / 入力
|
|
78
|
+
|
|
79
|
+
| 属性 | 型 | 既定 | 説明 |
|
|
80
|
+
| --------- | ------- | ------- | ------------ |
|
|
81
|
+
| `once` | boolean | `false` | 1 フレームだけ発火して停止。`repeat="1"` の糖衣。 |
|
|
82
|
+
| `repeat` | number | `0` | N フレームで停止(`0` = 無制限)。`once` より優先。 |
|
|
83
|
+
| `manual` | boolean | `false` | 接続時に自動 start しない。コマンド / trigger で開始。 |
|
|
84
|
+
|
|
85
|
+
`<wcs-timer>` から意図的に削除したもの: `interval`(rAF に周期は無い)と `immediate`(初回フレームがすでに「次の描画機会」であり、それより早い意味のある時点が存在しない)。
|
|
86
|
+
|
|
87
|
+
## 観測可能プロパティ(出力)
|
|
88
|
+
|
|
89
|
+
| プロパティ | イベント | 説明 |
|
|
90
|
+
| ----------- | ------------------------- | ------------ |
|
|
91
|
+
| `tick` | `wcs-raf:tick` | フレームカウンタ。毎発火で増加(`reset` で 0)。 |
|
|
92
|
+
| `elapsed` | `wcs-raf:tick` | 最後の reset からの**アクティブ**時間(Σdt、ms)。非表示・ポーズ期間は加算されない。粒度はフレーム単位。 |
|
|
93
|
+
| `dt` | `wcs-raf:tick` | 直前フレームとの差分(ms)。**`start()` / `resume()` / visibility 中断の直後の初回フレームは `0`** — 中断を跨いだ値は観測者に届かない。上限クランプは無し: 遅いフレームの扱いはドメイン判断(物理ループなら自前の `Math.min(dt, …)`)。 |
|
|
94
|
+
| `running` | `wcs-raf:running-changed` | 開始済みの**意図**。非表示タブでフレームが届かなくても `true` のまま。 |
|
|
95
|
+
| `suspended` | `wcs-raf:suspended-changed` | 配送の**実態**。`running` かつ非表示タブで `true`(rAF はスロットルでなく完全停止)。desired/actual の分離は `@wcstack/wakelock` の `active`/`held` と同型。 |
|
|
96
|
+
|
|
97
|
+
`tick` / `elapsed` / `dt` は単一の `wcs-raf:tick` イベントからの派生です(`detail = { count, elapsed, dt, timestamp }`。`timestamp` はフレームの `DOMHighResTimeStamp`、`reset()` 通知では `0`)。`tick` は同値ガード無しで毎フレーム発火、`running` / `suspended` は同値ガード付きです。
|
|
98
|
+
|
|
99
|
+
## コマンド
|
|
100
|
+
|
|
101
|
+
| コマンド | 説明 |
|
|
102
|
+
| --------- | ----------------------------------------------------------------------- |
|
|
103
|
+
| `start` | フレームループ開始(実行中は no-op)。 |
|
|
104
|
+
| `stop` | 停止。`tick` / `elapsed` は保持。 |
|
|
105
|
+
| `reset` | 停止して `tick` / `elapsed` / `dt` を `0` に。 |
|
|
106
|
+
| `pause` | 値と有限 run の残数を保持したまま中断。 |
|
|
107
|
+
| `resume` | `pause` から再開。直後の初回フレームは `dt = 0`。 |
|
|
108
|
+
|
|
109
|
+
state からの起動は command-token プロトコルで:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<wcs-raf manual data-wcs="command.start: $command.beginLoop"></wcs-raf>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## DOM トリガー(オプション)
|
|
116
|
+
|
|
117
|
+
`autoTrigger`(既定 on)が有効なら、`data-raftarget="<id>"` を持つ要素のクリックで対象 `<wcs-raf>` の `start()` が呼ばれます。マッチしたクリックは `event.preventDefault()` されます — デフォルトアクションも活かしたい要素(実リンク・submit ボタン等)には `data-raftarget` を付けないでください。
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<button data-raftarget="loop">Start</button>
|
|
121
|
+
<wcs-raf id="loop" manual data-wcs="eventToken.tick: frame"></wcs-raf>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## `:state()` による CSS スタイリング
|
|
125
|
+
|
|
126
|
+
`<wcs-raf>` は 2 つの boolean 出力状態を CustomStateSet に反映します:
|
|
127
|
+
|
|
128
|
+
| 状態 | オンになる条件 |
|
|
129
|
+
|-------|---------|
|
|
130
|
+
| `running` | `wcs-raf:running-changed` が `true` で発火(`false` でクリア) |
|
|
131
|
+
| `suspended` | `wcs-raf:suspended-changed` が `true` で発火(`false` でクリア) |
|
|
132
|
+
|
|
133
|
+
```css
|
|
134
|
+
wcs-raf:state(running) ~ .indicator { color: green; }
|
|
135
|
+
wcs-raf:state(suspended) ~ .indicator { color: orange; } /* タブ非表示でループ枯渇 */
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
対応: Chrome/Edge 125+、Safari 17.4+、Firefox 126+。非対応環境では状態が付かないだけで動作は継続します(graceful degradation / never-throw)。`debug-states` 属性は DevTools 用に `data-wcs-state-*` 属性をミラーします(デバッグ補助のみ — CSS は `:state()` に書くこと)。
|
|
139
|
+
|
|
140
|
+
## 注意と制約
|
|
141
|
+
|
|
142
|
+
- **非表示タブで rAF は完全停止します**(`setInterval` の ~1Hz スロットルと違う点)。`running` は意図を、`suspended` は実態を報告し、`elapsed` はアクティブ時間のみを数え、復帰後の初回フレームは `dt = 0` — dt 積分する利用側がテレポートを見ることはありません。
|
|
143
|
+
- **`error` 面はありません。** rAF に恒常的な失敗モードは無く、rAF の無い環境(SSR プリパス、worker)では `start()` が silent no-op になります(never-throw)。
|
|
144
|
+
- **SSR**: サーバーレンダリングするマークアップでは `manual` を推奨 — 自動開始したループは DOM エミュレーション環境でもフレームを予約し続けます。
|
|
145
|
+
- プラットフォーム API は呼び出し時に解決され(`globalThis.requestAnimationFrame`)、テスト用に `RafCore` へスケジューラを注入できます。
|
|
146
|
+
|
|
147
|
+
## ヘッドレス利用(`RafCore`)
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { RafCore } from "@wcstack/raf";
|
|
151
|
+
|
|
152
|
+
const core = new RafCore();
|
|
153
|
+
core.addEventListener("wcs-raf:tick", (e) => {
|
|
154
|
+
console.log((e as CustomEvent).detail); // { count, elapsed, dt, timestamp }
|
|
155
|
+
});
|
|
156
|
+
core.observe(); // visibilitychange を購読(`suspended` と hidden 跨ぎの dt=0 正規化の両方を駆動)
|
|
157
|
+
core.start();
|
|
158
|
+
// 後で:
|
|
159
|
+
core.dispose();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 設定
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
import { bootstrapRaf } from "@wcstack/raf";
|
|
166
|
+
|
|
167
|
+
bootstrapRaf({
|
|
168
|
+
autoTrigger: true, // data-raftarget クリック起動(既定: true)
|
|
169
|
+
triggerAttribute: "data-raftarget",
|
|
170
|
+
tagNames: { raf: "wcs-raf" },
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## ライセンス
|
|
175
|
+
|
|
176
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# @wcstack/raf
|
|
2
|
+
|
|
3
|
+
`@wcstack/raf` is a headless requestAnimationFrame component for the wcstack ecosystem.
|
|
4
|
+
|
|
5
|
+
It is not a visual UI widget.
|
|
6
|
+
It is an **async primitive node** that turns the browser's rendering opportunities into reactive state — `@wcstack/timer`'s sibling with the time source swapped from a period (`setInterval`) to the frame (`requestAnimationFrame`).
|
|
7
|
+
|
|
8
|
+
With `@wcstack/state`, `<wcs-raf>` can be bound directly through path contracts:
|
|
9
|
+
|
|
10
|
+
- **input surface**: `once`, `repeat`, `manual`, `trigger`
|
|
11
|
+
- **output state surface**: `tick`, `elapsed`, `dt`, `running`, `suspended`
|
|
12
|
+
- **commands**: `start`, `stop`, `reset`, `pause`, `resume`
|
|
13
|
+
|
|
14
|
+
This means a game loop or animation driver can be expressed declaratively in HTML, without writing `requestAnimationFrame` re-registration, dt bookkeeping, or teardown glue in your UI layer.
|
|
15
|
+
|
|
16
|
+
## Why this exists — and when to prefer it over `wcs-timer`
|
|
17
|
+
|
|
18
|
+
`<wcs-timer interval="16">` can drive a game loop, but `setInterval` is not aligned to the display's refresh, and consumers must measure their own frame delta. `<wcs-raf>` ticks on the browser's actual rendering opportunity and ships the delta as a first-class output.
|
|
19
|
+
|
|
20
|
+
| | `<wcs-timer>` | `<wcs-raf>` |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Time source | `setInterval` (a period you choose) | `requestAnimationFrame` (the display's frame) |
|
|
23
|
+
| `interval` input | yes | **no** — rAF has no period |
|
|
24
|
+
| `dt` output | no | **yes** (`0` across interruptions, see below) |
|
|
25
|
+
| Hidden tab | throttled (~1Hz) | **fully stopped** — surfaced via `suspended` |
|
|
26
|
+
| Use for | polling, countdowns, clocks | game loops, animation, per-frame measurement |
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @wcstack/raf
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### 1. A declarative game loop
|
|
37
|
+
|
|
38
|
+
When `<wcs-raf>` is connected to the DOM, it automatically starts a frame loop. Bind `tick` / `dt` to state paths — or receive the frame as an event token and run one physics step per frame:
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
42
|
+
<script type="module" src="https://esm.run/@wcstack/raf/auto"></script>
|
|
43
|
+
|
|
44
|
+
<wcs-state>
|
|
45
|
+
<script type="module">
|
|
46
|
+
export default {
|
|
47
|
+
x: 0,
|
|
48
|
+
$eventTokens: ["frame"],
|
|
49
|
+
$on: {
|
|
50
|
+
frame: (state, e) => { state.x += 60 * (e.detail.dt / 1000); }, // 60px/s
|
|
51
|
+
},
|
|
52
|
+
get transform() { return `translateX(${this.x}px)`; },
|
|
53
|
+
};
|
|
54
|
+
</script>
|
|
55
|
+
</wcs-state>
|
|
56
|
+
|
|
57
|
+
<wcs-raf data-wcs="eventToken.tick: frame"></wcs-raf>
|
|
58
|
+
<div class="box" data-wcs="style.transform: transform"></div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`e.detail` carries `{ count, elapsed, dt, timestamp }` — integrate against `dt` and the motion speed is frame-rate independent.
|
|
62
|
+
|
|
63
|
+
### 2. One frame (`requestAnimationFrame`-once equivalent)
|
|
64
|
+
|
|
65
|
+
`once` fires exactly one tick on the next rendering opportunity, then auto-stops — the declarative form of a single rAF call:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<wcs-raf once data-wcs="tick: afterNextPaint"></wcs-raf>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Note: the auto-started `once` frame fires about one frame after connect, exactly once — it is never re-fired. If the state itself loads asynchronously (e.g. `<wcs-state src="...">`), its binding may attach after that single tick and miss it permanently. In that setup use `manual` and start via a command / trigger once the state is ready, or keep the state inline (same-task attach is always in time).
|
|
72
|
+
|
|
73
|
+
### 3. Bounded frames
|
|
74
|
+
|
|
75
|
+
`repeat="N"` fires `N` frames and then stops (`running` becomes `false`).
|
|
76
|
+
|
|
77
|
+
## Attributes / Inputs
|
|
78
|
+
|
|
79
|
+
| Attribute | Type | Default | Description |
|
|
80
|
+
| --------- | ------- | ------- | ------------ |
|
|
81
|
+
| `once` | boolean | `false` | Fire a single frame, then stop. Sugar for `repeat="1"`. |
|
|
82
|
+
| `repeat` | number | `0` | Stop after N frames (`0` = unlimited). Takes precedence over `once`. |
|
|
83
|
+
| `manual` | boolean | `false` | Do not auto-start on connect; start via command / trigger. |
|
|
84
|
+
|
|
85
|
+
Deliberately absent vs `<wcs-timer>`: `interval` (rAF has no period) and `immediate` (the first frame already **is** the next rendering opportunity — no earlier meaningful moment exists).
|
|
86
|
+
|
|
87
|
+
## Observable Properties (outputs)
|
|
88
|
+
|
|
89
|
+
| Property | Event | Description |
|
|
90
|
+
| ----------- | ------------------------- | ------------ |
|
|
91
|
+
| `tick` | `wcs-raf:tick` | Frame counter, increments on every fire (reset to 0 on `reset`). |
|
|
92
|
+
| `elapsed` | `wcs-raf:tick` | Accumulated **active** milliseconds (Σdt) since the last reset — hidden/paused periods contribute nothing. Frame-granular: between frames the getter returns the value as of the last tick. |
|
|
93
|
+
| `dt` | `wcs-raf:tick` | Delta to the previous frame in ms. **`0` on the first frame after `start()` / `resume()` / a visibility interruption** — a value spanning an interruption never reaches observers. No upper clamp: how to treat a slow frame is your domain decision (a physics loop typically applies its own `Math.min(dt, …)`). |
|
|
94
|
+
| `running` | `wcs-raf:running-changed` | The started **intent**: `true` from `start` until `stop`/`pause`/bounded completion. Stays `true` in a hidden tab even though no frames arrive. |
|
|
95
|
+
| `suspended` | `wcs-raf:suspended-changed` | The delivery **actuality**: `true` while `running` in a hidden tab (rAF is fully stopped there — not throttled). The desired/actual split mirrors `@wcstack/wakelock`'s `active`/`held`. |
|
|
96
|
+
|
|
97
|
+
`tick` / `elapsed` / `dt` all derive from the single `wcs-raf:tick` event (`detail = { count, elapsed, dt, timestamp }`; `timestamp` is the frame's `DOMHighResTimeStamp`, `0` for the `reset()` notification). `tick` fires every frame with no equality guard; `running` / `suspended` are equality-guarded.
|
|
98
|
+
|
|
99
|
+
## Commands
|
|
100
|
+
|
|
101
|
+
| Command | Description |
|
|
102
|
+
| --------- | ----------------------------------------------------------------------- |
|
|
103
|
+
| `start` | Begin the frame loop (no-op if already running). |
|
|
104
|
+
| `stop` | Stop; `tick` / `elapsed` are retained. |
|
|
105
|
+
| `reset` | Stop and reset `tick` / `elapsed` / `dt` to `0`. |
|
|
106
|
+
| `pause` | Suspend the loop, preserving values and the bounded-run remainder. |
|
|
107
|
+
| `resume` | Continue from a `pause`; the first frame after it reports `dt = 0`. |
|
|
108
|
+
|
|
109
|
+
State-driven invocation uses the command-token protocol:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<wcs-raf manual data-wcs="command.start: $command.beginLoop"></wcs-raf>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Optional DOM Triggering
|
|
116
|
+
|
|
117
|
+
If `autoTrigger` is enabled (default), clicking an element carrying `data-raftarget="<id>"` calls `start()` on the referenced `<wcs-raf>`. A matched click calls `event.preventDefault()` — do not put `data-raftarget` on an element whose default action you also want.
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<button data-raftarget="loop">Start</button>
|
|
121
|
+
<wcs-raf id="loop" manual data-wcs="eventToken.tick: frame"></wcs-raf>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## CSS styling with `:state()`
|
|
125
|
+
|
|
126
|
+
`<wcs-raf>` reflects two boolean output states onto its
|
|
127
|
+
[`ElementInternals` `CustomStateSet`](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet):
|
|
128
|
+
|
|
129
|
+
| State | On when |
|
|
130
|
+
|-------|---------|
|
|
131
|
+
| `running` | `wcs-raf:running-changed` fires with `true` (cleared on `false`) |
|
|
132
|
+
| `suspended` | `wcs-raf:suspended-changed` fires with `true` (cleared on `false`) |
|
|
133
|
+
|
|
134
|
+
```css
|
|
135
|
+
wcs-raf:state(running) ~ .indicator { color: green; }
|
|
136
|
+
wcs-raf:state(suspended) ~ .indicator { color: orange; } /* tab hidden, loop starved */
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Browser support** (`:state(x)` syntax): Chrome/Edge 125+, Safari 17.4+, Firefox 126+. In older browsers the states are simply never set — `<wcs-raf>` itself keeps working (graceful degradation, never-throw). The `debug-states` attribute mirrors changes onto `data-wcs-state-*` attributes for DevTools inspection (debug aid only — style against `:state()`).
|
|
140
|
+
|
|
141
|
+
## Notes & limitations
|
|
142
|
+
|
|
143
|
+
- **Hidden tabs stop rAF completely** (unlike `setInterval`'s ~1Hz throttle). `running` keeps reporting the intent; `suspended` reports the reality; `elapsed` counts only active time; and the first frame after the tab becomes visible again reports `dt = 0`, so a dt-integrating consumer never sees a teleport.
|
|
144
|
+
- **No `error` surface.** rAF has no persistent failure mode; on a platform without it (SSR pre-pass, worker), `start()` is a silent no-op (never-throw).
|
|
145
|
+
- **SSR**: prefer `manual` in server-rendered markup — an auto-started loop keeps scheduling frames in DOM-emulating renderers.
|
|
146
|
+
- The platform API is resolved at call time (`globalThis.requestAnimationFrame`), and a scheduler can be injected into `RafCore` for testing.
|
|
147
|
+
|
|
148
|
+
## Headless usage (`RafCore`)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { RafCore } from "@wcstack/raf";
|
|
152
|
+
|
|
153
|
+
const core = new RafCore();
|
|
154
|
+
core.addEventListener("wcs-raf:tick", (e) => {
|
|
155
|
+
console.log((e as CustomEvent).detail); // { count, elapsed, dt, timestamp }
|
|
156
|
+
});
|
|
157
|
+
core.observe(); // subscribes visibilitychange (drives `suspended` AND the dt=0 normalization across hidden gaps)
|
|
158
|
+
core.start();
|
|
159
|
+
// later:
|
|
160
|
+
core.dispose();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
import { bootstrapRaf } from "@wcstack/raf";
|
|
167
|
+
|
|
168
|
+
bootstrapRaf({
|
|
169
|
+
autoTrigger: true, // data-raftarget click triggering (default: true)
|
|
170
|
+
triggerAttribute: "data-raftarget",
|
|
171
|
+
tagNames: { raf: "wcs-raf" },
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
package/dist/auto.js
ADDED
package/dist/auto.min.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
interface IWcBindableProperty {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly event: string;
|
|
4
|
+
readonly getter?: (event: Event) => any;
|
|
5
|
+
}
|
|
6
|
+
interface IWcBindableInput {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly attribute?: string;
|
|
9
|
+
}
|
|
10
|
+
interface IWcBindableCommand {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly async?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface IWcBindable {
|
|
15
|
+
readonly protocol: "wc-bindable";
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
readonly properties: readonly IWcBindableProperty[];
|
|
18
|
+
readonly inputs?: readonly IWcBindableInput[];
|
|
19
|
+
readonly commands?: readonly IWcBindableCommand[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ITagNames {
|
|
23
|
+
readonly raf: string;
|
|
24
|
+
}
|
|
25
|
+
interface IWritableTagNames {
|
|
26
|
+
raf?: string;
|
|
27
|
+
}
|
|
28
|
+
interface IConfig {
|
|
29
|
+
readonly autoTrigger: boolean;
|
|
30
|
+
readonly triggerAttribute: string;
|
|
31
|
+
readonly tagNames: ITagNames;
|
|
32
|
+
}
|
|
33
|
+
interface IWritableConfig {
|
|
34
|
+
autoTrigger?: boolean;
|
|
35
|
+
triggerAttribute?: string;
|
|
36
|
+
tagNames?: IWritableTagNames;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Payload carried by the `wcs-raf:tick` event.
|
|
41
|
+
* `count` is the number of frames fired since the last reset; `elapsed` is the
|
|
42
|
+
* accumulated ACTIVE milliseconds (Σdt — interruptions contribute nothing);
|
|
43
|
+
* `dt` is the delta to the previous frame within a continuous run, `0` on the
|
|
44
|
+
* first frame after start / resume / a visibility interruption; `timestamp` is
|
|
45
|
+
* the frame's `DOMHighResTimeStamp` (`0` for the reset() notification, which
|
|
46
|
+
* is not a frame).
|
|
47
|
+
*/
|
|
48
|
+
interface WcsRafTickDetail {
|
|
49
|
+
count: number;
|
|
50
|
+
elapsed: number;
|
|
51
|
+
dt: number;
|
|
52
|
+
timestamp: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Value types for RafCore (headless) — the observable state properties.
|
|
56
|
+
* Use with `bind()` from a wc-bindable binding core for compile-time type checking.
|
|
57
|
+
*/
|
|
58
|
+
interface WcsRafCoreValues {
|
|
59
|
+
tick: number;
|
|
60
|
+
elapsed: number;
|
|
61
|
+
dt: number;
|
|
62
|
+
running: boolean;
|
|
63
|
+
suspended: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Value types for the Shell (`<wcs-raf>`) — identical observable surface to
|
|
67
|
+
* the Core, plus the DOM-driven `trigger` command-property.
|
|
68
|
+
*/
|
|
69
|
+
interface WcsRafValues extends WcsRafCoreValues {
|
|
70
|
+
trigger: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface WcsRafInputs {
|
|
73
|
+
once: boolean;
|
|
74
|
+
repeat: number;
|
|
75
|
+
manual: boolean;
|
|
76
|
+
trigger: boolean;
|
|
77
|
+
}
|
|
78
|
+
interface WcsRafCoreCommands {
|
|
79
|
+
start(options?: {
|
|
80
|
+
repeat?: number;
|
|
81
|
+
}): void;
|
|
82
|
+
stop(): void;
|
|
83
|
+
reset(): void;
|
|
84
|
+
pause(): void;
|
|
85
|
+
resume(): void;
|
|
86
|
+
}
|
|
87
|
+
interface WcsRafCommands {
|
|
88
|
+
start(): void;
|
|
89
|
+
stop(): void;
|
|
90
|
+
reset(): void;
|
|
91
|
+
pause(): void;
|
|
92
|
+
resume(): void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare function bootstrapRaf(userConfig?: IWritableConfig): void;
|
|
96
|
+
|
|
97
|
+
declare function getConfig(): IConfig;
|
|
98
|
+
|
|
99
|
+
interface RafStartOptions {
|
|
100
|
+
repeat?: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Injectable frame scheduler. The default resolves
|
|
104
|
+
* `globalThis.requestAnimationFrame` / `cancelAnimationFrame` AT CALL TIME
|
|
105
|
+
* (async-io-node-guidelines §3.7); tests inject a fake that pumps frames with
|
|
106
|
+
* explicit timestamps (the `dt` contract is timestamp-derived, so tests must
|
|
107
|
+
* control the clock, not just the callback order).
|
|
108
|
+
*
|
|
109
|
+
* Contract: `request()` MUST return a non-null handle. The core uses `null`
|
|
110
|
+
* as its internal "not armed" sentinel, so a scheduler returning literal
|
|
111
|
+
* `null` would silently corrupt the handle bookkeeping (re-entrancy guards
|
|
112
|
+
* and cancel tracking). Native rAF returns a long, so this only concerns
|
|
113
|
+
* custom scheduler injections — return a number, object, or any other
|
|
114
|
+
* non-nullish token.
|
|
115
|
+
*/
|
|
116
|
+
interface RafScheduler {
|
|
117
|
+
request(callback: (timestamp: number) => void): unknown;
|
|
118
|
+
cancel(handle: unknown): void;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Headless requestAnimationFrame primitive — `TimerCore`'s sibling with the
|
|
122
|
+
* time source swapped from `setInterval` (a period) to rAF (the browser's
|
|
123
|
+
* rendering opportunity). Exposed through the wc-bindable protocol: it streams
|
|
124
|
+
* `tick` (frame counter), `elapsed` (accumulated ACTIVE milliseconds), `dt`
|
|
125
|
+
* (delta to the previous frame) and the `running` / `suspended` pair, and is
|
|
126
|
+
* driven by the `start` / `stop` / `reset` / `pause` / `resume` commands.
|
|
127
|
+
*
|
|
128
|
+
* `tick` / `elapsed` / `dt` are all surfaced via the single `wcs-raf:tick`
|
|
129
|
+
* event (read through getters, mirroring how FetchCore exposes value/status
|
|
130
|
+
* from one `wcs-fetch:response` event).
|
|
131
|
+
*
|
|
132
|
+
* Contracts specific to this node (docs/raf-tag-design.md):
|
|
133
|
+
*
|
|
134
|
+
* - **dt describes continuous running only.** The first frame after `start()`,
|
|
135
|
+
* `resume()`, or a visibility interruption reports `dt = 0` — a value that
|
|
136
|
+
* spans an interruption never reaches observers. Like `suspended`, the
|
|
137
|
+
* visibility boundary is only detected once observe() has subscribed to
|
|
138
|
+
* `visibilitychange`; a headless setup that skips observe() will see the
|
|
139
|
+
* raw spanning delta on the first frame after a hidden gap. There is
|
|
140
|
+
* deliberately NO upper clamp: how to treat a slow frame is the consumer's
|
|
141
|
+
* domain decision.
|
|
142
|
+
* - **elapsed is Σdt (active time).** Because interruption-spanning deltas are
|
|
143
|
+
* normalized to 0, summing dt yields exactly the time frames were actually
|
|
144
|
+
* being delivered — no separate segment bookkeeping is needed, and hidden /
|
|
145
|
+
* paused periods contribute nothing. Granularity is one frame: between
|
|
146
|
+
* frames the getter returns the value as of the last tick.
|
|
147
|
+
* - **running / suspended are a desired/actual pair** (the wakelock split): in
|
|
148
|
+
* a hidden tab the browser delivers no frames at all, so `running` (the
|
|
149
|
+
* started intent) stays true while `suspended` reports that delivery is
|
|
150
|
+
* actually stopped. `suspended` is only meaningful after `observe()` has
|
|
151
|
+
* subscribed to `visibilitychange`; without a document it stays false.
|
|
152
|
+
* - **No `error` surface.** rAF has no persistent failure mode; on a platform
|
|
153
|
+
* without it, `start()` is a silent no-op (never-throw, resize precedent).
|
|
154
|
+
*/
|
|
155
|
+
declare class RafCore extends EventTarget {
|
|
156
|
+
static wcBindable: IWcBindable;
|
|
157
|
+
private _target;
|
|
158
|
+
private _injectedScheduler;
|
|
159
|
+
private _handle;
|
|
160
|
+
private _globalScheduler;
|
|
161
|
+
private _gen;
|
|
162
|
+
private _ready;
|
|
163
|
+
private _tick;
|
|
164
|
+
private _dt;
|
|
165
|
+
private _elapsed;
|
|
166
|
+
private _running;
|
|
167
|
+
private _suspended;
|
|
168
|
+
private _paused;
|
|
169
|
+
private _lastTs;
|
|
170
|
+
private _repeat;
|
|
171
|
+
private _runStartTick;
|
|
172
|
+
private _visibilityDoc;
|
|
173
|
+
constructor(target?: EventTarget, scheduler?: RafScheduler);
|
|
174
|
+
get tick(): number;
|
|
175
|
+
get elapsed(): number;
|
|
176
|
+
get dt(): number;
|
|
177
|
+
get running(): boolean;
|
|
178
|
+
get suspended(): boolean;
|
|
179
|
+
get ready(): Promise<void>;
|
|
180
|
+
observe(): Promise<void>;
|
|
181
|
+
dispose(): void;
|
|
182
|
+
private _dispatchTick;
|
|
183
|
+
private _setRunning;
|
|
184
|
+
private _setSuspended;
|
|
185
|
+
private _updateSuspended;
|
|
186
|
+
start(options?: RafStartOptions): void;
|
|
187
|
+
stop(): void;
|
|
188
|
+
reset(): void;
|
|
189
|
+
pause(): void;
|
|
190
|
+
resume(): void;
|
|
191
|
+
private _frame;
|
|
192
|
+
private _onVisibilityChange;
|
|
193
|
+
private _resolveScheduler;
|
|
194
|
+
private _requestFrame;
|
|
195
|
+
private _clearHandle;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
declare class Raf extends HTMLElement {
|
|
199
|
+
static hasConnectedCallbackPromise: boolean;
|
|
200
|
+
static wcBindable: IWcBindable;
|
|
201
|
+
private _core;
|
|
202
|
+
private _trigger;
|
|
203
|
+
private _connectedCallbackPromise;
|
|
204
|
+
private _internals;
|
|
205
|
+
constructor();
|
|
206
|
+
get debugStates(): string[];
|
|
207
|
+
private _initInternals;
|
|
208
|
+
private _wireStates;
|
|
209
|
+
get connectedCallbackPromise(): Promise<void>;
|
|
210
|
+
get once(): boolean;
|
|
211
|
+
set once(value: boolean);
|
|
212
|
+
get repeat(): number;
|
|
213
|
+
set repeat(value: number);
|
|
214
|
+
get manual(): boolean;
|
|
215
|
+
set manual(value: boolean);
|
|
216
|
+
get tick(): number;
|
|
217
|
+
get elapsed(): number;
|
|
218
|
+
get dt(): number;
|
|
219
|
+
get running(): boolean;
|
|
220
|
+
get suspended(): boolean;
|
|
221
|
+
get trigger(): boolean;
|
|
222
|
+
set trigger(value: boolean);
|
|
223
|
+
start(): void;
|
|
224
|
+
stop(): void;
|
|
225
|
+
reset(): void;
|
|
226
|
+
pause(): void;
|
|
227
|
+
resume(): void;
|
|
228
|
+
connectedCallback(): void;
|
|
229
|
+
disconnectedCallback(): void;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export { RafCore, Raf as WcsRaf, bootstrapRaf, getConfig };
|
|
233
|
+
export type { IWritableConfig, IWritableTagNames, RafScheduler, RafStartOptions, WcsRafCommands, WcsRafCoreCommands, WcsRafCoreValues, WcsRafInputs, WcsRafTickDetail, WcsRafValues };
|