phase 0.0.6 → 0.0.8
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 +201 -15
- package/dist/{pointer-D5F7_Jjs.js → debounce-D5ugkrXN.js} +411 -2
- package/dist/debounce-D5ugkrXN.js.map +1 -0
- package/dist/{index-tRHsbTE2.d.ts → index-C10bmjm1.d.ts} +99 -2
- package/dist/index-C10bmjm1.d.ts.map +1 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/react.d.ts +109 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +191 -52
- package/dist/react.js.map +1 -1
- package/package.json +4 -4
- package/dist/index-tRHsbTE2.d.ts.map +0 -1
- package/dist/pointer-D5F7_Jjs.js.map +0 -1
package/README.md
CHANGED
|
@@ -34,6 +34,9 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
|
|
|
34
34
|
- [createSight](#createsight)
|
|
35
35
|
- [createLifecycle](#createlifecycle)
|
|
36
36
|
- [createScrollProgress](#createscrollprogress)
|
|
37
|
+
- [createScroll](#createscroll)
|
|
38
|
+
- [createThrottle](#createthrottle)
|
|
39
|
+
- [createDebounce](#createdebounce)
|
|
37
40
|
- [prefersReducedMotion](#prefersreducedmotion)
|
|
38
41
|
- [Easing and math](#easing-and-math)
|
|
39
42
|
- [Choosing a primitive](#choosing-a-primitive)
|
|
@@ -44,6 +47,9 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
|
|
|
44
47
|
- [useTween](#usetween)
|
|
45
48
|
- [usePresence](#usepresence)
|
|
46
49
|
- [useScrollProgress](#usescrollprogress)
|
|
50
|
+
- [useScroll](#usescroll)
|
|
51
|
+
- [useThrottledCallback](#usethrottledcallback)
|
|
52
|
+
- [useDebouncedCallback](#usedebouncedcallback)
|
|
47
53
|
- [Utility hooks](#utility-hooks)
|
|
48
54
|
- [React components](#react-components)
|
|
49
55
|
- [How animations work](#how-animations-work)
|
|
@@ -323,7 +329,7 @@ Pause priority is `reduced-motion` > `sight` > `manual`.
|
|
|
323
329
|
|
|
324
330
|
Reports what fraction of an element is currently visible in the viewport (0–1), via the shared IntersectionObserver pool. Zero forced reflows, zero extra observers. Ideal for reveal/opacity effects.
|
|
325
331
|
|
|
326
|
-
> **
|
|
332
|
+
> **Visibility ratio, not scroll offset.** This reports `intersectionRatio` (how much of an element is visible in the viewport), which plateaus for tall elements once they fill it. For a scroll container's _own_ offset (scrollbars, carousels) use [`createScroll`](#createscroll); for CSS-declarative scroll-linked animation use the native `ScrollTimeline` API; for spring/gesture scroll use `motion`.
|
|
327
333
|
|
|
328
334
|
```ts
|
|
329
335
|
import { createScrollProgress } from 'phase';
|
|
@@ -353,6 +359,112 @@ The `steps` option controls threshold granularity. Default `20` generates 21 eve
|
|
|
353
359
|
| `root` | `Element \| Document \| null` | — | IO root element |
|
|
354
360
|
| `rootMargin` | `string` | — | IO root margin |
|
|
355
361
|
|
|
362
|
+
### createScroll
|
|
363
|
+
|
|
364
|
+
Tracks a scroll container's offset and progress. Reads `scrollLeft`/`scrollTop` once per rAF frame and reads the reflow-heavy geometry (`scrollWidth`/`clientWidth`) only on resize or explicit `measure()`, never on the scroll path. Auto-pauses off-screen via the shared IntersectionObserver pool. This is to `scroll` + `scrollWidth` what `createPointer` is to `pointermove` + `getBoundingClientRect`.
|
|
365
|
+
|
|
366
|
+
> **Scroll offset, not visibility ratio.** This reports the element's own scroll position (for scrollbars, carousels, position indicators). For _how much of an element is in the viewport_, use [`createScrollProgress`](#createscrollprogress); for CSS-declarative scroll-linked animation, use the native `ScrollTimeline` API.
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { createScroll } from 'phase';
|
|
370
|
+
|
|
371
|
+
const scroll = createScroll({
|
|
372
|
+
element: viewport,
|
|
373
|
+
onScroll: (s) => {
|
|
374
|
+
// thumb CSS needs `transform-origin: left` so scaleX anchors to the track start
|
|
375
|
+
thumb.style.transform = `translateX(${s.progressX * (1 - s.visibleX) * 100}%) scaleX(${s.visibleX})`;
|
|
376
|
+
prevButton.disabled = s.x <= 1;
|
|
377
|
+
nextButton.disabled = s.x >= s.maxX - 1;
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// scroll.state.progressX === 0.5 (synchronous read)
|
|
382
|
+
|
|
383
|
+
// after mutating scrollable content:
|
|
384
|
+
scroll.measure();
|
|
385
|
+
|
|
386
|
+
// cleanup:
|
|
387
|
+
scroll.stop();
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`onScroll` receives the same `ScrollState` object every frame (mutated in place, zero per-frame allocations): `x`, `y`, `maxX`, `maxY`, `progressX`, `progressY`, and the visible fractions `visibleX`/`visibleY` (`clientWidth / scrollWidth`, i.e. a scrollbar thumb's `scaleX`). The `ResizeObserver` recomputes geometry on container resize; call `measure()` after content changes that alter `scrollWidth`.
|
|
391
|
+
|
|
392
|
+
#### Scroll options
|
|
393
|
+
|
|
394
|
+
| Option | Type | Default | Description |
|
|
395
|
+
| --------------------- | ------------------------------ | --------- | -------------------------------------------------- |
|
|
396
|
+
| `element` | `Element` | required | Scroll container to track |
|
|
397
|
+
| `onScroll` | `(state: ScrollState) => void` | required | Called once per rAF frame with position + progress |
|
|
398
|
+
| `onPhaseChange` | `(phase, reason) => void` | — | Called on phase transitions |
|
|
399
|
+
| `visibility` | `'pause' \| 'ignore'` | `'pause'` | Pause tracking when off-screen, or ignore |
|
|
400
|
+
| `intersectionOptions` | `IntersectionObserverInit` | — | Forwarded to the visibility observer |
|
|
401
|
+
| `signal` | `AbortSignal` | — | Stops the tracker when aborted |
|
|
402
|
+
|
|
403
|
+
The options type is `CreateScrollOptions` (`ScrollOptions` is a `lib.dom` global and must not be shadowed).
|
|
404
|
+
|
|
405
|
+
### createThrottle
|
|
406
|
+
|
|
407
|
+
Frame-aligned, visibility-aware throttle for event-driven work below frame rate (socket emits, worker messaging, expensive recompute). Leading calls fire synchronously; a pending trailing call fires with the latest value on the first animation frame at or past `interval`. Nothing is scheduled while the trigger is idle or the document is hidden.
|
|
408
|
+
|
|
409
|
+
> **Event-driven, not a loop.** This fires on the trigger and idles otherwise. To cap a continuous render loop, use `fps` on [`createLoop`](#createloop). To think in rates, `interval: 1000 / 20` reads as "at most 20 per second".
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { createThrottle } from 'phase';
|
|
413
|
+
|
|
414
|
+
const throttle = createThrottle({
|
|
415
|
+
callback: (state) => socket.emit('cursor', state.x, state.y),
|
|
416
|
+
interval: 50,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const pointer = createPointer({ element, onPointer: throttle.call });
|
|
420
|
+
|
|
421
|
+
// throttle.flush() fires a pending trailing call now
|
|
422
|
+
// throttle.cancel() discards it and resets the window
|
|
423
|
+
// cleanup:
|
|
424
|
+
throttle.stop();
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
When the document hides, a pending call is flushed with the latest value (default) or dropped per `hidden`. Calls made while hidden are recorded but fire nothing until the document is visible again.
|
|
428
|
+
|
|
429
|
+
#### Throttle options
|
|
430
|
+
|
|
431
|
+
| Option | Type | Default | Description |
|
|
432
|
+
| ---------- | ----------------------------------- | --------- | --------------------------------------------- |
|
|
433
|
+
| `callback` | `(value: T) => void` | required | Called with the latest value passed to `call` |
|
|
434
|
+
| `interval` | `number` | required | Minimum ms between invocations |
|
|
435
|
+
| `edge` | `'leading' \| 'trailing' \| 'both'` | `'both'` | Which edges fire |
|
|
436
|
+
| `hidden` | `'flush' \| 'drop'` | `'flush'` | Pending-call policy when the document hides |
|
|
437
|
+
| `signal` | `AbortSignal` | — | Stops the throttle when aborted |
|
|
438
|
+
|
|
439
|
+
### createDebounce
|
|
440
|
+
|
|
441
|
+
Visibility-aware trailing debounce: fires the callback with the latest value once `wait` ms pass without a new call. No timer runs while the document is hidden; the quiet period restarts on return. Use it for work that should wait out a burst, like reallocating canvas buffers after a resize stream settles.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { createDebounce } from 'phase';
|
|
445
|
+
|
|
446
|
+
const debounce = createDebounce({
|
|
447
|
+
callback: (size) => reallocateBuffers(size),
|
|
448
|
+
wait: 250,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
debounce.call({ width, height });
|
|
452
|
+
|
|
453
|
+
// cleanup:
|
|
454
|
+
debounce.stop();
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Same surface as `createThrottle`: `flush()`, `cancel()`, a synchronous `pending` read, and terminal `stop()`.
|
|
458
|
+
|
|
459
|
+
#### Debounce options
|
|
460
|
+
|
|
461
|
+
| Option | Type | Default | Description |
|
|
462
|
+
| ---------- | -------------------- | --------- | --------------------------------------------- |
|
|
463
|
+
| `callback` | `(value: T) => void` | required | Called with the latest value passed to `call` |
|
|
464
|
+
| `wait` | `number` | required | Quiet period in ms; each call restarts it |
|
|
465
|
+
| `hidden` | `'flush' \| 'drop'` | `'flush'` | Pending-call policy when the document hides |
|
|
466
|
+
| `signal` | `AbortSignal` | — | Stops the debounce when aborted |
|
|
467
|
+
|
|
356
468
|
### prefersReducedMotion
|
|
357
469
|
|
|
358
470
|
Returns `true` when reduced motion is enabled at the OS level. Use it to gate expensive setup or dynamic imports.
|
|
@@ -423,6 +535,8 @@ Easing, interpolation, and your value range are three separate concerns. `phase`
|
|
|
423
535
|
| Pause non-`phase` work inside a `Defer` subtree | `useRenderState` |
|
|
424
536
|
| Subscribe to scroll, size, or media values reactively | `useScrollProgress` / `useSize` / `useContainerQuery` / `useMediaQuery` |
|
|
425
537
|
| Scroll/size/visibility without re-renders? | Same hooks with a callback (`onProgress` / `onResize` / `onVisibilityChange`), read via ref |
|
|
538
|
+
| Rate-limit event-driven work (sockets, workers) | `useThrottledCallback` |
|
|
539
|
+
| Run once after a burst settles (resize, typing) | `useDebouncedCallback` |
|
|
426
540
|
|
|
427
541
|
**`useSight` vs `useLifecycle`:** `useSight` reports pure visibility (for lazy-mounting, analytics, `WhenVisible`). `useLifecycle` folds in reduced motion and a manual pause, so you can't accidentally animate for users who asked not to. If you're gating an animation, use `useLifecycle`. If you're gating content, use `useSight`.
|
|
428
542
|
|
|
@@ -576,7 +690,7 @@ return (
|
|
|
576
690
|
|
|
577
691
|
### useScrollProgress
|
|
578
692
|
|
|
579
|
-
Element visibility ratio as a 0–1 value. Wraps `createScrollProgress` with React lifecycle management (
|
|
693
|
+
Element visibility ratio as a 0–1 value. Wraps `createScrollProgress` with React lifecycle management. This is a _visibility_ fraction (how much of the element is on screen); for a scroll container's own _position_ (scrollbars, carousels) use [`useScroll`](#usescroll) instead. See the [note on scope](#createscrollprogress) for the full distinction.
|
|
580
694
|
|
|
581
695
|
```tsx
|
|
582
696
|
import { useScrollProgress } from 'phase/react';
|
|
@@ -593,6 +707,72 @@ function FadeIn({ children }) {
|
|
|
593
707
|
|
|
594
708
|
Re-renders only at threshold crossings (~20 per full viewport traversal at default steps). `progress` is `0` before first observation.
|
|
595
709
|
|
|
710
|
+
### useScroll
|
|
711
|
+
|
|
712
|
+
Scroll offset and progress for a scroll container. Wraps `createScroll` with React lifecycle management. Position is delivered imperatively via `onScroll` (never per-frame state); only the phase (`tracking`/`paused`) is reactive. Mirrors `usePointer`.
|
|
713
|
+
|
|
714
|
+
```tsx
|
|
715
|
+
import { useRef } from 'react';
|
|
716
|
+
import { useScroll } from 'phase/react';
|
|
717
|
+
|
|
718
|
+
function Carousel({ children }) {
|
|
719
|
+
// thumb uses `origin-left` so scaleX anchors to the track start
|
|
720
|
+
const thumbRef = useRef<HTMLDivElement>(null);
|
|
721
|
+
const { ref, measure } = useScroll<HTMLDivElement>({
|
|
722
|
+
onScroll: (s) => {
|
|
723
|
+
thumbRef.current?.style.setProperty(
|
|
724
|
+
'transform',
|
|
725
|
+
`translateX(${s.progressX * (1 - s.visibleX) * 100}%) scaleX(${s.visibleX})`,
|
|
726
|
+
);
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
return (
|
|
731
|
+
<div ref={ref} className="overflow-x-auto">
|
|
732
|
+
{children}
|
|
733
|
+
</div>
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Scrolling writes to the DOM directly with zero re-renders. Read the latest position on demand from `stateRef.current` (e.g. inside a `useLoop` tick), and call `measure()` after changing scrollable content.
|
|
739
|
+
|
|
740
|
+
### useThrottledCallback
|
|
741
|
+
|
|
742
|
+
Wraps `createThrottle` with React lifecycle management. Returns a stable-identity throttled function (with `flush()` and `cancel()` attached) that drops directly into any callback slot and always invokes the latest `callback`.
|
|
743
|
+
|
|
744
|
+
```tsx
|
|
745
|
+
import { usePointer, useThrottledCallback } from 'phase/react';
|
|
746
|
+
|
|
747
|
+
function LiveCursor() {
|
|
748
|
+
const emit = useThrottledCallback(
|
|
749
|
+
(s: PointerState) => socket.emit('cursor', { x: s.x, y: s.y }),
|
|
750
|
+
{ interval: 50 },
|
|
751
|
+
);
|
|
752
|
+
const { ref } = usePointer({ onPointer: emit });
|
|
753
|
+
return <div ref={ref} />;
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Unmount and option changes discard a pending trailing call. When the final value must land, flush in your own cleanup: `useEffect(() => () => emit.flush(), [emit])`.
|
|
758
|
+
|
|
759
|
+
### useDebouncedCallback
|
|
760
|
+
|
|
761
|
+
Wraps `createDebounce` with React lifecycle management. Same shape as `useThrottledCallback`, but fires once `wait` ms pass without a new call.
|
|
762
|
+
|
|
763
|
+
```tsx
|
|
764
|
+
import { useSize, useDebouncedCallback } from 'phase/react';
|
|
765
|
+
|
|
766
|
+
function SimulationCanvas() {
|
|
767
|
+
const realloc = useDebouncedCallback(
|
|
768
|
+
(size: Size) => reallocateBuffers(size),
|
|
769
|
+
{ wait: 250 },
|
|
770
|
+
);
|
|
771
|
+
const { ref } = useSize({ onResize: realloc });
|
|
772
|
+
return <canvas ref={ref} />;
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
596
776
|
### Utility hooks
|
|
597
777
|
|
|
598
778
|
| Hook | Purpose |
|
|
@@ -898,44 +1078,50 @@ Minimal footprint is a core promise (see [Why phase](#why-phase)). Every export
|
|
|
898
1078
|
| Export | Size (min+brotli) |
|
|
899
1079
|
| ------------------------- | ----------------: |
|
|
900
1080
|
| **Core** | |
|
|
901
|
-
| `createTicker` |
|
|
1081
|
+
| `createTicker` | 834 B |
|
|
902
1082
|
| `createSight` | 963 B |
|
|
903
1083
|
| `createLifecycle` | 1.47 kB |
|
|
904
1084
|
| `createLoop` | 2.59 kB |
|
|
905
|
-
| `createScrollProgress` |
|
|
1085
|
+
| `createScrollProgress` | 878 B |
|
|
906
1086
|
| `createRenderState` | 495 B |
|
|
907
1087
|
| `createDevicePixelRatio` | 544 B |
|
|
908
1088
|
| `createMutation` | 1.17 kB |
|
|
909
1089
|
| `createPointer` | 1.26 kB |
|
|
1090
|
+
| `createScroll` | 1.45 kB |
|
|
1091
|
+
| `createThrottle` | 657 B |
|
|
1092
|
+
| `createDebounce` | 559 B |
|
|
910
1093
|
| `whenIdle` | 409 B |
|
|
911
1094
|
| `prefersReducedMotion` | 101 B |
|
|
912
1095
|
| **Ease** | |
|
|
913
1096
|
| `ease (all)` | 210 B |
|
|
914
1097
|
| **React** | |
|
|
915
|
-
| `useLoop` | 2.
|
|
916
|
-
| `useLifecycle` | 1.
|
|
917
|
-
| `useSight` | 1.
|
|
1098
|
+
| `useLoop` | 2.82 kB |
|
|
1099
|
+
| `useLifecycle` | 1.68 kB |
|
|
1100
|
+
| `useSight` | 1.19 kB |
|
|
918
1101
|
| `useCanvas` | 3.44 kB |
|
|
919
1102
|
| `useMutation` | 1.36 kB |
|
|
920
1103
|
| `usePointer` | 1.48 kB |
|
|
921
|
-
| `
|
|
922
|
-
| `
|
|
1104
|
+
| `useScroll` | 1.72 kB |
|
|
1105
|
+
| `useThrottledCallback` | 797 B |
|
|
1106
|
+
| `useDebouncedCallback` | 688 B |
|
|
1107
|
+
| `useTween` | 619 B |
|
|
1108
|
+
| `usePresence` | 591 B |
|
|
923
1109
|
| `useScrollProgress` | 993 B |
|
|
924
|
-
| `useSize` |
|
|
925
|
-
| `useContainerQuery` |
|
|
1110
|
+
| `useSize` | 378 B |
|
|
1111
|
+
| `useContainerQuery` | 384 B |
|
|
926
1112
|
| `useMediaQuery` | 246 B |
|
|
927
1113
|
| `usePrefersReducedMotion` | 272 B |
|
|
928
1114
|
| `useDevicePixelRatio` | 231 B |
|
|
929
1115
|
| `useSyncedRef` | 22 B |
|
|
930
1116
|
| `useStableCallback` | 39 B |
|
|
931
|
-
| `Presence` |
|
|
1117
|
+
| `Presence` | 741 B |
|
|
932
1118
|
| `WhenVisible` | 1.44 kB |
|
|
933
1119
|
| `WhenIdle` | 593 B |
|
|
934
|
-
| `Defer` |
|
|
1120
|
+
| `Defer` | 86 B |
|
|
935
1121
|
| `useIdle` | 435 B |
|
|
936
|
-
| `useWhenIdle` |
|
|
1122
|
+
| `useWhenIdle` | 445 B |
|
|
937
1123
|
| `useRenderState` | 527 B |
|
|
938
|
-
| `Swap` | 1.
|
|
1124
|
+
| `Swap` | 1.13 kB |
|
|
939
1125
|
|
|
940
1126
|
<!-- SIZE-TABLE:END -->
|
|
941
1127
|
|