Repository navigation
chore(core): UIKit's spring, decay, rubber-band and projection math - #8054
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
d62a71f to
c88087c
Compare
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewNo new or modified components detected. Bundle Size SummaryGzip figures measure each package's entry file, not the code an import pulls in.
Accessibility AuditStatus: No accessibility violations detected. Visual Regression48 of 502 shot(s) changed. A change here is a question, not a failure: check whether the after is the
and 28 more. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
There was a problem hiding this comment.
This looks ready to merge: the spring, coast, rubber-band and projection math matches current spec:AST-094 FR3, and its tests pin it to an independent integration and to values read from UIKit, failing whenever a formula or constant changes. The diff is 2,107 lines, over the usual 2,000-line limit, and an engineering owner has waived the split for this math. Two non-blocking follow-ups: update the description, since #8053 merged without press.highlightOutCurve and the velocity note and its size and “review the last two commits” lines no longer match the diff, and give the legacy spring’s remaining guesses in spring.ts an open measurement task, because MEASUREMENTS.md lists M-K1 as done.
[Automated review]
| * read) and below 0.1 (extrapolated as c ∝ 1 ÷ ζ). A non-zero initial | ||
| * velocity shifts UIKit's root slightly, and at low ratios makes its solver | ||
| * jump to a far smaller one (ζ 0.1, velocity 1 → ω₀·D 9.74); neither is | ||
| * modeled. Measurement task M-K1. |
There was a problem hiding this comment.
Non-blocking: MEASUREMENTS.md lists M-K1 as done (“ratios 0.1–1 in 0.05 steps”), so these guesses (off-grid ratios, ζ under 0.1, the velocity-dependent root) have no open task that can replace them. An open row there, cited here, would give them one.
One conflict: main's uikit/gestures.ts is now one export per group (meta#8468). It takes main's file, with this branch's AST-100 note on the velocity blend placed in UIKIT_VELOCITY.
… the press curve pinned) into the animator
There was a problem hiding this comment.
This looks ready to merge: the spring, coast, rubber-band and projection math matches current spec:AST-094 FR3, and its tests pin it to an independent integration and to values read from UIKit, failing whenever a formula or constant changes. The diff is 2,107 lines, over the usual 2,000-line limit, and an engineering owner has waived the split for this math. A few non-blocking follow-ups:
- The test "breaks an exact tie toward the direction of travel" (
projection.test.ts:96) projects 1e-7 px off the midpoint, so it never reaches the tie-break; a release at 49 px moving 1,000 px/s with rate 0.5 lands exactly on it. - Two guesses lack an open measurement task:
selectPage's at-most-one-page limit, which M-PG1 still asks, and the legacy spring's off-grid ratios, whose M-K1 is listed as done. - The description still says the branch carries
press.highlightOutCurveand the velocity note until #8053 merges; #8053 merged without them, so they land here, and the size and "review the last two commits" lines no longer match.
[Automated review]
| it('breaks an exact tie toward the direction of travel', () => { | ||
| expect( | ||
| selectSnapPoint([0, 100], {position: 50, velocity: 0.0001, rate: 0.5}) | ||
| .value, | ||
| ).toBe(100); | ||
| expect( | ||
| selectSnapPoint([0, 100], {position: 50, velocity: -0.0001, rate: 0.5}) | ||
| .value, | ||
| ).toBe(0); | ||
| }); |
There was a problem hiding this comment.
Non-blocking: these releases project to 50 ± 1e-7, so one end is nearer by 2e-7, outside the 1e-9 tie window in selectSnapPoint, and the tie-break never runs; inverting it (isTie && !isAhead) still passes the suite. A release that lands exactly on the midpoint exercises it:
expect(selectSnapPoint([0, 100], {position: 49, velocity: 1000, rate: 0.5}).value).toBe(100);
expect(selectSnapPoint([0, 100], {position: 51, velocity: -1000, rate: 0.5}).value).toBe(0);| * UIScrollView paging (`isPagingEnabled`): the page a release settles on, at | ||
| * most one page from `currentPage`, chosen by projecting the release with | ||
| * the paging projection rate (uikit/scroll.ts; a GUESS until measured). |
There was a problem hiding this comment.
Non-blocking: only the rate is marked a guess here, but MEASUREMENTS.md still lists "at most one page?" as open under M-PG1. Labelling the one-page limit (maxSteps: 1) a GUESS under M-PG1 as well would keep its first caller from inheriting an unmarked guess.
…rena (#8058) Depends on #8053 (and its predecessors); review the last commit. <!-- Template: other.md (internal infrastructure: nothing is exported, so new-feature.md does not fit). --> ## Primary intent Add the core of an internal gesture-recognizer system for touch presentations: UIKit's recognizer state machine and the arena that settles which recognizer owns a touch sequence. Recognizers, the DOM host and component migrations follow in their own pull requests; nothing is exported from the package. ## User or maintainer impact Touch components each carry their own pointer handling (sheet drags, swipe actions, toast dismissal, long press, press feedback), with their own answer to "who owns this touch". Two can act on one touch, and a second finger can break a drag. With one arena every component gets UIKit's answers: exclusive recognition by default, `requireToFail`, simultaneous recognition only on request, a deterministic resolution order, and the view's touches cancelled (no click, no pressed state) once a gesture takes them. ## Problem and solution fit - Why does it happen? Ownership of a touch is decided separately in each component. - Why does that hurt? Ghost clicks, double actions, and rules that differ from component to component. - Why does it matter? Touch presentations aim at UIKit parity, and UIKit settles this in one place. Deltas: - `GestureRecognizer`: - UIKit's states (possible, began, changed, ended, cancelled, failed) and allowed transitions; - discrete and continuous recognizers; - `requireToFail`, `recognizeSimultaneouslyWith`, `shouldBegin`, `shouldReceiveTouch`; - `cancelsTouchesInView`, `delaysTouchesBegan` and `delaysTouchesEnded`, `allowedPointerTypes`. - `GestureArena`, with no DOM: - participants per touch sequence, resolution and claims; - delivery to the view; - click dispositions: allow, suppress, or defer and replay if the recognizer fails; - timers as deadlines on the event clock. ## Authority `spec:AST-094` FR15 (arbitration) and FR16 (the event clock), proposed; pending owner review. ## Change and evidence The arena's rules are tested with scripted recognizers, test doubles that begin, fail or wait on command. The rules hold independently of any real recognizer: - exclusivity and simultaneity; - require-to-fail: waiting, failing, delegate requirements, idle recognizers; - delegate hooks and innermost resolution; - independent touches; - delivery to the view, deferred and replayed clicks, the browser taking a touch; - cancellation; - a deadline that runs at its own time before a late input. Size: 2,198 lines, about 400 of them tests. This is one indivisible intent: the recognizer base and the arena call each other, so neither builds or tests alone, and the doubles-only tests are the smallest proof of the rules. ## Scope - [x] This PR has one primary intent. - [x] Independently removable deltas were removed or split. - [x] Public text and artifacts contain no internal Meta context. ## Testing - `pnpm exec vitest run --project ui packages/core/src/gestures packages/core/src/motion`: 159 tests passed. - `tsc --project packages/core/tsconfig.json --noEmit`: clean. - `eslint packages/core/src/gestures`: clean. - `pnpm check:changesets`, `node scripts/release/changeset-coverage.mjs pr --base origin/main`: clean. - Size: 6 files, 2,205 lines added. Stack (merge in this order): #8052 spec → #8053 constants table → #8058 gesture core → #8059 tap and long press → #8060 pan → #8061 other recognizers → #8054 math → #8055 animator → #8056 animator curves; the gesture DOM host, press tracking and browser proof follow the animator.
… plan MEASUREMENTS.md and the motion README take this branch's lines where they conflict: its M-RM1 row (the played 200 ms fade) and the animator rows and section that meta#8054 does not carry.
…s a dependency
The touch foundation's fold, mergeable against main cd4aca6: her chain on main (meta#8058, meta#8054, meta#8059, meta#8060) and meta#8258's rework, which keeps touch motion internal (no theme surface): the generated role table, its script and the role kinds file are gone, and a table registers only the roles it lists. The motion doc and its golden take meta#8076's side; the role kinds file stays deleted.
…cation export reverted; overlap and borrow fixes) Conflicts in animator.ts and the motion README: meta#8056's Reduce Motion plan wins, which already carries a waiting move to the next call's invisible moment and keeps a running dip's borrow. meta#8055's new tests run against the plan, the re-timing one advancing the clock to the landing and the preference change written through the media stub.
meta#8076's head is F1's fold on the maintainer's chain now on main (meta#8058, meta#8054, meta#8059, meta#8060): touch motion is internal (the tables register their values as internal motion roles, with no theme surface), and the `isRtlElement` resolver. Conflicts, resolved: - The theme-roles changeset and the motion guide take meta#8076's side; their sentence on ChatLayout's Reduce Motion cross-fade goes with the theme surface. - The CLI goldens for the empty icon set take meta#8076's side and regenerate. - keyboardModel.test.ts reads the internal role registry (`motionRoleIn`) now that the generated role-kind table is gone, and no longer names a theme.
Depends on #8053 (and its predecessors); review the last two commits. Until #8053 merges at its approved head, this branch also carries two table commits for it,
press.highlightOutCurve(M-PR1) and the velocity note (spec:AST-100FR2), and its merge withmain.User need
Touch presentations need UIKit's motion math: a sheet released at 1,200 pt/s must land on the detent UIKit would pick, with UIKit's spring and the finger's velocity, and a scroll-like surface must coast and resist like a scroll view.
Problem and solution fit
Pure functions for
spec:AST-094FR3:spring.ts: the damped spring in closed form for all three regimes; conversions between SwiftUI's duration and bounce, response and damping ratio, Core Animation's mass, stiffness and damping, and UIKit's legacyusingSpringWithDamping(from UIKit's measured grid); Core Animation's settling duration; a spring chasing a target moving at constant speed. Overdamping clamps to critical, as Core Animation does.decay.ts,rubberBand.ts,projection.ts: UIScrollView's coast, its resistance past an edge (and inverse), and the release target nearest the projected rest, paging included.uikit/values.test.ts: plays the constants table through this math.One intent at 2,050 lines: each module ships with the tests that pin it to independent references.
Current specification
spec:AST-094FR3.Public delta and caller burden
None: internal, not exported, and no component uses it yet. The changeset is
[chore].Design and alternatives
toSpringnormalizes a description kept in its source's vocabulary.Evidence
settlingDurationgrid (underdamped to the last digit, critical within 0.02 %), the durations UIKit gives its spring APIs, and the legacy grid from ζ 0.1 to 1.Scope
Testing
pnpm exec vitest run --project ui packages/core/src/motion: 7 files, 316 tests passed.pnpm build,pnpm typecheck, ui and node suites,pnpm check:repo: clean.pnpm -F @astryxdesign/core typecheck: clean.ASTRYX_STRICT_LINT=1 pnpm exec eslint packages/core/src/motion: clean.pnpm check:changesets,pnpm check:repo(pre-commit): clean.Stack, in merge order: #8053, #8058, #8059, #8060, #8061, #8054, #8257, #8055, #8056, #8067, #8258, #8071, #8072, #8073, #8074, #8076, #8340; #8067 also follows #8043, #8044, #8348, #8045 and #8446. #8062 is on
main.