@weasel-js/react 1.10.0 → 1.10.1
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.md +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -14
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ a value into a ref prop of either shape. No weasel domain in it.
|
|
|
7
7
|
|
|
8
8
|
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
9
9
|
scene-graph canvas kit for React. See the
|
|
10
|
-
[API reference](https://orochi235.github.io/weasel/
|
|
10
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/react).
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
package/dist/index.d.ts
CHANGED
|
@@ -108,11 +108,11 @@ interface VisibleRaf {
|
|
|
108
108
|
isVisible(): boolean;
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
* @param frame Runs on the frame, with the timestamp the clock supplied
|
|
112
|
-
* A continuous loop calls `request()`
|
|
113
|
-
* does not.
|
|
111
|
+
* @param frame Runs on the frame, with the timestamp the clock supplied and the
|
|
112
|
+
* loop's own handle. A continuous loop calls `loop.request()` from inside it;
|
|
113
|
+
* a one-shot loop does not.
|
|
114
114
|
*/
|
|
115
|
-
declare function useVisibleRaf(frame: (time: number) => void, options?: VisibleRafOptions): VisibleRaf;
|
|
115
|
+
declare function useVisibleRaf(frame: (time: number, loop: VisibleRaf) => void, options?: VisibleRafOptions): VisibleRaf;
|
|
116
116
|
|
|
117
117
|
/** Options for {@link useFrameClock}. */
|
|
118
118
|
interface FrameClockOptions {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useMemo, useInsertionEffect, useLayoutEffect, useEffect } from 'react';
|
|
2
|
+
import { createEmitter } from '@weasel-js/registry';
|
|
2
3
|
|
|
3
4
|
// src/assignRef.ts
|
|
4
5
|
function assignRef(ref, value) {
|
|
@@ -54,6 +55,7 @@ function useVisibleRaf(frame, options = {}) {
|
|
|
54
55
|
const intersectingRef = useRef(true);
|
|
55
56
|
const observerRef = useRef(null);
|
|
56
57
|
const observedRef = useRef(null);
|
|
58
|
+
const handleRef = useRef(null);
|
|
57
59
|
const isVisible = useCallback(
|
|
58
60
|
() => optsRef.current.dangerouslyRunWhenHidden === true || !documentHidden() && intersectingRef.current,
|
|
59
61
|
[optsRef]
|
|
@@ -69,7 +71,7 @@ function useVisibleRaf(frame, options = {}) {
|
|
|
69
71
|
rafRef.current = null;
|
|
70
72
|
if (!aliveRef.current || !pendingRef.current || !isVisible()) return;
|
|
71
73
|
pendingRef.current = false;
|
|
72
|
-
frameRef.current(time);
|
|
74
|
+
frameRef.current(time, handleRef.current);
|
|
73
75
|
});
|
|
74
76
|
}, [frameRef, isVisible, optsRef]);
|
|
75
77
|
const syncObserver = useCallback(() => {
|
|
@@ -131,20 +133,24 @@ function useVisibleRaf(frame, options = {}) {
|
|
|
131
133
|
suspendedRef.current = false;
|
|
132
134
|
};
|
|
133
135
|
}, [cancelRaf, syncGate, syncObserver]);
|
|
134
|
-
|
|
136
|
+
const handle = useMemo(() => ({ request, cancel, isVisible }), [request, cancel, isVisible]);
|
|
137
|
+
useLayoutEffect(() => {
|
|
138
|
+
handleRef.current = handle;
|
|
139
|
+
}, [handle]);
|
|
140
|
+
return handle;
|
|
135
141
|
}
|
|
136
142
|
function useFrameClock(options = {}) {
|
|
137
143
|
const { paused = false, requestFrame, cancelFrame } = options;
|
|
138
144
|
const elapsed = useRef(options.start ?? 0);
|
|
139
145
|
const last = useRef(null);
|
|
140
|
-
const
|
|
146
|
+
const [ticked] = useState(() => createEmitter());
|
|
141
147
|
const pausedRef = useLatest(paused);
|
|
142
148
|
const loop = useVisibleRaf(
|
|
143
|
-
(now) => {
|
|
149
|
+
(now, loop2) => {
|
|
144
150
|
if (last.current !== null) elapsed.current += (now - last.current) / 1e3;
|
|
145
151
|
last.current = now;
|
|
146
|
-
|
|
147
|
-
if (
|
|
152
|
+
ticked.emit();
|
|
153
|
+
if (ticked.size > 0 && !pausedRef.current) loop2.request();
|
|
148
154
|
},
|
|
149
155
|
{ requestFrame, cancelFrame, onResume: () => {
|
|
150
156
|
last.current = null;
|
|
@@ -153,22 +159,21 @@ function useFrameClock(options = {}) {
|
|
|
153
159
|
useEffect(() => {
|
|
154
160
|
if (paused) return;
|
|
155
161
|
last.current = null;
|
|
156
|
-
if (
|
|
162
|
+
if (ticked.size > 0) loop.request();
|
|
157
163
|
return () => loop.cancel();
|
|
158
|
-
}, [paused, loop]);
|
|
164
|
+
}, [paused, loop, ticked]);
|
|
159
165
|
return useMemo(() => ({
|
|
160
166
|
seconds: () => elapsed.current,
|
|
161
167
|
subscribe(listener) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
set.add(listener);
|
|
168
|
+
if (ticked.size === 0) last.current = null;
|
|
169
|
+
const off = ticked.subscribe(listener);
|
|
165
170
|
if (!pausedRef.current) loop.request();
|
|
166
171
|
return () => {
|
|
167
|
-
|
|
168
|
-
if (
|
|
172
|
+
off();
|
|
173
|
+
if (ticked.size === 0) loop.cancel();
|
|
169
174
|
};
|
|
170
175
|
}
|
|
171
|
-
}), [loop, pausedRef]);
|
|
176
|
+
}), [loop, pausedRef, ticked]);
|
|
172
177
|
}
|
|
173
178
|
function useStableByContent(value, same) {
|
|
174
179
|
const held = useRef(value);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assignRef.ts","../src/useCanvasFocus.ts","../src/useLatest.ts","../src/useVisibleRaf.ts","../src/useFrameClock.ts","../src/useStableByContent.ts"],"names":["useRef","useCallback","useMemo","useInsertionEffect"],"mappings":";;;AASO,SAAS,SAAA,CAAa,KAAyB,KAAA,EAAuB;AAC3E,EAAA,IAAI,OAAO,GAAA,KAAQ,UAAA,EAAY,GAAA,CAAI,KAAK,CAAA;AAAA,OAAA,IAC/B,GAAA,EAAM,GAAA,CAA8B,OAAA,GAAU,KAAA;AACzD;ACgCO,SAAS,cAAA,CAAe,OAAA,GAAiC,EAAC,EAAsB;AACrF,EAAA,MAAM,EAAE,OAAA,GAAU,KAAA,EAAO,QAAA,GAAW,GAAE,GAAI,OAAA;AAC1C,EAAA,MAAM,CAAC,OAAA,EAAS,eAAe,CAAA,GAAI,SAAkB,OAAO,CAAA;AAE5D,EAAA,MAAM,UAAA,GAAa,OAAgB,OAAO,CAAA;AAE1C,EAAA,MAAM,OAAA,GAAU,YAAY,MAAM;AAChC,IAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AACvB,MAAA,UAAA,CAAW,OAAA,GAAU,IAAA;AACrB,MAAA,eAAA,CAAgB,IAAI,CAAA;AAAA,IACtB;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AACL,EAAA,MAAM,MAAA,GAAS,YAAY,MAAM;AAC/B,IAAA,IAAI,WAAW,OAAA,EAAS;AACtB,MAAA,UAAA,CAAW,OAAA,GAAU,KAAA;AACrB,MAAA,eAAA,CAAgB,KAAK,CAAA;AAAA,IACvB;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AACL,EAAA,MAAM,aAAa,WAAA,CAAY,MAAM,UAAA,CAAW,OAAA,EAAS,EAAE,CAAA;AAC3D,EAAA,MAAM,UAAA,GAAa,WAAA,CAAY,CAAC,IAAA,KAAkB;AAChD,IAAA,IAAI,UAAA,CAAW,YAAY,IAAA,EAAM;AACjC,IAAA,UAAA,CAAW,OAAA,GAAU,IAAA;AACrB,IAAA,eAAA,CAAgB,IAAI,CAAA;AAAA,EACtB,CAAA,EAAG,EAAE,CAAA;AAIL,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAO,EAAE,QAAA,EAAU,OAAA,EAAS,MAAA,EAAO,CAAA,EAAI,CAAC,QAAA,EAAU,OAAA,EAAS,MAAM,CAAC,CAAA;AAE7F,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IACA,UAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AACF;AC7DO,SAAS,UAAa,KAAA,EAAmC;AAC9D,EAAA,MAAM,GAAA,GAAMA,OAAO,KAAK,CAAA;AACxB,EAAA,kBAAA,CAAmB,MAAM;AACvB,IAAA,GAAA,CAAI,OAAA,GAAU,KAAA;AAAA,EAChB,CAAC,CAAA;AACD,EAAA,OAAO,GAAA;AACT;AC2BA,IAAM,cAAA,GAAiB,MAAe,OAAO,QAAA,KAAa,eAAe,QAAA,CAAS,MAAA;AAElF,IAAM,aAAA,GAAgB,CAAC,MAAA,KACrB,OAAO,WAAW,UAAA,GAAa,MAAA,EAAO,GAAK,MAAA,EAAQ,OAAA,IAAW,IAAA;AAOzD,SAAS,aAAA,CACd,KAAA,EACA,OAAA,GAA6B,EAAC,EAClB;AACZ,EAAA,MAAM,QAAA,GAAW,UAAU,KAAK,CAAA;AAChC,EAAA,MAAM,OAAA,GAAU,UAAU,OAAO,CAAA;AAEjC,EAAA,MAAM,MAAA,GAASA,OAAsB,IAAI,CAAA;AACzC,EAAA,MAAM,UAAA,GAAaA,OAAO,KAAK,CAAA;AAC/B,EAAA,MAAM,YAAA,GAAeA,OAAO,KAAK,CAAA;AACjC,EAAA,MAAM,QAAA,GAAWA,OAAO,IAAI,CAAA;AAC5B,EAAA,MAAM,eAAA,GAAkBA,OAAO,IAAI,CAAA;AACnC,EAAA,MAAM,WAAA,GAAcA,OAAoC,IAAI,CAAA;AAC5D,EAAA,MAAM,WAAA,GAAcA,OAAuB,IAAI,CAAA;AAE/C,EAAA,MAAM,SAAA,GAAYC,WAAAA;AAAA,IAChB,MACE,QAAQ,OAAA,CAAQ,wBAAA,KAA6B,QAC5C,CAAC,cAAA,MAAoB,eAAA,CAAgB,OAAA;AAAA,IACxC,CAAC,OAAO;AAAA,GACV;AAEA,EAAA,MAAM,SAAA,GAAYA,YAAY,MAAM;AAClC,IAAA,IAAI,MAAA,CAAO,YAAY,IAAA,EAAM;AAC7B,IAAA,CAAC,OAAA,CAAQ,OAAA,CAAQ,WAAA,IAAe,oBAAA,EAAsB,OAAO,OAAO,CAAA;AACpE,IAAA,MAAA,CAAO,OAAA,GAAU,IAAA;AAAA,EACnB,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,MAAM,WAAA,GAAcA,YAAY,MAAM;AACpC,IAAA,IAAI,MAAA,CAAO,OAAA,KAAY,IAAA,IAAQ,CAAC,SAAS,OAAA,EAAS;AAClD,IAAA,MAAA,CAAO,WAAW,OAAA,CAAQ,OAAA,CAAQ,YAAA,IAAgB,qBAAA,EAAuB,CAAC,IAAA,KAAS;AAGjF,MAAA,MAAA,CAAO,OAAA,GAAU,IAAA;AACjB,MAAA,IAAI,CAAC,SAAS,OAAA,IAAW,CAAC,WAAW,OAAA,IAAW,CAAC,WAAU,EAAG;AAC9D,MAAA,UAAA,CAAW,OAAA,GAAU,KAAA;AACrB,MAAA,QAAA,CAAS,QAAQ,IAAI,CAAA;AAAA,IACvB,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,QAAA,EAAU,SAAA,EAAW,OAAO,CAAC,CAAA;AAIjC,EAAA,MAAM,YAAA,GAAeA,YAAY,MAAM;AACrC,IAAA,MAAM,KAAK,WAAA,CAAY,OAAA;AACvB,IAAA,IAAI,CAAC,EAAA,EAAI;AACT,IAAA,MAAM,EAAA,GAAK,aAAA,CAAc,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAA;AAC/C,IAAA,IAAI,EAAA,KAAO,YAAY,OAAA,EAAS;AAChC,IAAA,IAAI,WAAA,CAAY,OAAA,EAAS,EAAA,CAAG,SAAA,CAAU,YAAY,OAAO,CAAA;AACzD,IAAA,WAAA,CAAY,OAAA,GAAU,EAAA;AAEtB,IAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,IAAA,IAAI,EAAA,EAAI,EAAA,CAAG,OAAA,CAAQ,EAAE,CAAA;AAAA,EACvB,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAKZ,EAAA,MAAM,QAAA,GAAWA,YAAY,MAAM;AACjC,IAAA,IAAI,CAAC,WAAU,EAAG;AAChB,MAAA,IAAI,aAAa,OAAA,EAAS;AAC1B,MAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AACvB,MAAA,SAAA,EAAU;AACV,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,aAAa,OAAA,EAAS;AAC3B,IAAA,YAAA,CAAa,OAAA,GAAU,KAAA;AACvB,IAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AACvB,IAAA,OAAA,CAAQ,QAAQ,QAAA,IAAW;AAC3B,IAAA,IAAI,UAAA,CAAW,SAAS,WAAA,EAAY;AAAA,EACtC,GAAG,CAAC,SAAA,EAAW,SAAA,EAAW,OAAA,EAAS,WAAW,CAAC,CAAA;AAE/C,EAAA,MAAM,OAAA,GAAUA,YAAY,MAAM;AAChC,IAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AACvB,IAAA,YAAA,EAAa;AACb,IAAA,UAAA,CAAW,OAAA,GAAU,IAAA;AACrB,IAAA,IAAI,CAAC,WAAU,EAAG;AAChB,MAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AACvB,MAAA;AAAA,IACF;AACA,IAAA,WAAA,EAAY;AAAA,EACd,CAAA,EAAG,CAAC,SAAA,EAAW,WAAA,EAAa,YAAY,CAAC,CAAA;AAEzC,EAAA,MAAM,MAAA,GAASA,YAAY,MAAM;AAC/B,IAAA,UAAA,CAAW,OAAA,GAAU,KAAA;AACrB,IAAA,SAAA,EAAU;AAAA,EACZ,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAKd,EAAA,eAAA,CAAgB,MAAM;AACpB,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,IAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,QAAQ,CAAA;AACtD,IAAA,IAAI,OAAO,yBAAyB,WAAA,EAAa;AAC/C,MAAA,WAAA,CAAY,OAAA,GAAU,IAAI,oBAAA,CAAqB,CAAC,OAAA,KAAY;AAC1D,QAAA,eAAA,CAAgB,UAAU,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA,KAAM,EAAE,cAAc,CAAA;AAC9D,QAAA,QAAA,EAAS;AAAA,MACX,CAAC,CAAA;AACD,MAAA,YAAA,EAAa;AAAA,IACf;AACA,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,OAAA,GAAU,KAAA;AACnB,MAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,QAAQ,CAAA;AACzD,MAAA,WAAA,CAAY,SAAS,UAAA,EAAW;AAChC,MAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AACtB,MAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AACtB,MAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,MAAA,SAAA,EAAU;AACV,MAAA,UAAA,CAAW,OAAA,GAAU,KAAA;AACrB,MAAA,YAAA,CAAa,OAAA,GAAU,KAAA;AAAA,IACzB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,SAAA,EAAW,QAAA,EAAU,YAAY,CAAC,CAAA;AAItC,EAAA,OAAOC,OAAAA,CAAQ,OAAO,EAAE,OAAA,EAAS,MAAA,EAAQ,SAAA,EAAU,CAAA,EAAI,CAAC,OAAA,EAAS,MAAA,EAAQ,SAAS,CAAC,CAAA;AACrF;ACpJO,SAAS,aAAA,CAAc,OAAA,GAA6B,EAAC,EAAe;AACzE,EAAA,MAAM,EAAE,MAAA,GAAS,KAAA,EAAO,YAAA,EAAc,aAAY,GAAI,OAAA;AACtD,EAAA,MAAM,OAAA,GAAUF,MAAAA,CAAO,OAAA,CAAQ,KAAA,IAAS,CAAC,CAAA;AACzC,EAAA,MAAM,IAAA,GAAOA,OAAsB,IAAI,CAAA;AACvC,EAAA,MAAM,SAAA,GAAYA,MAAAA,iBAAO,IAAI,GAAA,EAAiB,CAAA;AAC9C,EAAA,MAAM,SAAA,GAAY,UAAU,MAAM,CAAA;AAElC,EAAA,MAAM,IAAA,GAAO,aAAA;AAAA,IACX,CAAC,GAAA,KAAQ;AACP,MAAA,IAAI,KAAK,OAAA,KAAY,IAAA,UAAc,OAAA,IAAA,CAAY,GAAA,GAAM,KAAK,OAAA,IAAW,GAAA;AACrE,MAAA,IAAA,CAAK,OAAA,GAAU,GAAA;AACf,MAAA,KAAA,MAAW,YAAY,CAAC,GAAG,SAAA,CAAU,OAAO,GAAG,QAAA,EAAS;AACxD,MAAA,IAAI,SAAA,CAAU,QAAQ,IAAA,GAAO,CAAA,IAAK,CAAC,SAAA,CAAU,OAAA,OAAc,OAAA,EAAQ;AAAA,IACrE,CAAA;AAAA,IACA,EAAE,YAAA,EAAc,WAAA,EAAa,QAAA,EAAU,MAAM;AAAE,MAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,IAAM,CAAA;AAAE,GACxE;AAEA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,MAAA,EAAQ;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,IAAI,SAAA,CAAU,OAAA,CAAQ,IAAA,GAAO,CAAA,OAAQ,OAAA,EAAQ;AAC7C,IAAA,OAAO,MAAM,KAAK,MAAA,EAAO;AAAA,EAC3B,CAAA,EAAG,CAAC,MAAA,EAAQ,IAAI,CAAC,CAAA;AAEjB,EAAA,OAAOE,QAAoB,OAAO;AAAA,IAChC,OAAA,EAAS,MAAM,OAAA,CAAQ,OAAA;AAAA,IACvB,UAAU,QAAA,EAAU;AAClB,MAAA,MAAM,MAAM,SAAA,CAAU,OAAA;AACtB,MAAA,IAAI,GAAA,CAAI,IAAA,KAAS,CAAA,EAAG,IAAA,CAAK,OAAA,GAAU,IAAA;AACnC,MAAA,GAAA,CAAI,IAAI,QAAQ,CAAA;AAChB,MAAA,IAAI,CAAC,SAAA,CAAU,OAAA,EAAS,IAAA,CAAK,OAAA,EAAQ;AACrC,MAAA,OAAO,MAAM;AACX,QAAA,GAAA,CAAI,OAAO,QAAQ,CAAA;AACnB,QAAA,IAAI,GAAA,CAAI,IAAA,KAAS,CAAA,EAAG,IAAA,CAAK,MAAA,EAAO;AAAA,MAClC,CAAA;AAAA,IACF;AAAA,GACF,CAAA,EAAI,CAAC,IAAA,EAAM,SAAS,CAAC,CAAA;AACvB;AC1DO,SAAS,kBAAA,CAAsB,OAAU,IAAA,EAAkC;AAChF,EAAA,MAAM,IAAA,GAAOF,OAAO,KAAK,CAAA;AACzB,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,KAAY,KAAA,IAAS,IAAA,CAAK,KAAK,OAAA,EAAS,KAAK,CAAA,GAAI,IAAA,CAAK,OAAA,GAAU,KAAA;AACjF,EAAAG,mBAAmB,MAAM;AACvB,IAAA,IAAA,CAAK,OAAA,GAAU,GAAA;AAAA,EACjB,CAAC,CAAA;AACD,EAAA,OAAO,GAAA;AACT;AAGO,SAAS,QAAA,CAAY,GAAiB,CAAA,EAA0B;AACrE,EAAA,OAAO,CAAA,CAAE,MAAA,KAAW,CAAA,CAAE,MAAA,IAAU,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,KAAM,CAAA,CAAE,CAAC,CAAC,CAAA;AAC9D","file":"index.js","sourcesContent":["import type { Ref } from 'react';\n\n/**\n * Write `value` into a ref a caller passed in, whichever shape it has: call a\n * callback ref, set an object ref's `current`, ignore `null` or `undefined`.\n *\n * For a component that keeps the element (or handle) for itself and also\n * forwards it to a ref prop — call this from the component's own callback ref.\n */\nexport function assignRef<T>(ref: Ref<T> | undefined, value: T | null): void {\n if (typeof ref === 'function') ref(value);\n else if (ref) (ref as { current: T | null }).current = value;\n}\n","/**\n * Track DOM focus on a canvas (or any focusable host element). Returns the\n * live `focused` boolean, props to spread onto the focusable element, and a\n * stable `getFocused()` getter for use inside per-frame closures (notably\n * `RenderLayer.draw`, which is constructed once but called every frame).\n *\n * No Canvas/SceneCanvas integration. Consumers compose the hook with core's\n * `gateLayer` (or their own gate) to hide affordances when blurred.\n */\nimport { useCallback, useMemo, useRef, useState } from 'react';\n\n/** Options for `useCanvasFocus`. */\nexport interface UseCanvasFocusOptions {\n /** Initial focus state. Default `false` — handles hidden until first click. */\n initial?: boolean;\n /** `tabIndex` to set on the focusable element. Default `0` (focusable in\n * document tab order). Set to `-1` for programmatic-only focus. */\n tabIndex?: number;\n}\n\n/** What `useCanvasFocus` hands back: the focus state, the props that\n * maintain it, and a getter for reading it from per-frame code. */\nexport interface CanvasFocusReturn {\n /** Live boolean — true when the element has DOM focus. Triggers re-renders\n * when the focus state changes (set via React state). */\n focused: boolean;\n /** Spread onto the focusable element (e.g. the canvas). Includes\n * `tabIndex`, `onFocus`, `onBlur`. */\n focusProps: {\n tabIndex: number;\n onFocus: () => void;\n onBlur: () => void;\n };\n /** Stable getter for the live focused state. Use inside per-frame closures\n * like `RenderLayer.draw`, where the layer is constructed once and\n * reading `focused` directly would capture the value at construction\n * time. The getter always returns the current value. */\n getFocused: () => boolean;\n /** Imperative focus / blur. Useful for tests and programmatic control. */\n setFocused: (next: boolean) => void;\n}\n\n/** Track DOM focus on the canvas, so chrome that should only show while the\n * canvas is focused can be gated on it. */\nexport function useCanvasFocus(options: UseCanvasFocusOptions = {}): CanvasFocusReturn {\n const { initial = false, tabIndex = 0 } = options;\n const [focused, setFocusedState] = useState<boolean>(initial);\n // Every state change goes through a handler below that writes this first.\n const focusedRef = useRef<boolean>(initial);\n\n const onFocus = useCallback(() => {\n if (!focusedRef.current) {\n focusedRef.current = true;\n setFocusedState(true);\n }\n }, []);\n const onBlur = useCallback(() => {\n if (focusedRef.current) {\n focusedRef.current = false;\n setFocusedState(false);\n }\n }, []);\n const getFocused = useCallback(() => focusedRef.current, []);\n const setFocused = useCallback((next: boolean) => {\n if (focusedRef.current === next) return;\n focusedRef.current = next;\n setFocusedState(next);\n }, []);\n\n // Stable until tabIndex changes, so it can be spread onto memoized elements\n // without retriggering downstream effects.\n const focusProps = useMemo(() => ({ tabIndex, onFocus, onBlur }), [tabIndex, onFocus, onBlur]);\n\n return {\n focused,\n focusProps,\n getFocused,\n setFocused,\n };\n}\n","import { useInsertionEffect, useRef } from 'react';\n\n/**\n * A ref holding the `value` of the last **committed** render, for event\n * handlers, frame loops and other code that runs outside render and must not\n * re-subscribe every time `value` changes.\n *\n * Writing `ref.current = value` in a render body is not the same thing: React\n * may start a render and throw it away (a transition that suspends, a render\n * interrupted by a more urgent update), and the value that render computed\n * would stay behind for the next event to read. This writes from\n * `useInsertionEffect`, so only a render that commits ever lands, and it lands\n * before any layout effect in that commit runs.\n *\n * Don't read `.current` during render expecting this render's value: until the\n * commit it holds the previous committed one (on mount, this first `value`).\n * Render code should use `value` itself.\n */\nexport function useLatest<T>(value: T): { readonly current: T } {\n const ref = useRef(value);\n useInsertionEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n","/**\n * The one place weasel decides whether a frame may run, so that \"a loop does no\n * work nobody can see\" holds by construction rather than by every loop author\n * remembering. Nothing runs while the document is hidden; a loop that names an\n * element also stops while that element is outside the viewport.\n *\n * A request made while suspended is held, not dropped, and re-armed when the\n * surface comes back — so a loop never needs to poll visibility itself.\n */\n\nimport { type RefObject, useCallback, useLayoutEffect, useMemo, useRef } from 'react';\nimport { useLatest } from './useLatest';\n\n/** What a loop names as its element, resolved every time the gate is consulted\n * so a ref filled in after mount still starts the observer. */\nexport type VisibleRafTarget = RefObject<Element | null> | (() => Element | null);\n\n/** Options for {@link useVisibleRaf}. */\nexport interface VisibleRafOptions {\n /** Also gate on this element's intersection with the viewport. Omit it and\n * the loop is gated on document visibility alone. */\n target?: VisibleRafTarget;\n /**\n * Run frames regardless of visibility. The escape hatch for a loop that is\n * not painting for a viewer — an offscreen recording, an export driving its\n * own frames. It is the wrong fix for a loop that stalls after a tab switch:\n * that is a missing `request()`, and setting this hides it.\n */\n dangerouslyRunWhenHidden?: boolean;\n /** Frame clock. Read live, so a test may inject one after mount. */\n requestFrame?: (cb: FrameRequestCallback) => number;\n cancelFrame?: (handle: number) => void;\n /**\n * Runs once when the loop leaves suspension, before the frame that follows\n * it. Where a loop that measures elapsed time drops the interval it spent\n * suspended — without it, an hour hidden arrives as one hour-long frame.\n */\n onResume?: () => void;\n}\n\n/** The handle {@link useVisibleRaf} returns. Nothing runs until `request()` is called. */\nexport interface VisibleRaf {\n /** Ask for a frame. Idempotent while one is outstanding, and held rather than\n * dropped while suspended. Identity is stable for the component's lifetime. */\n request(): void;\n /** Drop the outstanding request, held or scheduled. */\n cancel(): void;\n /** Whether a frame would run right now. */\n isVisible(): boolean;\n}\n\nconst documentHidden = (): boolean => typeof document !== 'undefined' && document.hidden;\n\nconst resolveTarget = (target: VisibleRafTarget | undefined): Element | null =>\n typeof target === 'function' ? target() : (target?.current ?? null);\n\n/**\n * @param frame Runs on the frame, with the timestamp the clock supplied.\n * A continuous loop calls `request()` again from inside it; a one-shot loop\n * does not.\n */\nexport function useVisibleRaf(\n frame: (time: number) => void,\n options: VisibleRafOptions = {},\n): VisibleRaf {\n const frameRef = useLatest(frame);\n const optsRef = useLatest(options);\n\n const rafRef = useRef<number | null>(null);\n const pendingRef = useRef(false);\n const suspendedRef = useRef(false);\n const aliveRef = useRef(true);\n const intersectingRef = useRef(true);\n const observerRef = useRef<IntersectionObserver | null>(null);\n const observedRef = useRef<Element | null>(null);\n\n const isVisible = useCallback(\n (): boolean =>\n optsRef.current.dangerouslyRunWhenHidden === true ||\n (!documentHidden() && intersectingRef.current),\n [optsRef],\n );\n\n const cancelRaf = useCallback(() => {\n if (rafRef.current === null) return;\n (optsRef.current.cancelFrame ?? cancelAnimationFrame)(rafRef.current);\n rafRef.current = null;\n }, [optsRef]);\n\n const scheduleRaf = useCallback(() => {\n if (rafRef.current !== null || !aliveRef.current) return;\n rafRef.current = (optsRef.current.requestFrame ?? requestAnimationFrame)((time) => {\n // Released before the frame runs, so a `request()` issued from inside it\n // schedules the next one instead of being swallowed.\n rafRef.current = null;\n if (!aliveRef.current || !pendingRef.current || !isVisible()) return;\n pendingRef.current = false;\n frameRef.current(time);\n });\n }, [frameRef, isVisible, optsRef]);\n\n /** Attaches the observer once the named element exists. Ref callbacks and\n * lazily-mounted hosts fill their refs after this hook's effect has run. */\n const syncObserver = useCallback(() => {\n const io = observerRef.current;\n if (!io) return;\n const el = resolveTarget(optsRef.current.target);\n if (el === observedRef.current) return;\n if (observedRef.current) io.unobserve(observedRef.current);\n observedRef.current = el;\n // An element that has gone away stops gating rather than stopping the loop.\n intersectingRef.current = true;\n if (el) io.observe(el);\n }, [optsRef]);\n\n /** Re-reads the gate after something that could have changed it. Suspending\n * drops the outstanding frame but keeps the request; resuming hands the\n * loop its rebase before re-arming. */\n const syncGate = useCallback(() => {\n if (!isVisible()) {\n if (suspendedRef.current) return;\n suspendedRef.current = true;\n cancelRaf();\n return;\n }\n if (!suspendedRef.current) return;\n suspendedRef.current = false;\n if (!aliveRef.current) return;\n optsRef.current.onResume?.();\n if (pendingRef.current) scheduleRaf();\n }, [cancelRaf, isVisible, optsRef, scheduleRaf]);\n\n const request = useCallback(() => {\n if (!aliveRef.current) return;\n syncObserver();\n pendingRef.current = true;\n if (!isVisible()) {\n suspendedRef.current = true;\n return;\n }\n scheduleRaf();\n }, [isVisible, scheduleRaf, syncObserver]);\n\n const cancel = useCallback(() => {\n pendingRef.current = false;\n cancelRaf();\n }, [cancelRaf]);\n\n // Re-armed in setup because the refs survive StrictMode's simulated remount, and\n // a layout effect so the arming precedes the host's own layout effects — a loop\n // driven from one of those would otherwise be declined on the second mount.\n useLayoutEffect(() => {\n aliveRef.current = true;\n document.addEventListener('visibilitychange', syncGate);\n if (typeof IntersectionObserver !== 'undefined') {\n observerRef.current = new IntersectionObserver((entries) => {\n intersectingRef.current = entries.some((e) => e.isIntersecting);\n syncGate();\n });\n syncObserver();\n }\n return () => {\n aliveRef.current = false;\n document.removeEventListener('visibilitychange', syncGate);\n observerRef.current?.disconnect();\n observerRef.current = null;\n observedRef.current = null;\n intersectingRef.current = true;\n cancelRaf();\n pendingRef.current = false;\n suspendedRef.current = false;\n };\n }, [cancelRaf, syncGate, syncObserver]);\n\n // Stable identity: `requestRedraw` and friends outlive the render that made\n // them, and consumers capture the handle.\n return useMemo(() => ({ request, cancel, isVisible }), [request, cancel, isVisible]);\n}\n","import { useEffect, useMemo, useRef } from 'react';\nimport { useLatest } from './useLatest';\nimport { useVisibleRaf } from './useVisibleRaf';\n\n/** Options for {@link useFrameClock}. */\nexport interface FrameClockOptions {\n /** Stop the clock where it is. Time spent paused is not counted. */\n paused?: boolean;\n /** What `seconds()` reads before any frame has run. Read once, at mount. */\n start?: number;\n /** Frame source, as on `useVisibleRaf`. */\n requestFrame?: (cb: FrameRequestCallback) => number;\n cancelFrame?: (handle: number) => void;\n}\n\n/** A clock a paint reads without rendering. It has `subscribe`, so a render\n * layer can name it as its redraw source. */\nexport interface FrameClock {\n /** Seconds of frames that ran: time hidden, paused, or with no subscriber is left out. */\n seconds(): number;\n /** Called once per frame. The clock runs while it has a subscriber. */\n subscribe(listener: () => void): () => void;\n}\n\n/**\n * Elapsed time for something that animates in its paint and nowhere else: a\n * shader uniform, a procedural layer. Each frame notifies the subscribers and\n * changes no React state, so the component that owns it does not re-render.\n */\nexport function useFrameClock(options: FrameClockOptions = {}): FrameClock {\n const { paused = false, requestFrame, cancelFrame } = options;\n const elapsed = useRef(options.start ?? 0);\n const last = useRef<number | null>(null);\n const listeners = useRef(new Set<() => void>());\n const pausedRef = useLatest(paused);\n\n const loop = useVisibleRaf(\n (now) => {\n if (last.current !== null) elapsed.current += (now - last.current) / 1000;\n last.current = now;\n for (const listener of [...listeners.current]) listener();\n if (listeners.current.size > 0 && !pausedRef.current) loop.request();\n },\n { requestFrame, cancelFrame, onResume: () => { last.current = null; } },\n );\n\n useEffect(() => {\n if (paused) return;\n last.current = null;\n if (listeners.current.size > 0) loop.request();\n return () => loop.cancel();\n }, [paused, loop]);\n\n return useMemo<FrameClock>(() => ({\n seconds: () => elapsed.current,\n subscribe(listener) {\n const set = listeners.current;\n if (set.size === 0) last.current = null;\n set.add(listener);\n if (!pausedRef.current) loop.request();\n return () => {\n set.delete(listener);\n if (set.size === 0) loop.cancel();\n };\n },\n }), [loop, pausedRef]);\n}\n","import { useInsertionEffect, useRef } from 'react';\n\n/**\n * `value`, or the one the last committed render returned when `same` says the\n * two are equal — so a caller rebuilding an equivalent object every render\n * hands its readers one identity. Compared against the committed value only:\n * a render React throws away never becomes the one the next render keeps.\n */\nexport function useStableByContent<T>(value: T, same: (a: T, b: T) => boolean): T {\n const held = useRef(value);\n const out = held.current === value || same(held.current, value) ? held.current : value;\n useInsertionEffect(() => {\n held.current = out;\n });\n return out;\n}\n\n/** Same length, and the same element at every index. */\nexport function sameList<T>(a: readonly T[], b: readonly T[]): boolean {\n return a.length === b.length && a.every((t, i) => t === b[i]);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/assignRef.ts","../src/useCanvasFocus.ts","../src/useLatest.ts","../src/useVisibleRaf.ts","../src/useFrameClock.ts","../src/useStableByContent.ts"],"names":["useRef","useCallback","useMemo","useState","loop","useInsertionEffect"],"mappings":"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","file":"index.js","sourcesContent":["import type { Ref } from 'react';\n\n/**\n * Write `value` into a ref a caller passed in, whichever shape it has: call a\n * callback ref, set an object ref's `current`, ignore `null` or `undefined`.\n *\n * For a component that keeps the element (or handle) for itself and also\n * forwards it to a ref prop — call this from the component's own callback ref.\n */\nexport function assignRef<T>(ref: Ref<T> | undefined, value: T | null): void {\n if (typeof ref === 'function') ref(value);\n else if (ref) (ref as { current: T | null }).current = value;\n}\n","/**\n * Track DOM focus on a canvas (or any focusable host element). Returns the\n * live `focused` boolean, props to spread onto the focusable element, and a\n * stable `getFocused()` getter for use inside per-frame closures (notably\n * `RenderLayer.draw`, which is constructed once but called every frame).\n *\n * No Canvas/SceneCanvas integration. Consumers compose the hook with core's\n * `gateLayer` (or their own gate) to hide affordances when blurred.\n */\nimport { useCallback, useMemo, useRef, useState } from 'react';\n\n/** Options for `useCanvasFocus`. */\nexport interface UseCanvasFocusOptions {\n /** Initial focus state. Default `false` — handles hidden until first click. */\n initial?: boolean;\n /** `tabIndex` to set on the focusable element. Default `0` (focusable in\n * document tab order). Set to `-1` for programmatic-only focus. */\n tabIndex?: number;\n}\n\n/** What `useCanvasFocus` hands back: the focus state, the props that\n * maintain it, and a getter for reading it from per-frame code. */\nexport interface CanvasFocusReturn {\n /** Live boolean — true when the element has DOM focus. Triggers re-renders\n * when the focus state changes (set via React state). */\n focused: boolean;\n /** Spread onto the focusable element (e.g. the canvas). Includes\n * `tabIndex`, `onFocus`, `onBlur`. */\n focusProps: {\n tabIndex: number;\n onFocus: () => void;\n onBlur: () => void;\n };\n /** Stable getter for the live focused state. Use inside per-frame closures\n * like `RenderLayer.draw`, where the layer is constructed once and\n * reading `focused` directly would capture the value at construction\n * time. The getter always returns the current value. */\n getFocused: () => boolean;\n /** Imperative focus / blur. Useful for tests and programmatic control. */\n setFocused: (next: boolean) => void;\n}\n\n/** Track DOM focus on the canvas, so chrome that should only show while the\n * canvas is focused can be gated on it. */\nexport function useCanvasFocus(options: UseCanvasFocusOptions = {}): CanvasFocusReturn {\n const { initial = false, tabIndex = 0 } = options;\n const [focused, setFocusedState] = useState<boolean>(initial);\n // Every state change goes through a handler below that writes this first.\n const focusedRef = useRef<boolean>(initial);\n\n const onFocus = useCallback(() => {\n if (!focusedRef.current) {\n focusedRef.current = true;\n setFocusedState(true);\n }\n }, []);\n const onBlur = useCallback(() => {\n if (focusedRef.current) {\n focusedRef.current = false;\n setFocusedState(false);\n }\n }, []);\n const getFocused = useCallback(() => focusedRef.current, []);\n const setFocused = useCallback((next: boolean) => {\n if (focusedRef.current === next) return;\n focusedRef.current = next;\n setFocusedState(next);\n }, []);\n\n // Stable until tabIndex changes, so it can be spread onto memoized elements\n // without retriggering downstream effects.\n const focusProps = useMemo(() => ({ tabIndex, onFocus, onBlur }), [tabIndex, onFocus, onBlur]);\n\n return {\n focused,\n focusProps,\n getFocused,\n setFocused,\n };\n}\n","import { useInsertionEffect, useRef } from 'react';\n\n/**\n * A ref holding the `value` of the last **committed** render, for event\n * handlers, frame loops and other code that runs outside render and must not\n * re-subscribe every time `value` changes.\n *\n * Writing `ref.current = value` in a render body is not the same thing: React\n * may start a render and throw it away (a transition that suspends, a render\n * interrupted by a more urgent update), and the value that render computed\n * would stay behind for the next event to read. This writes from\n * `useInsertionEffect`, so only a render that commits ever lands, and it lands\n * before any layout effect in that commit runs.\n *\n * Don't read `.current` during render expecting this render's value: until the\n * commit it holds the previous committed one (on mount, this first `value`).\n * Render code should use `value` itself.\n */\nexport function useLatest<T>(value: T): { readonly current: T } {\n const ref = useRef(value);\n useInsertionEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n","/**\n * The one place weasel decides whether a frame may run, so that \"a loop does no\n * work nobody can see\" holds by construction rather than by every loop author\n * remembering. Nothing runs while the document is hidden; a loop that names an\n * element also stops while that element is outside the viewport.\n *\n * A request made while suspended is held, not dropped, and re-armed when the\n * surface comes back — so a loop never needs to poll visibility itself.\n */\n\nimport { type RefObject, useCallback, useLayoutEffect, useMemo, useRef } from 'react';\nimport { useLatest } from './useLatest';\n\n/** What a loop names as its element, resolved every time the gate is consulted\n * so a ref filled in after mount still starts the observer. */\nexport type VisibleRafTarget = RefObject<Element | null> | (() => Element | null);\n\n/** Options for {@link useVisibleRaf}. */\nexport interface VisibleRafOptions {\n /** Also gate on this element's intersection with the viewport. Omit it and\n * the loop is gated on document visibility alone. */\n target?: VisibleRafTarget;\n /**\n * Run frames regardless of visibility. The escape hatch for a loop that is\n * not painting for a viewer — an offscreen recording, an export driving its\n * own frames. It is the wrong fix for a loop that stalls after a tab switch:\n * that is a missing `request()`, and setting this hides it.\n */\n dangerouslyRunWhenHidden?: boolean;\n /** Frame clock. Read live, so a test may inject one after mount. */\n requestFrame?: (cb: FrameRequestCallback) => number;\n cancelFrame?: (handle: number) => void;\n /**\n * Runs once when the loop leaves suspension, before the frame that follows\n * it. Where a loop that measures elapsed time drops the interval it spent\n * suspended — without it, an hour hidden arrives as one hour-long frame.\n */\n onResume?: () => void;\n}\n\n/** The handle {@link useVisibleRaf} returns. Nothing runs until `request()` is called. */\nexport interface VisibleRaf {\n /** Ask for a frame. Idempotent while one is outstanding, and held rather than\n * dropped while suspended. Identity is stable for the component's lifetime. */\n request(): void;\n /** Drop the outstanding request, held or scheduled. */\n cancel(): void;\n /** Whether a frame would run right now. */\n isVisible(): boolean;\n}\n\nconst documentHidden = (): boolean => typeof document !== 'undefined' && document.hidden;\n\nconst resolveTarget = (target: VisibleRafTarget | undefined): Element | null =>\n typeof target === 'function' ? target() : (target?.current ?? null);\n\n/**\n * @param frame Runs on the frame, with the timestamp the clock supplied and the\n * loop's own handle. A continuous loop calls `loop.request()` from inside it;\n * a one-shot loop does not.\n */\nexport function useVisibleRaf(\n frame: (time: number, loop: VisibleRaf) => void,\n options: VisibleRafOptions = {},\n): VisibleRaf {\n const frameRef = useLatest(frame);\n const optsRef = useLatest(options);\n\n const rafRef = useRef<number | null>(null);\n const pendingRef = useRef(false);\n const suspendedRef = useRef(false);\n const aliveRef = useRef(true);\n const intersectingRef = useRef(true);\n const observerRef = useRef<IntersectionObserver | null>(null);\n const observedRef = useRef<Element | null>(null);\n const handleRef = useRef<VisibleRaf | null>(null);\n\n const isVisible = useCallback(\n (): boolean =>\n optsRef.current.dangerouslyRunWhenHidden === true ||\n (!documentHidden() && intersectingRef.current),\n [optsRef],\n );\n\n const cancelRaf = useCallback(() => {\n if (rafRef.current === null) return;\n (optsRef.current.cancelFrame ?? cancelAnimationFrame)(rafRef.current);\n rafRef.current = null;\n }, [optsRef]);\n\n const scheduleRaf = useCallback(() => {\n if (rafRef.current !== null || !aliveRef.current) return;\n rafRef.current = (optsRef.current.requestFrame ?? requestAnimationFrame)((time) => {\n // Released before the frame runs, so a `request()` issued from inside it\n // schedules the next one instead of being swallowed.\n rafRef.current = null;\n if (!aliveRef.current || !pendingRef.current || !isVisible()) return;\n pendingRef.current = false;\n frameRef.current(time, handleRef.current!);\n });\n }, [frameRef, isVisible, optsRef]);\n\n /** Attaches the observer once the named element exists. Ref callbacks and\n * lazily-mounted hosts fill their refs after this hook's effect has run. */\n const syncObserver = useCallback(() => {\n const io = observerRef.current;\n if (!io) return;\n const el = resolveTarget(optsRef.current.target);\n if (el === observedRef.current) return;\n if (observedRef.current) io.unobserve(observedRef.current);\n observedRef.current = el;\n // An element that has gone away stops gating rather than stopping the loop.\n intersectingRef.current = true;\n if (el) io.observe(el);\n }, [optsRef]);\n\n /** Re-reads the gate after something that could have changed it. Suspending\n * drops the outstanding frame but keeps the request; resuming hands the\n * loop its rebase before re-arming. */\n const syncGate = useCallback(() => {\n if (!isVisible()) {\n if (suspendedRef.current) return;\n suspendedRef.current = true;\n cancelRaf();\n return;\n }\n if (!suspendedRef.current) return;\n suspendedRef.current = false;\n if (!aliveRef.current) return;\n optsRef.current.onResume?.();\n if (pendingRef.current) scheduleRaf();\n }, [cancelRaf, isVisible, optsRef, scheduleRaf]);\n\n const request = useCallback(() => {\n if (!aliveRef.current) return;\n syncObserver();\n pendingRef.current = true;\n if (!isVisible()) {\n suspendedRef.current = true;\n return;\n }\n scheduleRaf();\n }, [isVisible, scheduleRaf, syncObserver]);\n\n const cancel = useCallback(() => {\n pendingRef.current = false;\n cancelRaf();\n }, [cancelRaf]);\n\n // Re-armed in setup because the refs survive StrictMode's simulated remount, and\n // a layout effect so the arming precedes the host's own layout effects — a loop\n // driven from one of those would otherwise be declined on the second mount.\n useLayoutEffect(() => {\n aliveRef.current = true;\n document.addEventListener('visibilitychange', syncGate);\n if (typeof IntersectionObserver !== 'undefined') {\n observerRef.current = new IntersectionObserver((entries) => {\n intersectingRef.current = entries.some((e) => e.isIntersecting);\n syncGate();\n });\n syncObserver();\n }\n return () => {\n aliveRef.current = false;\n document.removeEventListener('visibilitychange', syncGate);\n observerRef.current?.disconnect();\n observerRef.current = null;\n observedRef.current = null;\n intersectingRef.current = true;\n cancelRaf();\n pendingRef.current = false;\n suspendedRef.current = false;\n };\n }, [cancelRaf, syncGate, syncObserver]);\n\n // Stable identity: `requestRedraw` and friends outlive the render that made\n // them, and consumers capture the handle.\n const handle = useMemo(() => ({ request, cancel, isVisible }), [request, cancel, isVisible]);\n useLayoutEffect(() => { handleRef.current = handle; }, [handle]);\n return handle;\n}\n","import { createEmitter } from '@weasel-js/registry';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useLatest } from './useLatest';\nimport { useVisibleRaf } from './useVisibleRaf';\n\n/** Options for {@link useFrameClock}. */\nexport interface FrameClockOptions {\n /** Stop the clock where it is. Time spent paused is not counted. */\n paused?: boolean;\n /** What `seconds()` reads before any frame has run. Read once, at mount. */\n start?: number;\n /** Frame source, as on `useVisibleRaf`. */\n requestFrame?: (cb: FrameRequestCallback) => number;\n cancelFrame?: (handle: number) => void;\n}\n\n/** A clock a paint reads without rendering. It has `subscribe`, so a render\n * layer can name it as its redraw source. */\nexport interface FrameClock {\n /** Seconds of frames that ran: time hidden, paused, or with no subscriber is left out. */\n seconds(): number;\n /** Called once per frame. The clock runs while it has a subscriber. */\n subscribe(listener: () => void): () => void;\n}\n\n/**\n * Elapsed time for something that animates in its paint and nowhere else: a\n * shader uniform, a procedural layer. Each frame notifies the subscribers and\n * changes no React state, so the component that owns it does not re-render.\n */\nexport function useFrameClock(options: FrameClockOptions = {}): FrameClock {\n const { paused = false, requestFrame, cancelFrame } = options;\n const elapsed = useRef(options.start ?? 0);\n const last = useRef<number | null>(null);\n const [ticked] = useState(() => createEmitter());\n const pausedRef = useLatest(paused);\n\n const loop = useVisibleRaf(\n (now, loop) => {\n if (last.current !== null) elapsed.current += (now - last.current) / 1000;\n last.current = now;\n ticked.emit();\n if (ticked.size > 0 && !pausedRef.current) loop.request();\n },\n { requestFrame, cancelFrame, onResume: () => { last.current = null; } },\n );\n\n useEffect(() => {\n if (paused) return;\n last.current = null;\n if (ticked.size > 0) loop.request();\n return () => loop.cancel();\n }, [paused, loop, ticked]);\n\n return useMemo<FrameClock>(() => ({\n seconds: () => elapsed.current,\n subscribe(listener) {\n if (ticked.size === 0) last.current = null;\n const off = ticked.subscribe(listener);\n if (!pausedRef.current) loop.request();\n return () => {\n off();\n if (ticked.size === 0) loop.cancel();\n };\n },\n }), [loop, pausedRef, ticked]);\n}\n","import { useInsertionEffect, useRef } from 'react';\n\n/**\n * `value`, or the one the last committed render returned when `same` says the\n * two are equal — so a caller rebuilding an equivalent object every render\n * hands its readers one identity. Compared against the committed value only:\n * a render React throws away never becomes the one the next render keeps.\n */\nexport function useStableByContent<T>(value: T, same: (a: T, b: T) => boolean): T {\n const held = useRef(value);\n const out = held.current === value || same(held.current, value) ? held.current : value;\n useInsertionEffect(() => {\n held.current = out;\n });\n return out;\n}\n\n/** Same length, and the same element at every index. */\nexport function sameList<T>(a: readonly T[], b: readonly T[]): boolean {\n return a.length === b.length && a.every((t, i) => t === b[i]);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/react",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "Generic React hooks the weasel packages share: a committed-value ref and content-stable identity. No weasel domain in it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"peerDependenciesMeta": {
|
|
22
22
|
"react": { "optional": true }
|
|
23
23
|
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@weasel-js/registry": "1.10.1"
|
|
26
|
+
},
|
|
24
27
|
"author": "orochi235",
|
|
25
28
|
"homepage": "https://orochi235.github.io/weasel/",
|
|
26
29
|
"repository": {
|