@weasel-js/react 1.9.4 → 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 +113 -5
- package/dist/index.js +165 -2
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ref } from 'react';
|
|
1
|
+
import { Ref, RefObject } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Write `value` into a ref a caller passed in, whichever shape it has: call a
|
|
@@ -9,6 +9,39 @@ import { Ref } from 'react';
|
|
|
9
9
|
*/
|
|
10
10
|
declare function assignRef<T>(ref: Ref<T> | undefined, value: T | null): void;
|
|
11
11
|
|
|
12
|
+
/** Options for `useCanvasFocus`. */
|
|
13
|
+
interface UseCanvasFocusOptions {
|
|
14
|
+
/** Initial focus state. Default `false` — handles hidden until first click. */
|
|
15
|
+
initial?: boolean;
|
|
16
|
+
/** `tabIndex` to set on the focusable element. Default `0` (focusable in
|
|
17
|
+
* document tab order). Set to `-1` for programmatic-only focus. */
|
|
18
|
+
tabIndex?: number;
|
|
19
|
+
}
|
|
20
|
+
/** What `useCanvasFocus` hands back: the focus state, the props that
|
|
21
|
+
* maintain it, and a getter for reading it from per-frame code. */
|
|
22
|
+
interface CanvasFocusReturn {
|
|
23
|
+
/** Live boolean — true when the element has DOM focus. Triggers re-renders
|
|
24
|
+
* when the focus state changes (set via React state). */
|
|
25
|
+
focused: boolean;
|
|
26
|
+
/** Spread onto the focusable element (e.g. the canvas). Includes
|
|
27
|
+
* `tabIndex`, `onFocus`, `onBlur`. */
|
|
28
|
+
focusProps: {
|
|
29
|
+
tabIndex: number;
|
|
30
|
+
onFocus: () => void;
|
|
31
|
+
onBlur: () => void;
|
|
32
|
+
};
|
|
33
|
+
/** Stable getter for the live focused state. Use inside per-frame closures
|
|
34
|
+
* like `RenderLayer.draw`, where the layer is constructed once and
|
|
35
|
+
* reading `focused` directly would capture the value at construction
|
|
36
|
+
* time. The getter always returns the current value. */
|
|
37
|
+
getFocused: () => boolean;
|
|
38
|
+
/** Imperative focus / blur. Useful for tests and programmatic control. */
|
|
39
|
+
setFocused: (next: boolean) => void;
|
|
40
|
+
}
|
|
41
|
+
/** Track DOM focus on the canvas, so chrome that should only show while the
|
|
42
|
+
* canvas is focused can be gated on it. */
|
|
43
|
+
declare function useCanvasFocus(options?: UseCanvasFocusOptions): CanvasFocusReturn;
|
|
44
|
+
|
|
12
45
|
/**
|
|
13
46
|
* A ref holding the `value` of the last **committed** render, for event
|
|
14
47
|
* handlers, frame loops and other code that runs outside render and must not
|
|
@@ -19,9 +52,7 @@ declare function assignRef<T>(ref: Ref<T> | undefined, value: T | null): void;
|
|
|
19
52
|
* interrupted by a more urgent update), and the value that render computed
|
|
20
53
|
* would stay behind for the next event to read. This writes from
|
|
21
54
|
* `useInsertionEffect`, so only a render that commits ever lands, and it lands
|
|
22
|
-
* before any layout effect in that commit runs
|
|
23
|
-
* `useLayoutEffect`, or a synchronous paint one of them triggers, already sees
|
|
24
|
-
* it.
|
|
55
|
+
* before any layout effect in that commit runs.
|
|
25
56
|
*
|
|
26
57
|
* Don't read `.current` during render expecting this render's value: until the
|
|
27
58
|
* commit it holds the previous committed one (on mount, this first `value`).
|
|
@@ -31,6 +62,83 @@ declare function useLatest<T>(value: T): {
|
|
|
31
62
|
readonly current: T;
|
|
32
63
|
};
|
|
33
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The one place weasel decides whether a frame may run, so that "a loop does no
|
|
67
|
+
* work nobody can see" holds by construction rather than by every loop author
|
|
68
|
+
* remembering. Nothing runs while the document is hidden; a loop that names an
|
|
69
|
+
* element also stops while that element is outside the viewport.
|
|
70
|
+
*
|
|
71
|
+
* A request made while suspended is held, not dropped, and re-armed when the
|
|
72
|
+
* surface comes back — so a loop never needs to poll visibility itself.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/** What a loop names as its element, resolved every time the gate is consulted
|
|
76
|
+
* so a ref filled in after mount still starts the observer. */
|
|
77
|
+
type VisibleRafTarget = RefObject<Element | null> | (() => Element | null);
|
|
78
|
+
/** Options for {@link useVisibleRaf}. */
|
|
79
|
+
interface VisibleRafOptions {
|
|
80
|
+
/** Also gate on this element's intersection with the viewport. Omit it and
|
|
81
|
+
* the loop is gated on document visibility alone. */
|
|
82
|
+
target?: VisibleRafTarget;
|
|
83
|
+
/**
|
|
84
|
+
* Run frames regardless of visibility. The escape hatch for a loop that is
|
|
85
|
+
* not painting for a viewer — an offscreen recording, an export driving its
|
|
86
|
+
* own frames. It is the wrong fix for a loop that stalls after a tab switch:
|
|
87
|
+
* that is a missing `request()`, and setting this hides it.
|
|
88
|
+
*/
|
|
89
|
+
dangerouslyRunWhenHidden?: boolean;
|
|
90
|
+
/** Frame clock. Read live, so a test may inject one after mount. */
|
|
91
|
+
requestFrame?: (cb: FrameRequestCallback) => number;
|
|
92
|
+
cancelFrame?: (handle: number) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Runs once when the loop leaves suspension, before the frame that follows
|
|
95
|
+
* it. Where a loop that measures elapsed time drops the interval it spent
|
|
96
|
+
* suspended — without it, an hour hidden arrives as one hour-long frame.
|
|
97
|
+
*/
|
|
98
|
+
onResume?: () => void;
|
|
99
|
+
}
|
|
100
|
+
/** The handle {@link useVisibleRaf} returns. Nothing runs until `request()` is called. */
|
|
101
|
+
interface VisibleRaf {
|
|
102
|
+
/** Ask for a frame. Idempotent while one is outstanding, and held rather than
|
|
103
|
+
* dropped while suspended. Identity is stable for the component's lifetime. */
|
|
104
|
+
request(): void;
|
|
105
|
+
/** Drop the outstanding request, held or scheduled. */
|
|
106
|
+
cancel(): void;
|
|
107
|
+
/** Whether a frame would run right now. */
|
|
108
|
+
isVisible(): boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
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
|
+
*/
|
|
115
|
+
declare function useVisibleRaf(frame: (time: number, loop: VisibleRaf) => void, options?: VisibleRafOptions): VisibleRaf;
|
|
116
|
+
|
|
117
|
+
/** Options for {@link useFrameClock}. */
|
|
118
|
+
interface FrameClockOptions {
|
|
119
|
+
/** Stop the clock where it is. Time spent paused is not counted. */
|
|
120
|
+
paused?: boolean;
|
|
121
|
+
/** What `seconds()` reads before any frame has run. Read once, at mount. */
|
|
122
|
+
start?: number;
|
|
123
|
+
/** Frame source, as on `useVisibleRaf`. */
|
|
124
|
+
requestFrame?: (cb: FrameRequestCallback) => number;
|
|
125
|
+
cancelFrame?: (handle: number) => void;
|
|
126
|
+
}
|
|
127
|
+
/** A clock a paint reads without rendering. It has `subscribe`, so a render
|
|
128
|
+
* layer can name it as its redraw source. */
|
|
129
|
+
interface FrameClock {
|
|
130
|
+
/** Seconds of frames that ran: time hidden, paused, or with no subscriber is left out. */
|
|
131
|
+
seconds(): number;
|
|
132
|
+
/** Called once per frame. The clock runs while it has a subscriber. */
|
|
133
|
+
subscribe(listener: () => void): () => void;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Elapsed time for something that animates in its paint and nowhere else: a
|
|
137
|
+
* shader uniform, a procedural layer. Each frame notifies the subscribers and
|
|
138
|
+
* changes no React state, so the component that owns it does not re-render.
|
|
139
|
+
*/
|
|
140
|
+
declare function useFrameClock(options?: FrameClockOptions): FrameClock;
|
|
141
|
+
|
|
34
142
|
/**
|
|
35
143
|
* `value`, or the one the last committed render returned when `same` says the
|
|
36
144
|
* two are equal — so a caller rebuilding an equivalent object every render
|
|
@@ -41,4 +149,4 @@ declare function useStableByContent<T>(value: T, same: (a: T, b: T) => boolean):
|
|
|
41
149
|
/** Same length, and the same element at every index. */
|
|
42
150
|
declare function sameList<T>(a: readonly T[], b: readonly T[]): boolean;
|
|
43
151
|
|
|
44
|
-
export { assignRef, sameList, useLatest, useStableByContent };
|
|
152
|
+
export { type CanvasFocusReturn, type FrameClock, type FrameClockOptions, type UseCanvasFocusOptions, type VisibleRaf, type VisibleRafOptions, type VisibleRafTarget, assignRef, sameList, useCanvasFocus, useFrameClock, useLatest, useStableByContent, useVisibleRaf };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
import { useRef, useInsertionEffect } from 'react';
|
|
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) {
|
|
5
6
|
if (typeof ref === "function") ref(value);
|
|
6
7
|
else if (ref) ref.current = value;
|
|
7
8
|
}
|
|
9
|
+
function useCanvasFocus(options = {}) {
|
|
10
|
+
const { initial = false, tabIndex = 0 } = options;
|
|
11
|
+
const [focused, setFocusedState] = useState(initial);
|
|
12
|
+
const focusedRef = useRef(initial);
|
|
13
|
+
const onFocus = useCallback(() => {
|
|
14
|
+
if (!focusedRef.current) {
|
|
15
|
+
focusedRef.current = true;
|
|
16
|
+
setFocusedState(true);
|
|
17
|
+
}
|
|
18
|
+
}, []);
|
|
19
|
+
const onBlur = useCallback(() => {
|
|
20
|
+
if (focusedRef.current) {
|
|
21
|
+
focusedRef.current = false;
|
|
22
|
+
setFocusedState(false);
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
const getFocused = useCallback(() => focusedRef.current, []);
|
|
26
|
+
const setFocused = useCallback((next) => {
|
|
27
|
+
if (focusedRef.current === next) return;
|
|
28
|
+
focusedRef.current = next;
|
|
29
|
+
setFocusedState(next);
|
|
30
|
+
}, []);
|
|
31
|
+
const focusProps = useMemo(() => ({ tabIndex, onFocus, onBlur }), [tabIndex, onFocus, onBlur]);
|
|
32
|
+
return {
|
|
33
|
+
focused,
|
|
34
|
+
focusProps,
|
|
35
|
+
getFocused,
|
|
36
|
+
setFocused
|
|
37
|
+
};
|
|
38
|
+
}
|
|
8
39
|
function useLatest(value) {
|
|
9
40
|
const ref = useRef(value);
|
|
10
41
|
useInsertionEffect(() => {
|
|
@@ -12,6 +43,138 @@ function useLatest(value) {
|
|
|
12
43
|
});
|
|
13
44
|
return ref;
|
|
14
45
|
}
|
|
46
|
+
var documentHidden = () => typeof document !== "undefined" && document.hidden;
|
|
47
|
+
var resolveTarget = (target) => typeof target === "function" ? target() : target?.current ?? null;
|
|
48
|
+
function useVisibleRaf(frame, options = {}) {
|
|
49
|
+
const frameRef = useLatest(frame);
|
|
50
|
+
const optsRef = useLatest(options);
|
|
51
|
+
const rafRef = useRef(null);
|
|
52
|
+
const pendingRef = useRef(false);
|
|
53
|
+
const suspendedRef = useRef(false);
|
|
54
|
+
const aliveRef = useRef(true);
|
|
55
|
+
const intersectingRef = useRef(true);
|
|
56
|
+
const observerRef = useRef(null);
|
|
57
|
+
const observedRef = useRef(null);
|
|
58
|
+
const handleRef = useRef(null);
|
|
59
|
+
const isVisible = useCallback(
|
|
60
|
+
() => optsRef.current.dangerouslyRunWhenHidden === true || !documentHidden() && intersectingRef.current,
|
|
61
|
+
[optsRef]
|
|
62
|
+
);
|
|
63
|
+
const cancelRaf = useCallback(() => {
|
|
64
|
+
if (rafRef.current === null) return;
|
|
65
|
+
(optsRef.current.cancelFrame ?? cancelAnimationFrame)(rafRef.current);
|
|
66
|
+
rafRef.current = null;
|
|
67
|
+
}, [optsRef]);
|
|
68
|
+
const scheduleRaf = useCallback(() => {
|
|
69
|
+
if (rafRef.current !== null || !aliveRef.current) return;
|
|
70
|
+
rafRef.current = (optsRef.current.requestFrame ?? requestAnimationFrame)((time) => {
|
|
71
|
+
rafRef.current = null;
|
|
72
|
+
if (!aliveRef.current || !pendingRef.current || !isVisible()) return;
|
|
73
|
+
pendingRef.current = false;
|
|
74
|
+
frameRef.current(time, handleRef.current);
|
|
75
|
+
});
|
|
76
|
+
}, [frameRef, isVisible, optsRef]);
|
|
77
|
+
const syncObserver = useCallback(() => {
|
|
78
|
+
const io = observerRef.current;
|
|
79
|
+
if (!io) return;
|
|
80
|
+
const el = resolveTarget(optsRef.current.target);
|
|
81
|
+
if (el === observedRef.current) return;
|
|
82
|
+
if (observedRef.current) io.unobserve(observedRef.current);
|
|
83
|
+
observedRef.current = el;
|
|
84
|
+
intersectingRef.current = true;
|
|
85
|
+
if (el) io.observe(el);
|
|
86
|
+
}, [optsRef]);
|
|
87
|
+
const syncGate = useCallback(() => {
|
|
88
|
+
if (!isVisible()) {
|
|
89
|
+
if (suspendedRef.current) return;
|
|
90
|
+
suspendedRef.current = true;
|
|
91
|
+
cancelRaf();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!suspendedRef.current) return;
|
|
95
|
+
suspendedRef.current = false;
|
|
96
|
+
if (!aliveRef.current) return;
|
|
97
|
+
optsRef.current.onResume?.();
|
|
98
|
+
if (pendingRef.current) scheduleRaf();
|
|
99
|
+
}, [cancelRaf, isVisible, optsRef, scheduleRaf]);
|
|
100
|
+
const request = useCallback(() => {
|
|
101
|
+
if (!aliveRef.current) return;
|
|
102
|
+
syncObserver();
|
|
103
|
+
pendingRef.current = true;
|
|
104
|
+
if (!isVisible()) {
|
|
105
|
+
suspendedRef.current = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
scheduleRaf();
|
|
109
|
+
}, [isVisible, scheduleRaf, syncObserver]);
|
|
110
|
+
const cancel = useCallback(() => {
|
|
111
|
+
pendingRef.current = false;
|
|
112
|
+
cancelRaf();
|
|
113
|
+
}, [cancelRaf]);
|
|
114
|
+
useLayoutEffect(() => {
|
|
115
|
+
aliveRef.current = true;
|
|
116
|
+
document.addEventListener("visibilitychange", syncGate);
|
|
117
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
118
|
+
observerRef.current = new IntersectionObserver((entries) => {
|
|
119
|
+
intersectingRef.current = entries.some((e) => e.isIntersecting);
|
|
120
|
+
syncGate();
|
|
121
|
+
});
|
|
122
|
+
syncObserver();
|
|
123
|
+
}
|
|
124
|
+
return () => {
|
|
125
|
+
aliveRef.current = false;
|
|
126
|
+
document.removeEventListener("visibilitychange", syncGate);
|
|
127
|
+
observerRef.current?.disconnect();
|
|
128
|
+
observerRef.current = null;
|
|
129
|
+
observedRef.current = null;
|
|
130
|
+
intersectingRef.current = true;
|
|
131
|
+
cancelRaf();
|
|
132
|
+
pendingRef.current = false;
|
|
133
|
+
suspendedRef.current = false;
|
|
134
|
+
};
|
|
135
|
+
}, [cancelRaf, syncGate, syncObserver]);
|
|
136
|
+
const handle = useMemo(() => ({ request, cancel, isVisible }), [request, cancel, isVisible]);
|
|
137
|
+
useLayoutEffect(() => {
|
|
138
|
+
handleRef.current = handle;
|
|
139
|
+
}, [handle]);
|
|
140
|
+
return handle;
|
|
141
|
+
}
|
|
142
|
+
function useFrameClock(options = {}) {
|
|
143
|
+
const { paused = false, requestFrame, cancelFrame } = options;
|
|
144
|
+
const elapsed = useRef(options.start ?? 0);
|
|
145
|
+
const last = useRef(null);
|
|
146
|
+
const [ticked] = useState(() => createEmitter());
|
|
147
|
+
const pausedRef = useLatest(paused);
|
|
148
|
+
const loop = useVisibleRaf(
|
|
149
|
+
(now, loop2) => {
|
|
150
|
+
if (last.current !== null) elapsed.current += (now - last.current) / 1e3;
|
|
151
|
+
last.current = now;
|
|
152
|
+
ticked.emit();
|
|
153
|
+
if (ticked.size > 0 && !pausedRef.current) loop2.request();
|
|
154
|
+
},
|
|
155
|
+
{ requestFrame, cancelFrame, onResume: () => {
|
|
156
|
+
last.current = null;
|
|
157
|
+
} }
|
|
158
|
+
);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (paused) return;
|
|
161
|
+
last.current = null;
|
|
162
|
+
if (ticked.size > 0) loop.request();
|
|
163
|
+
return () => loop.cancel();
|
|
164
|
+
}, [paused, loop, ticked]);
|
|
165
|
+
return useMemo(() => ({
|
|
166
|
+
seconds: () => elapsed.current,
|
|
167
|
+
subscribe(listener) {
|
|
168
|
+
if (ticked.size === 0) last.current = null;
|
|
169
|
+
const off = ticked.subscribe(listener);
|
|
170
|
+
if (!pausedRef.current) loop.request();
|
|
171
|
+
return () => {
|
|
172
|
+
off();
|
|
173
|
+
if (ticked.size === 0) loop.cancel();
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}), [loop, pausedRef, ticked]);
|
|
177
|
+
}
|
|
15
178
|
function useStableByContent(value, same) {
|
|
16
179
|
const held = useRef(value);
|
|
17
180
|
const out = held.current === value || same(held.current, value) ? held.current : value;
|
|
@@ -24,6 +187,6 @@ function sameList(a, b) {
|
|
|
24
187
|
return a.length === b.length && a.every((t, i) => t === b[i]);
|
|
25
188
|
}
|
|
26
189
|
|
|
27
|
-
export { assignRef, sameList, useLatest, useStableByContent };
|
|
190
|
+
export { assignRef, sameList, useCanvasFocus, useFrameClock, useLatest, useStableByContent, useVisibleRaf };
|
|
28
191
|
//# sourceMappingURL=index.js.map
|
|
29
192
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assignRef.ts","../src/useLatest.ts","../src/useStableByContent.ts"],"names":["useRef","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;ACQO,SAAS,UAAa,KAAA,EAAmC;AAC9D,EAAA,MAAM,GAAA,GAAM,OAAO,KAAK,CAAA;AACxB,EAAA,kBAAA,CAAmB,MAAM;AACvB,IAAA,GAAA,CAAI,OAAA,GAAU,KAAA;AAAA,EAChB,CAAC,CAAA;AACD,EAAA,OAAO,GAAA;AACT;AClBO,SAAS,kBAAA,CAAsB,OAAU,IAAA,EAAkC;AAChF,EAAA,MAAM,IAAA,GAAOA,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,EAAAC,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","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 — a sibling's or a child's\n * `useLayoutEffect`, or a synchronous paint one of them triggers, already sees\n * it.\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","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.
|
|
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": {
|