@astryxdesign/core 0.7.6 → 0.7.7-canary.0eb44a2
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/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +23 -3
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +16 -9
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +3 -2
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -0
- package/dist/Chat/ChatMessage.js +2 -2
- package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
- package/dist/Chat/chatMessage.vars.stylex.js +8 -8
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +8 -1
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +72 -2
- package/dist/DateInput/Wheel.d.ts +17 -1
- package/dist/DateInput/Wheel.d.ts.map +1 -1
- package/dist/DateInput/Wheel.js +179 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +63 -49
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +37 -1
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +6 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
- package/dist/Drawer/useDrawerDialogPresence.js +4 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +20 -4
- package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
- package/dist/DropdownMenu/useMenuOverflow.js +31 -8
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +8 -1
- package/dist/Field/textEntryTraits.d.ts +6 -6
- package/dist/Field/textEntryTraits.js +6 -6
- package/dist/Layer/layerClickBoundary.d.ts +7 -1
- package/dist/Layer/layerClickBoundary.d.ts.map +1 -1
- package/dist/Layer/layerClickBoundary.js +2 -2
- package/dist/Layer/usePressPin.d.ts +95 -0
- package/dist/Layer/usePressPin.d.ts.map +1 -0
- package/dist/Layer/usePressPin.js +365 -0
- package/dist/Layer/useTouchTrigger.d.ts +23 -1
- package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
- package/dist/Layer/useTouchTrigger.js +45 -7
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +2 -1
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +1 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +56 -7
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +38 -30
- package/dist/ProgressBar/ProgressBar.d.ts +1 -1
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +52 -14
- package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
- package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
- package/dist/ProgressBar/ProgressBarTouch.js +91 -0
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +2 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +25 -13
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +404 -131
- package/dist/Slider/sliderTouchController.d.ts +189 -0
- package/dist/Slider/sliderTouchController.d.ts.map +1 -0
- package/dist/Slider/sliderTouchController.js +1017 -0
- package/dist/Slider/useSliderTouch.d.ts +22 -0
- package/dist/Slider/useSliderTouch.d.ts.map +1 -0
- package/dist/Slider/useSliderTouch.js +56 -0
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +200 -46
- package/dist/Switch/switchFill.d.ts +31 -0
- package/dist/Switch/switchFill.d.ts.map +1 -0
- package/dist/Switch/switchFill.js +88 -0
- package/dist/Switch/switchTouch.d.ts +81 -0
- package/dist/Switch/switchTouch.d.ts.map +1 -0
- package/dist/Switch/switchTouch.js +133 -0
- package/dist/Switch/useSwitchTouch.d.ts +285 -0
- package/dist/Switch/useSwitchTouch.d.ts.map +1 -0
- package/dist/Switch/useSwitchTouch.js +998 -0
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +4 -3
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +12 -5
- package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts +3 -0
- package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -0
- package/dist/Tokenizer/TokenizerItemRemovableContext.js +17 -0
- package/dist/Tooltip/Tooltip.d.ts +30 -6
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +5 -0
- package/dist/Tooltip/index.d.ts +1 -1
- package/dist/Tooltip/index.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +31 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +59 -18
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +19 -9
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +26 -15
- package/dist/astryx.css +91 -8
- package/dist/gestures/GestureArena.d.ts +21 -2
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +23 -3
- package/dist/gestures/GestureHost.d.ts +123 -3
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +797 -15
- package/dist/gestures/GestureRecognizer.d.ts +15 -1
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +12 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.js +10 -0
- package/dist/gestures/PressTracker.d.ts +6 -0
- package/dist/gestures/PressTracker.d.ts.map +1 -1
- package/dist/gestures/PressTracker.js +3 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
- package/dist/gestures/nativeScroll.d.ts +16 -2
- package/dist/gestures/nativeScroll.d.ts.map +1 -1
- package/dist/gestures/nativeScroll.js +39 -8
- package/dist/gestures/recognizerInternals.d.ts +4 -2
- package/dist/gestures/recognizerInternals.d.ts.map +1 -1
- package/dist/gestures/types.d.ts +17 -0
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +7 -0
- package/dist/gestures/usePressTracker.d.ts.map +1 -1
- package/dist/gestures/usePressTracker.js +1 -0
- package/dist/gestures/wheelSegmenter.d.ts +56 -0
- package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
- package/dist/gestures/wheelSegmenter.js +122 -0
- package/dist/hooks/useAttachment.d.ts +16 -8
- package/dist/hooks/useAttachment.d.ts.map +1 -1
- package/dist/hooks/useAttachment.js +35 -15
- package/dist/hooks/useFocusTrap.d.ts +15 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +13 -2
- package/dist/hooks/usePager.d.ts +261 -0
- package/dist/hooks/usePager.d.ts.map +1 -0
- package/dist/hooks/usePager.js +1169 -0
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +140 -19
- package/dist/motion/paging.d.ts +69 -0
- package/dist/motion/paging.d.ts.map +1 -0
- package/dist/motion/paging.js +148 -0
- package/dist/motion/uikit/index.d.ts +8 -4
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +8 -4
- package/dist/motion/uikit/paging.d.ts +171 -0
- package/dist/motion/uikit/paging.d.ts.map +1 -0
- package/dist/motion/uikit/paging.js +192 -0
- package/dist/motion/uikit/progressBar.d.ts +79 -0
- package/dist/motion/uikit/progressBar.d.ts.map +1 -0
- package/dist/motion/uikit/progressBar.js +102 -0
- package/dist/motion/uikit/scroll.d.ts +0 -19
- package/dist/motion/uikit/scroll.d.ts.map +1 -1
- package/dist/motion/uikit/scroll.js +2 -21
- package/dist/motion/uikit/slider.d.ts +97 -0
- package/dist/motion/uikit/slider.d.ts.map +1 -0
- package/dist/motion/uikit/slider.js +111 -0
- package/dist/motion/uikit/springs.d.ts +59 -30
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +64 -34
- package/dist/motion/uikit/switch.d.ts +166 -0
- package/dist/motion/uikit/switch.d.ts.map +1 -0
- package/dist/motion/uikit/switch.js +186 -0
- package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts +18 -0
- package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/pressHoldsDrag.stylex.js +28 -0
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +5 -0
- package/dist/utils/selectionLock.d.ts +43 -0
- package/dist/utils/selectionLock.d.ts.map +1 -1
- package/dist/utils/selectionLock.js +394 -6
- package/package.json +5 -5
- package/src/AppShell/AppShell.doc.mjs +10 -0
- package/src/AppShell/AppShell.tsx +48 -2
- package/src/Avatar/Avatar.tsx +3 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Button/Button.tsx +3 -2
- package/src/Chat/Chat.doc.mjs +1 -0
- package/src/Chat/ChatComposerInput.doc.mjs +11 -0
- package/src/Chat/ChatComposerInput.tsx +11 -0
- package/src/Chat/ChatMessage.tsx +2 -2
- package/src/Chat/chatMessage.vars.stylex.ts +8 -8
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
- package/src/DateInput/MonthScroller.tsx +63 -5
- package/src/DateInput/Wheel.tsx +174 -14
- package/src/DateRangeInput/DateRangeInput.doc.mjs +3 -2
- package/src/DateRangeInput/DateRangeInput.tsx +117 -71
- package/src/Dialog/Dialog.tsx +37 -1
- package/src/Drawer/Drawer.tsx +6 -0
- package/src/Drawer/useDrawerDialogPresence.ts +4 -0
- package/src/DropdownMenu/DropdownMenu.tsx +28 -6
- package/src/DropdownMenu/useMenuOverflow.ts +44 -8
- package/src/Field/PanelSearchInput.tsx +6 -0
- package/src/Field/textEntryTraits.ts +6 -6
- package/src/Layer/layerClickBoundary.ts +2 -2
- package/src/Layer/usePressPin.ts +516 -0
- package/src/Layer/useTouchTrigger.ts +56 -6
- package/src/Lightbox/Lightbox.tsx +2 -1
- package/src/Link/Link.tsx +3 -0
- package/src/Popover/usePopover.tsx +71 -9
- package/src/PowerSearch/PowerSearch.tsx +38 -31
- package/src/ProgressBar/ProgressBar.doc.mjs +7 -5
- package/src/ProgressBar/ProgressBar.tsx +147 -16
- package/src/ProgressBar/ProgressBarTouch.ts +109 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +2 -1
- package/src/Selector/Selector.tsx +29 -2
- package/src/Slider/Slider.doc.mjs +11 -7
- package/src/Slider/Slider.tsx +539 -132
- package/src/Slider/sliderTouchController.ts +1209 -0
- package/src/Slider/useSliderTouch.ts +78 -0
- package/src/Switch/Switch.doc.mjs +3 -3
- package/src/Switch/Switch.tsx +412 -86
- package/src/Switch/switchFill.ts +106 -0
- package/src/Switch/switchTouch.ts +194 -0
- package/src/Switch/useSwitchTouch.ts +1121 -0
- package/src/TabList/Tab.tsx +5 -3
- package/src/Tokenizer/Tokenizer.tsx +15 -5
- package/src/Tokenizer/TokenizerItemRemovableContext.ts +22 -0
- package/src/Tooltip/Tooltip.doc.mjs +21 -6
- package/src/Tooltip/Tooltip.tsx +42 -5
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/useTooltip.doc.mjs +4 -2
- package/src/Tooltip/useTooltip.tsx +84 -21
- package/src/Typeahead/BaseTypeahead.tsx +16 -8
- package/src/Typeahead/Typeahead.tsx +29 -15
- package/src/gestures/GestureArena.ts +45 -4
- package/src/gestures/GestureHost.ts +958 -15
- package/src/gestures/GestureRecognizer.ts +28 -0
- package/src/gestures/PanGestureRecognizer.ts +8 -0
- package/src/gestures/PinchGestureRecognizer.ts +16 -1
- package/src/gestures/PressTracker.ts +10 -0
- package/src/gestures/README.md +82 -3
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
- package/src/gestures/nativeScroll.ts +49 -9
- package/src/gestures/recognizerInternals.ts +9 -2
- package/src/gestures/types.ts +18 -0
- package/src/gestures/usePressTracker.ts +2 -0
- package/src/gestures/wheelSegmenter.ts +143 -0
- package/src/hooks/useAttachment.ts +56 -14
- package/src/hooks/useFocusTrap.ts +26 -2
- package/src/hooks/usePager.ts +1446 -0
- package/src/motion/MEASUREMENTS.md +36 -27
- package/src/motion/README.md +7 -3
- package/src/motion/animator.ts +193 -25
- package/src/motion/paging.ts +161 -0
- package/src/motion/uikit/index.ts +7 -4
- package/src/motion/uikit/paging.ts +193 -0
- package/src/motion/uikit/progressBar.ts +102 -0
- package/src/motion/uikit/scroll.ts +2 -18
- package/src/motion/uikit/slider.ts +105 -0
- package/src/motion/uikit/springs.ts +53 -32
- package/src/motion/uikit/switch.ts +174 -0
- package/src/privateAttributes/pressHoldsDrag.stylex.ts +28 -0
- package/src/utils/pressFeedback.ts +5 -0
- package/src/utils/selectionLock.ts +501 -6
- package/dist/motion/selectPage.d.ts +0 -26
- package/dist/motion/selectPage.d.ts.map +0 -1
- package/dist/motion/selectPage.js +0 -55
- package/src/motion/selectPage.ts +0 -55
|
@@ -37,27 +37,28 @@ installed); rows marked † also on iOS 18 for the pre-glass delta. iPhone
|
|
|
37
37
|
|
|
38
38
|
## Shared (foundation)
|
|
39
39
|
|
|
40
|
-
| ID | What | App and gesture | Extract
|
|
41
|
-
| ----- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
-
| M-S1 | Deceleration rates | none | `UIScrollView.DecelerationRate.normal.rawValue`, `.fast.rawValue`
|
|
43
|
-
| M-S2 | Momentum coast | A long list (Contacts, or a probe table of 500 rows): flicks released at 1000, 2500, 5000 pt/s | contentOffset(t) from release to rest; decay constant; per-millisecond vs per-frame model; the speed at which it stops
|
|
44
|
-
| M-S3 | Bounce-back spring | Same list: drag 120 pt past the top, hold still, release; a flick into the top edge at 3000 pt/s | contentOffset(t) after release and after edge contact; spring response and damping ratio; velocity hand-off
|
|
45
|
-
| M-S5 | Rubber band | Same list on an 852 pt view: drag 100, 300, 600 pt past the top; a horizontal pager | displayed overflow against finger overflow; confirm c = 0.55 and d = the view's size on the axis
|
|
46
|
-
| M-PG1 | Paging | Photos full-screen swipes; a probe paging scroll view | page chosen per release position × velocity (thresholds; at most one page?); contentOffset(t) onto the page
|
|
47
|
-
| M-ST1 | Scroll to top | Settings scrolled down 500, 2000, 8000 pt; tap the status bar | contentOffset(t); duration against distance; curve or spring fit
|
|
48
|
-
| M-N1 | Navigation push | Settings: tap General | incoming leading edge x(t), outgoing x(t) (parallax fraction), dimming alpha(t); animation parameters
|
|
49
|
-
| M-N2 | Navigation pop | General: back button | same as M-N1
|
|
50
|
-
| M-N3 | Interactive pop | Settings ▸ General: edge swipe released at 40 % of the width at 600 pt/s (finishes) and at 30 % at rest (cancels) | x(t) after release; spring parameters; the finish / cancel rule (position, velocity)
|
|
51
|
-
| M-A1 | Alert | Probe app: `UIAlertController(.alert)` presented and dismissed | scale(t), alpha(t) of the alert and the dimming view; animation parameters
|
|
52
|
-
| M-AS1 | Action sheet and menu | Probe app: `.actionSheet`; a button with a `UIMenu` as its primary action; Files: the … menu | scale, alpha, position (t) on open and close; animation parameters
|
|
53
|
-
| M-CM1 | Context menu | Files: long-press an item; dismiss by tapping outside | preview scale(t) on lift, backdrop blur and dim (t), menu scale(t); the preview's return on dismiss
|
|
54
|
-
| M-K1 | UIKit legacy spring | Probe app: `UIView.animate(withDuration:usingSpringWithDamping:initialSpringVelocity:)`, durations 0.3, 0.5, 1 × ratios 0.5, 0.8, 1 × velocities 0, 2 | the `CASpringAnimation` UIKit attaches: mass, stiffness, damping, initialVelocity, duration, settlingDuration
|
|
55
|
-
| M-K2 | Default spring, keyboard | Probe app: an animator with `UISpringTimingParameters()`; focus and blur a text field | both springs' parameters and allowsOverdamping; the keyboard notification's duration and curve
|
|
56
|
-
| M-CA1 | Settling duration | none: `CASpringAnimation` mass 1, stiffness 100, damping 5, 10, 15, 20, 40 × initialVelocity 0, 5, −5 × allowsOverdamping on, off | `settlingDuration` of each (checks the critically damped and overdamped branches)
|
|
57
|
-
| M-T1 | Banner | A notification pushed to the simulator while an app is in the foreground | banner y(t) in and out; time on screen
|
|
58
|
-
| M-RM1 | Reduced Motion | Reduce Motion on, Prefer Cross-Fade Transitions on and off: Settings push and pop, an alert (sheets: M-SH13; controls: M-SW11, M-SL12, M-SG15) | which transitions become cross-fades; alpha(t), duration and curve; what still moves
|
|
59
|
-
| M-PR1 | Press release curve | Settings and Contacts: a row tapped and released (the deselect cross-fade) | the timing function Core Animation attaches to the deselect fade (control points)
|
|
60
|
-
| M-NB1 | Navigation bar hide / show | none | `UINavigationController.hideShowBarDuration`
|
|
40
|
+
| ID | What | App and gesture | Extract | Method | Feeds |
|
|
41
|
+
| ----- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| M-S1 | Deceleration rates | none | `UIScrollView.DecelerationRate.normal.rawValue`, `.fast.rawValue` | D | `scroll.decelerationRate` |
|
|
43
|
+
| M-S2 | Momentum coast | A long list (Contacts, or a probe table of 500 rows): flicks released at 1000, 2500, 5000 pt/s | contentOffset(t) from release to rest; decay constant; per-millisecond vs per-frame model; the speed at which it stops | C | `scroll.decelerationRate` |
|
|
44
|
+
| M-S3 | Bounce-back spring | Same list: drag 120 pt past the top, hold still, release; a flick into the top edge at 3000 pt/s | contentOffset(t) after release and after edge contact; spring response and damping ratio; velocity hand-off | C | `scroll.bounce` |
|
|
45
|
+
| M-S5 | Rubber band | Same list on an 852 pt view: drag 100, 300, 600 pt past the top; a horizontal pager | displayed overflow against finger overflow; confirm c = 0.55 and d = the view's size on the axis | C | `scroll.rubberBand` |
|
|
46
|
+
| M-PG1 | Paging | Photos full-screen swipes; a probe paging scroll view | page chosen per release position × velocity (thresholds; at most one page?); contentOffset(t) onto the page | B, C | `paging` |
|
|
47
|
+
| M-ST1 | Scroll to top | Settings scrolled down 500, 2000, 8000 pt; tap the status bar | contentOffset(t); duration against distance; curve or spring fit | B, C | `scroll.scrollToTopRate` |
|
|
48
|
+
| M-N1 | Navigation push | Settings: tap General | incoming leading edge x(t), outgoing x(t) (parallax fraction), dimming alpha(t); animation parameters | A, B | `springs.navigationPush` |
|
|
49
|
+
| M-N2 | Navigation pop | General: back button | same as M-N1 | A, B | `springs.navigationPop` |
|
|
50
|
+
| M-N3 | Interactive pop | Settings ▸ General: edge swipe released at 40 % of the width at 600 pt/s (finishes) and at 30 % at rest (cancels) | x(t) after release; spring parameters; the finish / cancel rule (position, velocity) | A, B | `springs.navigationInteractivePopFinish` |
|
|
51
|
+
| M-A1 | Alert | Probe app: `UIAlertController(.alert)` presented and dismissed | scale(t), alpha(t) of the alert and the dimming view; animation parameters | A | `springs.alertPresent`, `.alertDismiss` |
|
|
52
|
+
| M-AS1 | Action sheet and menu | Probe app: `.actionSheet`; a button with a `UIMenu` as its primary action; Files: the … menu | scale, alpha, position (t) on open and close; animation parameters | A, B | `springs.actionSheet*`, `.menu*` |
|
|
53
|
+
| M-CM1 | Context menu | Files: long-press an item; dismiss by tapping outside | preview scale(t) on lift, backdrop blur and dim (t), menu scale(t); the preview's return on dismiss | A, B | `springs.contextMenu*` |
|
|
54
|
+
| M-K1 | UIKit legacy spring | Probe app: `UIView.animate(withDuration:usingSpringWithDamping:initialSpringVelocity:)`, durations 0.3, 0.5, 1 × ratios 0.5, 0.8, 1 × velocities 0, 2 | the `CASpringAnimation` UIKit attaches: mass, stiffness, damping, initialVelocity, duration, settlingDuration | A | `springs.ts` (UIKit's legacy springs) |
|
|
55
|
+
| M-K2 | Default spring, keyboard | Probe app: an animator with `UISpringTimingParameters()`; focus and blur a text field | both springs' parameters and allowsOverdamping; the keyboard notification's duration and curve | A, D | `springs.viewChange`, `.keyboard`, `timing.keyboard*` |
|
|
56
|
+
| M-CA1 | Settling duration | none: `CASpringAnimation` mass 1, stiffness 100, damping 5, 10, 15, 20, 40 × initialVelocity 0, 5, −5 × allowsOverdamping on, off | `settlingDuration` of each (checks the critically damped and overdamped branches) | D | `springs.ts` (settling) |
|
|
57
|
+
| M-T1 | Banner | A notification pushed to the simulator while an app is in the foreground | banner y(t) in and out; time on screen | B | `springs.banner` |
|
|
58
|
+
| M-RM1 | Reduced Motion | Reduce Motion on, Prefer Cross-Fade Transitions on and off: Settings push and pop, an alert (sheets: M-SH13; controls: M-SW11, M-SL12, M-SG15) | which transitions become cross-fades; alpha(t), duration and curve; what still moves | B | `timing.reducedMotionCrossFadeMs` (played at 200 ms: `design:reduced-motion` DR2 caps it, DEC-1 of 2026-10-24, departing from the measured 0.5 s) |
|
|
59
|
+
| M-PR1 | Press release curve | Settings and Contacts: a row tapped and released (the deselect cross-fade) | the timing function Core Animation attaches to the deselect fade (control points) | A | `timing.press.highlightOutCurve` |
|
|
60
|
+
| M-NB1 | Navigation bar hide / show | none | `UINavigationController.hideShowBarDuration` | D | `timing.navigationBarHideShowMs` |
|
|
61
|
+
| M-PB1 | Indeterminate pulse | Reduce Motion on: Safari loading a page (its indeterminate bar) beside ProgressBar's reduced-motion pulse | Not a parity value: an owner value (`component:ProgressBar` DEC-2); confirm the 2000 ms pulse reads as calm and the 0.84 floor keeps 3:1 (AR4) | B | `progressBar.reducedMotionPulseMs`, `progressBar.reducedMotionPulseFloor` |
|
|
61
62
|
|
|
62
63
|
Done (UIKit lab, iOS 26.2 simulator, Oct 23 2026; values in `uikit/` tagged
|
|
63
64
|
`measured:`): M-S1, M-NB1, M-K2, M-CA1 (underdamped grid matched to the last
|
|
@@ -72,6 +73,13 @@ initial velocity's sign flipped, a curve that overshoots. Push and pop are
|
|
|
72
73
|
not Core Animation animations on iOS 26.2: UIKit writes presentation
|
|
73
74
|
modifiers every frame from the main thread.
|
|
74
75
|
|
|
76
|
+
## Paging (UIScrollView `isPagingEnabled`, UIPageViewController)
|
|
77
|
+
|
|
78
|
+
| ID | What | App and gesture | Extract | Method | Feeds |
|
|
79
|
+
| ------ | --------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------ | ------------------------------ |
|
|
80
|
+
| M-PG12 | End of a settle | Same pager: releases at rest from 10 %, 49 %, 60 % and 90 % of a page, logged at 120 fps | remaining distance and step velocity at the frame the offset lands exactly on the page | C | `paging.integrator.restSpeed` |
|
|
81
|
+
| M-PG13 | Settle velocity floor | Same pager: steady releases from 200 to 250 pt/s in 5 pt/s steps, at 25 % of a page | the slowest release whose `scrollViewWillEndDragging` velocity is not 0 | C | `paging.release.velocityFloor` |
|
|
82
|
+
|
|
75
83
|
## Gestures (UIKit gesture recognizers)
|
|
76
84
|
|
|
77
85
|
Thresholds the gesture system reads (`uikit/gestures.ts`). Safari on iOS runs
|
|
@@ -121,12 +129,13 @@ Subject "switch S": the first switch in Settings ▸ Accessibility ▸ Display &
|
|
|
121
129
|
Text Size (Bold Text row); a minimal host app holds a UISwitch on a plain view
|
|
122
130
|
where a switch outside any scroll view is needed.
|
|
123
131
|
|
|
124
|
-
Captured (iOS 26.2 simulator, Oct 23 2026), with their values
|
|
125
|
-
switch's own table: M-SW02, M-SW03, M-SW04, M-SW05,
|
|
126
|
-
makes no haptic call), M-SW11 (Reduce Motion: the
|
|
127
|
-
goes), M-SW16 (iOS 26.2).
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
Captured (iOS 26.2 simulator, Oct 23 2026), with their values in the
|
|
133
|
+
switch's own table (`uikit/switch.ts`): M-SW02, M-SW03, M-SW04, M-SW05,
|
|
134
|
+
M-SW06, M-SW09 (UISwitch makes no haptic call), M-SW11 (Reduce Motion: the
|
|
135
|
+
lift stays, the stretch goes), M-SW16 (iOS 26.2). `springs.switchThumbMove`
|
|
136
|
+
holds the measured travel the switch's table reads; WebKit's
|
|
137
|
+
`switchThumbStretch` is retired for the table's `lift`. Partly: M-SW07 (a single
|
|
138
|
+
tap only). Still open: M-SW17, and M-SW11's On/Off
|
|
130
139
|
labels, Reduce Transparency and Bold Text.
|
|
131
140
|
|
|
132
141
|
| ID | Scene | Gesture | Extract | Method |
|
package/src/motion/README.md
CHANGED
|
@@ -7,7 +7,8 @@ source), the spring, decay, rubber-band and projection math UIKit uses, and a
|
|
|
7
7
|
compositor animator that plays it. Nothing here is exported from the package
|
|
8
8
|
entry point. Core's motion and gesture modules read the table by path;
|
|
9
9
|
components read touch motion through its internal motion roles (`roles.ts`),
|
|
10
|
-
so each keeps its measured value;
|
|
10
|
+
so each keeps its measured value; ProgressBar plays the progress table's two
|
|
11
|
+
roles through `resolveMotion`.
|
|
11
12
|
|
|
12
13
|
## Files
|
|
13
14
|
|
|
@@ -15,10 +16,13 @@ so each keeps its measured value; no shipped component reads the roles yet.
|
|
|
15
16
|
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
17
|
| `uikit/` | THE constants table: every spring, duration, deceleration rate, rubber-band constant and curve, with its source. |
|
|
17
18
|
| `uikit/springs.ts` | Interaction springs (navigation, sheets, alerts, menus, controls, keyboard, banners) and the SwiftUI / UIKit presets. |
|
|
18
|
-
| `uikit/scroll.ts` | UIScrollView: deceleration rates, rubber band, bounce-back spring,
|
|
19
|
+
| `uikit/scroll.ts` | UIScrollView: deceleration rates, rubber band, bounce-back spring, `delaysContentTouches`, scroll to top. |
|
|
19
20
|
| `uikit/timing.ts` | Non-spring clocks and curves: press highlight, Core Animation's named curves, keyboard, Reduced Motion cross-fade. |
|
|
21
|
+
| `uikit/paging.ts` | A paging UIScrollView: the integrator that settles a release on its page, the page rule, the catch, `setContentOffset(animated:)`. |
|
|
20
22
|
| `uikit/navigation.ts` | UINavigationController: push velocity, parallax, dimming, transition clock, interactive pop finish rule. |
|
|
23
|
+
| `uikit/progressBar.ts` | UIProgressView's fill: the whole-track duration and curve, its minimum length and empty alpha; the reduced-motion pulse (three roles). |
|
|
21
24
|
| `uikit/gestures.ts` | Gesture recognizer thresholds, the velocity filter and haptic stand-ins, as `{value, unit}` entries (sources in JSDoc). |
|
|
25
|
+
| `uikit/switch.ts` | UISwitch on iOS 26.2: geometry, the lift, settle, travel and tracking springs, the flip distance, the track colour. |
|
|
22
26
|
| `roles.ts` | The internal motion roles: the springs, durations and curves `spec:AST-094` FR4 names, registered by the shared tables, and a role's CSS form read back. |
|
|
23
27
|
| `roleValue.ts` | A role's CSS form, written: a spring's duration and bounce, a duration in ms, a curve's `cubic-bezier()`. |
|
|
24
28
|
| `resolveMotion.ts` | A role's value: its table value, as the per-role tables stay internal (`spec:AST-094` DEC-15). |
|
|
@@ -28,8 +32,8 @@ so each keeps its measured value; no shipped component reads the roles yet.
|
|
|
28
32
|
| `coast.ts` | A release's momentum curves for `animator.decay`: the coast, the edge spring, a release past an edge. |
|
|
29
33
|
| `rubberBand.ts` | UIScrollView's resistance past an edge, and its inverse. |
|
|
30
34
|
| `projection.ts` | Release targets: the rest point nearest the projected coast. |
|
|
35
|
+
| `paging.ts` | Paging: UIKit's paging settle as a playable curve, its velocity floor, the animated page scroll. |
|
|
31
36
|
| `resolveSnapPoint.ts` | Release policy: the snap point a release lands on (flick, back-out cancel, step limit), on `selectSnapPoint`. Internal. |
|
|
32
|
-
| `selectPage.ts` | Paging: at most one page per release, on `resolveSnapPoint`. |
|
|
33
37
|
| `bezier.ts` | Cubic-bézier timing functions (Core Animation's named curves). |
|
|
34
38
|
| `animator.ts` | Compositor animator: springs, coasts and any closed-form curve compiled to Web Animations, read back analytically. |
|
|
35
39
|
| `presentation.ts` | Where each moving element is drawn relative to where the engine hit-tests it, reported by the animator while translate runs; read by the gesture host at touch-down. |
|
package/src/motion/animator.ts
CHANGED
|
@@ -517,6 +517,14 @@ interface StartGroup {
|
|
|
517
517
|
madeInFrame?: boolean;
|
|
518
518
|
/** The task it was made in, with every call made in that task. */
|
|
519
519
|
task?: TaskMark;
|
|
520
|
+
/**
|
|
521
|
+
* Per channel, when its animation's first frame ran without a known start.
|
|
522
|
+
* That frame came too late to stand for the start, and the browser had not
|
|
523
|
+
* told it yet. The compositor starts the animation within about a frame of
|
|
524
|
+
* this time. Until the browser tells its start, a read of the channel takes
|
|
525
|
+
* the curve from here.
|
|
526
|
+
*/
|
|
527
|
+
latest?: Map<Channel, number>;
|
|
520
528
|
/** A later call in the same task that took over its curves. */
|
|
521
529
|
adoptedBy?: StartGroup;
|
|
522
530
|
readonly resolve: (start: number) => void;
|
|
@@ -1044,7 +1052,7 @@ export function createAnimator(
|
|
|
1044
1052
|
if (segment == null) {
|
|
1045
1053
|
return null;
|
|
1046
1054
|
}
|
|
1047
|
-
if (time >=
|
|
1055
|
+
if (time >= endOf(property, segment)) {
|
|
1048
1056
|
return null;
|
|
1049
1057
|
}
|
|
1050
1058
|
return segment;
|
|
@@ -1070,16 +1078,89 @@ export function createAnimator(
|
|
|
1070
1078
|
};
|
|
1071
1079
|
|
|
1072
1080
|
/**
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
|
|
1076
|
-
|
|
1081
|
+
* A held curve's start once a frame dated its call, as the browser's start
|
|
1082
|
+
* moves it: by that frame's lead over the call's own time.
|
|
1083
|
+
*/
|
|
1084
|
+
const datedFromLatest = (
|
|
1085
|
+
segment: Segment,
|
|
1086
|
+
latest: number | undefined,
|
|
1087
|
+
): {latest: number; start: number} | null =>
|
|
1088
|
+
latest == null || segment.group == null
|
|
1089
|
+
? null
|
|
1090
|
+
: {latest, start: segment.start + latest - segment.group.provisional};
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* When a waiting curve's call had its first frame, which put the curve on
|
|
1094
|
+
* screen. Undefined for a curve that does not wait, or whose call has had
|
|
1095
|
+
* no first frame.
|
|
1096
|
+
*/
|
|
1097
|
+
const shownSince = (
|
|
1098
|
+
property: MotionProperty,
|
|
1099
|
+
segment: Segment,
|
|
1100
|
+
): number | undefined =>
|
|
1101
|
+
segment.group?.resolved === false
|
|
1102
|
+
? segment.group.latest?.get(CHANNEL_OF[property])
|
|
1103
|
+
: undefined;
|
|
1104
|
+
|
|
1105
|
+
/** A curve's end, dated from its call's first frame while it waits. */
|
|
1106
|
+
const endOf = (property: MotionProperty, segment: Segment): number =>
|
|
1107
|
+
(datedFromLatest(segment, shownSince(property, segment))?.start ??
|
|
1108
|
+
segment.start) + segment.duration;
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Takes a curve on screen out of the call that still waits for the
|
|
1112
|
+
* browser's start. The curve keeps its course, dated from the call's first
|
|
1113
|
+
* frame. A call that has no channel left starts at that frame.
|
|
1114
|
+
*/
|
|
1115
|
+
const leaveWaitingCall = (
|
|
1116
|
+
segment: Segment,
|
|
1117
|
+
channel: Channel,
|
|
1118
|
+
latest: number,
|
|
1119
|
+
) => {
|
|
1120
|
+
const group = segment.group;
|
|
1121
|
+
if (group == null) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const index = group.segments.indexOf(segment);
|
|
1125
|
+
if (index >= 0) {
|
|
1126
|
+
group.segments.splice(index, 1);
|
|
1127
|
+
}
|
|
1128
|
+
segment.start += latest - group.provisional;
|
|
1129
|
+
segment.group = undefined;
|
|
1130
|
+
if (!CHANNELS[channel].some(p => segments.get(p)?.group === group)) {
|
|
1131
|
+
group.latest?.delete(channel);
|
|
1132
|
+
group.channels.delete(channel);
|
|
1133
|
+
}
|
|
1134
|
+
if (group.channels.size === 0 && group.segments.length === 0) {
|
|
1135
|
+
resolveGroup(group, latest);
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* The value on screen at `time`. Until the browser reports its start, a
|
|
1141
|
+
* held curve shows its first value. A frame that ran without that start
|
|
1142
|
+
* dates the curve. Without that frame, a later time reads from the
|
|
1143
|
+
* provisional start, so a companion that samples ahead follows the curve's
|
|
1144
|
+
* shape.
|
|
1077
1145
|
*/
|
|
1078
1146
|
const valueAt = (property: MotionProperty, time: number): number => {
|
|
1079
1147
|
const segment = segments.get(property);
|
|
1080
|
-
if (segment != null && isHeld(segment)
|
|
1148
|
+
if (segment != null && isHeld(segment)) {
|
|
1149
|
+
// The compositor starts the call near its first frame. The curve is
|
|
1150
|
+
// dated as the browser's start would date it. A curve that joined by an
|
|
1151
|
+
// older clock keeps its lead over the call's own time.
|
|
1152
|
+
const latest = segment.group?.latest?.get(CHANNEL_OF[property]);
|
|
1153
|
+
const dated = datedFromLatest(segment, latest);
|
|
1154
|
+
if (dated != null) {
|
|
1155
|
+
const elapsed = Math.max(time, dated.latest) - dated.start;
|
|
1156
|
+
return elapsed >= segment.duration
|
|
1157
|
+
? segment.target
|
|
1158
|
+
: segment.at(elapsed);
|
|
1159
|
+
}
|
|
1081
1160
|
// Nothing has moved on screen until the browser starts the call.
|
|
1082
|
-
|
|
1161
|
+
if (time <= now()) {
|
|
1162
|
+
return segment.at(0);
|
|
1163
|
+
}
|
|
1083
1164
|
}
|
|
1084
1165
|
return curveAt(property, time);
|
|
1085
1166
|
};
|
|
@@ -1087,10 +1168,22 @@ export function createAnimator(
|
|
|
1087
1168
|
const velocityAt = (property: MotionProperty, time: number): number => {
|
|
1088
1169
|
const held = segments.get(property);
|
|
1089
1170
|
if (held != null && isHeld(held)) {
|
|
1090
|
-
// Still at its start on screen, but moving in the model
|
|
1091
|
-
// carries the velocity the curve has reached by now
|
|
1092
|
-
// moves all land before the browser starts any keeps its speed.
|
|
1093
|
-
|
|
1171
|
+
// Still at its start on screen, but moving in the model. A retarget
|
|
1172
|
+
// carries the velocity the curve has reached by now. So a flick whose
|
|
1173
|
+
// moves all land before the browser starts any keeps its speed. Past
|
|
1174
|
+
// its first frame, it reads from that frame, as its value does.
|
|
1175
|
+
const dated = datedFromLatest(
|
|
1176
|
+
held,
|
|
1177
|
+
held.group?.latest?.get(CHANNEL_OF[property]),
|
|
1178
|
+
);
|
|
1179
|
+
return held.speed(
|
|
1180
|
+
Math.max(
|
|
1181
|
+
0,
|
|
1182
|
+
dated != null && time > dated.latest
|
|
1183
|
+
? time - dated.start
|
|
1184
|
+
: time - held.start,
|
|
1185
|
+
),
|
|
1186
|
+
);
|
|
1094
1187
|
}
|
|
1095
1188
|
const segment = segmentAt(property, time);
|
|
1096
1189
|
if (segment == null || time < segment.start) {
|
|
@@ -1270,12 +1363,54 @@ export function createAnimator(
|
|
|
1270
1363
|
// Settled curves become resting values.
|
|
1271
1364
|
for (const property of properties) {
|
|
1272
1365
|
const segment = segments.get(property);
|
|
1273
|
-
if (segment != null && time >=
|
|
1366
|
+
if (segment != null && time >= endOf(property, segment)) {
|
|
1274
1367
|
resting[property] = segment.target;
|
|
1275
1368
|
segments.delete(property);
|
|
1276
1369
|
}
|
|
1277
1370
|
}
|
|
1278
1371
|
const moving = properties.filter(p => segments.has(p));
|
|
1372
|
+
if (callGroup == null) {
|
|
1373
|
+
// A call that starts now rebuilds a channel the compositor has moving
|
|
1374
|
+
// since a first frame without a known start. The channel carries on
|
|
1375
|
+
// from that frame, with a start of its own, instead of starting its
|
|
1376
|
+
// curve again.
|
|
1377
|
+
for (const property of moving) {
|
|
1378
|
+
const segment = segments.get(property) as Segment;
|
|
1379
|
+
const latest = shownSince(property, segment);
|
|
1380
|
+
if (latest != null) {
|
|
1381
|
+
leaveWaitingCall(segment, channel, latest);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// An explicit call can rebuild a channel while one of its curves still
|
|
1386
|
+
// waits for the browser's start. The animation then joins the current
|
|
1387
|
+
// call, or else that waiting call, with no start of its own.
|
|
1388
|
+
const waiting =
|
|
1389
|
+
callGroup ??
|
|
1390
|
+
moving
|
|
1391
|
+
.map(p => (segments.get(p) as Segment).group)
|
|
1392
|
+
.find(group => group != null && !group.resolved) ??
|
|
1393
|
+
null;
|
|
1394
|
+
const rebuiltAfterFirstFrame = waiting?.framed === true;
|
|
1395
|
+
// The new animation starts at the frame that commits it, from its first
|
|
1396
|
+
// keyframe. That holds for a waiting call it joins after that call's
|
|
1397
|
+
// first frame, and for every waiting call it takes a curve from. So this
|
|
1398
|
+
// channel's reads below, and until that frame, take that first value.
|
|
1399
|
+
const restarted = new Set<StartGroup>();
|
|
1400
|
+
if (rebuiltAfterFirstFrame) {
|
|
1401
|
+
restarted.add(waiting);
|
|
1402
|
+
}
|
|
1403
|
+
if (callGroup != null) {
|
|
1404
|
+
for (const property of moving) {
|
|
1405
|
+
const group = (segments.get(property) as Segment).group;
|
|
1406
|
+
if (group != null && group !== callGroup && !group.resolved) {
|
|
1407
|
+
restarted.add(group);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
for (const group of restarted) {
|
|
1412
|
+
group.latest?.delete(channel);
|
|
1413
|
+
}
|
|
1279
1414
|
if (channel === 'translate') {
|
|
1280
1415
|
// The animation built below starts from the value at `time` (its first
|
|
1281
1416
|
// keyframe); a rebase or a retarget builds a new one, so a new start.
|
|
@@ -1308,15 +1443,6 @@ export function createAnimator(
|
|
|
1308
1443
|
previous?.cancel();
|
|
1309
1444
|
return;
|
|
1310
1445
|
}
|
|
1311
|
-
// A channel rebuilt by an explicit call (set, rebase, a retarget of
|
|
1312
|
-
// another axis) while one of its curves still waits for the browser's
|
|
1313
|
-
// start keeps that start: no explicit start for this animation.
|
|
1314
|
-
const waiting =
|
|
1315
|
-
callGroup ??
|
|
1316
|
-
moving
|
|
1317
|
-
.map(p => (segments.get(p) as Segment).group)
|
|
1318
|
-
.find(group => group != null && !group.resolved) ??
|
|
1319
|
-
null;
|
|
1320
1446
|
// The keyframes start where the animation will: at the waiting call's
|
|
1321
1447
|
// own time, which its start moves to the browser's (resolveGroup), so a
|
|
1322
1448
|
// write later in the task keeps the screen on the model's clock.
|
|
@@ -1431,6 +1557,20 @@ export function createAnimator(
|
|
|
1431
1557
|
waiting.channels.add(channel);
|
|
1432
1558
|
// A cancel before the first frame rejects its ready promise.
|
|
1433
1559
|
(animation as Partial<Animation>).ready?.catch(() => {});
|
|
1560
|
+
if (rebuiltAfterFirstFrame) {
|
|
1561
|
+
// The frame that commits it dates this channel's start again. The
|
|
1562
|
+
// call's other channels keep their own.
|
|
1563
|
+
requestFrame?.(() => {
|
|
1564
|
+
if (
|
|
1565
|
+
!waiting.resolved &&
|
|
1566
|
+
running.get(channel) === animation &&
|
|
1567
|
+
animation.startTime == null &&
|
|
1568
|
+
animation.playState === 'running'
|
|
1569
|
+
) {
|
|
1570
|
+
(waiting.latest ??= new Map()).set(channel, now());
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1434
1574
|
} else {
|
|
1435
1575
|
// One clock: the keyframes were sampled from `time`.
|
|
1436
1576
|
animation.startTime = time;
|
|
@@ -1937,10 +2077,14 @@ export function createAnimator(
|
|
|
1937
2077
|
for (const channel of start === 'commit' ? [] : channels) {
|
|
1938
2078
|
for (const property of CHANNELS[channel]) {
|
|
1939
2079
|
const segment = segments.get(property);
|
|
2080
|
+
// A call's first frame puts its channels on screen, even before the
|
|
2081
|
+
// browser reports the start. So such a channel is not at rest either.
|
|
1940
2082
|
if (
|
|
1941
2083
|
segment != null &&
|
|
1942
|
-
time <
|
|
1943
|
-
(segment.group == null ||
|
|
2084
|
+
time < endOf(property, segment) &&
|
|
2085
|
+
(segment.group == null ||
|
|
2086
|
+
segment.group.resolved ||
|
|
2087
|
+
shownSince(property, segment) != null)
|
|
1944
2088
|
) {
|
|
1945
2089
|
return null;
|
|
1946
2090
|
}
|
|
@@ -2135,6 +2279,22 @@ export function createAnimator(
|
|
|
2135
2279
|
) {
|
|
2136
2280
|
resolveGroup(group, now());
|
|
2137
2281
|
}
|
|
2282
|
+
// The browser's start is still unread. The compositor starts the call
|
|
2283
|
+
// near this frame, so each moving channel's reads take the curve from
|
|
2284
|
+
// now until the browser tells its start.
|
|
2285
|
+
if (!group.resolved) {
|
|
2286
|
+
for (const channel of group.channels) {
|
|
2287
|
+
const animation = running.get(channel);
|
|
2288
|
+
if (
|
|
2289
|
+
animation != null &&
|
|
2290
|
+
group.animations.includes(animation) &&
|
|
2291
|
+
animation.startTime == null &&
|
|
2292
|
+
animation.playState === 'running'
|
|
2293
|
+
) {
|
|
2294
|
+
(group.latest ??= new Map()).set(channel, now());
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2138
2298
|
});
|
|
2139
2299
|
ready.then(
|
|
2140
2300
|
() => syncGroup(group),
|
|
@@ -3643,9 +3803,11 @@ export function createAnimator(
|
|
|
3643
3803
|
}
|
|
3644
3804
|
owners.add(segment.owner);
|
|
3645
3805
|
// A curve still waiting for the browser's start stays in its call:
|
|
3646
|
-
// it starts with that call's commit.
|
|
3806
|
+
// it starts with that call's commit. A curve whose call had its first
|
|
3807
|
+
// frame is on screen already, so it leaves the call (leaveWaitingCall).
|
|
3808
|
+
const latest = shownSince(property, segment);
|
|
3647
3809
|
const group =
|
|
3648
|
-
segment.group != null && !segment.group.resolved
|
|
3810
|
+
latest == null && segment.group != null && !segment.group.resolved
|
|
3649
3811
|
? segment.group
|
|
3650
3812
|
: undefined;
|
|
3651
3813
|
const replace = (next: Segment) => {
|
|
@@ -3662,6 +3824,9 @@ export function createAnimator(
|
|
|
3662
3824
|
if (segment.spring != null) {
|
|
3663
3825
|
const from = valueAt(property, time) + delta;
|
|
3664
3826
|
const velocity = velocityAt(property, time);
|
|
3827
|
+
if (latest != null) {
|
|
3828
|
+
leaveWaitingCall(segment, CHANNEL_OF[property], latest);
|
|
3829
|
+
}
|
|
3665
3830
|
const curve = solveSpring(toSpring(segment.spring), {
|
|
3666
3831
|
from,
|
|
3667
3832
|
to: segment.target,
|
|
@@ -3682,6 +3847,9 @@ export function createAnimator(
|
|
|
3682
3847
|
spring: segment.spring,
|
|
3683
3848
|
});
|
|
3684
3849
|
} else {
|
|
3850
|
+
if (latest != null) {
|
|
3851
|
+
leaveWaitingCall(segment, CHANNEL_OF[property], latest);
|
|
3852
|
+
}
|
|
3685
3853
|
replace({
|
|
3686
3854
|
...segment,
|
|
3687
3855
|
target: segment.target + delta,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file paging.ts
|
|
5
|
+
* @input A release velocity, a settle's start, end and velocity, and the UIKit
|
|
6
|
+
* paging constants
|
|
7
|
+
* @output Exports `pagingSettleVelocity`, `solvePaging` (UIKit's paging settle
|
|
8
|
+
* as a curve the animator plays) and `solveAnimatedPageScroll`
|
|
9
|
+
* @position Pure math, with no DOM and no React; hooks/usePager.ts reads it
|
|
10
|
+
*
|
|
11
|
+
* SYNC: When modified, update:
|
|
12
|
+
* - /packages/core/src/motion/paging.test.ts
|
|
13
|
+
* - /packages/core/src/motion/README.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {MotionCurve} from './animator';
|
|
17
|
+
import {cubicBezier, type BezierPoints} from './bezier';
|
|
18
|
+
import {paging} from './uikit/paging';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The velocity a paging settle starts at. It is the release velocity that every
|
|
22
|
+
* gesture reads, zeroed below `velocityFloor`. The page choice reads the
|
|
23
|
+
* release velocity itself.
|
|
24
|
+
*/
|
|
25
|
+
export function pagingSettleVelocity(velocity: number): number {
|
|
26
|
+
return Math.abs(velocity) < paging.release.velocityFloor ? 0 : velocity;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A paging settle: the curve and where it ends. */
|
|
30
|
+
export interface PagingCurve extends MotionCurve {
|
|
31
|
+
readonly from: number;
|
|
32
|
+
readonly to: number;
|
|
33
|
+
/** Integration steps the settle took. */
|
|
34
|
+
readonly steps: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* UIKit's paging settle from `from` (px), moving at `velocity` (px/s), onto
|
|
39
|
+
* `to`. It steps `paging.integrator` `stepsPerSecond` times a second and ends
|
|
40
|
+
* exactly on `to`. The same motion returns an overscrolled pager to its first
|
|
41
|
+
* or last page.
|
|
42
|
+
*
|
|
43
|
+
* Positions are content offsets along the paging axis, 0 at page 0 and growing
|
|
44
|
+
* toward later pages. Velocities are px/s of that offset.
|
|
45
|
+
*
|
|
46
|
+
* No closed form reproduces the per-frame stepping, so the settle integrates
|
|
47
|
+
* once into a table, and the curve reads the table. A release needs at most
|
|
48
|
+
* about 400 steps, well under a millisecond of work.
|
|
49
|
+
*
|
|
50
|
+
* The release velocity enters in px per millisecond per step, as UIKit hands it
|
|
51
|
+
* to the integrator. At 960 steps a second, the content starts at 96 % of the
|
|
52
|
+
* finger's speed, as in UIKit.
|
|
53
|
+
*/
|
|
54
|
+
export function solvePaging({
|
|
55
|
+
from,
|
|
56
|
+
velocity,
|
|
57
|
+
to,
|
|
58
|
+
}: {
|
|
59
|
+
from: number;
|
|
60
|
+
velocity: number;
|
|
61
|
+
to: number;
|
|
62
|
+
}): PagingCurve {
|
|
63
|
+
const {springPull, friction, stepsPerSecond, restDistance, restSpeed} =
|
|
64
|
+
paging.integrator;
|
|
65
|
+
const xs: number[] = [from];
|
|
66
|
+
const vs: number[] = [velocity / 1000];
|
|
67
|
+
let x = from;
|
|
68
|
+
let v = velocity / 1000;
|
|
69
|
+
// ~10 s of steps: far beyond any settle (≈ 0.35 s from rest).
|
|
70
|
+
const maxSteps = stepsPerSecond * 10;
|
|
71
|
+
let steps = 0;
|
|
72
|
+
while (steps < maxSteps) {
|
|
73
|
+
if (Math.abs(to - x) < restDistance && Math.abs(v) < restSpeed) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
v = (v + (to - x) * springPull) * friction;
|
|
77
|
+
x += v;
|
|
78
|
+
steps += 1;
|
|
79
|
+
xs.push(x);
|
|
80
|
+
vs.push(v);
|
|
81
|
+
}
|
|
82
|
+
xs[xs.length - 1] = to;
|
|
83
|
+
vs[vs.length - 1] = 0;
|
|
84
|
+
const duration = steps / stepsPerSecond;
|
|
85
|
+
const last = xs.length - 1;
|
|
86
|
+
const read = (table: number[], t: number): number => {
|
|
87
|
+
if (!(t > 0)) {
|
|
88
|
+
return table[0];
|
|
89
|
+
}
|
|
90
|
+
const s = t * stepsPerSecond;
|
|
91
|
+
if (s >= last) {
|
|
92
|
+
return table[last];
|
|
93
|
+
}
|
|
94
|
+
const i = Math.floor(s);
|
|
95
|
+
const f = s - i;
|
|
96
|
+
return table[i] + (table[i + 1] - table[i]) * f;
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
from,
|
|
100
|
+
to,
|
|
101
|
+
steps,
|
|
102
|
+
duration,
|
|
103
|
+
at: t => read(xs, t),
|
|
104
|
+
// px per step × steps per second
|
|
105
|
+
velocity: t => read(vs, t) * stepsPerSecond,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `setContentOffset(_:animated:)`: `paging.animatedScroll.durationMs` on its
|
|
111
|
+
* curve, from the current offset, at any distance. The pager passes the
|
|
112
|
+
* duration and curve that `resolveMotion` gives, which are the measured values
|
|
113
|
+
* of the table.
|
|
114
|
+
*
|
|
115
|
+
* `velocity` (px/s) is the velocity of a move that this one retargets. The new
|
|
116
|
+
* curve starts at it and still ends on `to`, at rest and on time. A new target
|
|
117
|
+
* keeps the current velocity, where UIKit drops it. The carry is a Hermite
|
|
118
|
+
* term, v × duration × u(1 − u)², less the curve's own starting slope. So a
|
|
119
|
+
* move from rest is the curve itself.
|
|
120
|
+
*/
|
|
121
|
+
export function solveAnimatedPageScroll({
|
|
122
|
+
from,
|
|
123
|
+
to,
|
|
124
|
+
durationMs = paging.animatedScroll.durationMs,
|
|
125
|
+
curve = paging.animatedScroll.curve,
|
|
126
|
+
velocity,
|
|
127
|
+
}: {
|
|
128
|
+
from: number;
|
|
129
|
+
to: number;
|
|
130
|
+
durationMs?: number;
|
|
131
|
+
curve?: BezierPoints;
|
|
132
|
+
velocity?: number;
|
|
133
|
+
}): MotionCurve {
|
|
134
|
+
const duration = durationMs / 1000;
|
|
135
|
+
const distance = to - from;
|
|
136
|
+
const ease = cubicBezier(curve);
|
|
137
|
+
const progress = (u: number) => ease(Math.min(1, Math.max(0, u)));
|
|
138
|
+
// The carry, in px: the retargeted velocity over the whole duration, less
|
|
139
|
+
// what the curve's own starting slope already gives.
|
|
140
|
+
const startSlope = progress(1e-4) / 1e-4;
|
|
141
|
+
const carry =
|
|
142
|
+
velocity == null ? 0 : velocity * duration - distance * startSlope;
|
|
143
|
+
const at = (t: number) => {
|
|
144
|
+
const u = Math.min(1, Math.max(0, t / duration));
|
|
145
|
+
return from + distance * progress(u) + carry * u * (1 - u) * (1 - u);
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
duration,
|
|
149
|
+
at,
|
|
150
|
+
velocity: t => {
|
|
151
|
+
if (t <= 0) {
|
|
152
|
+
return velocity ?? 0;
|
|
153
|
+
}
|
|
154
|
+
if (t >= duration) {
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
const h = Math.min(1e-4, t / 2, (duration - t) / 2);
|
|
158
|
+
return (at(t + h) - at(t - h)) / (2 * h);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|